@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.
- package/LICENSE +21 -0
- package/LICENSE-0BSD +12 -0
- package/README.md +268 -3
- package/docs/README.md +26 -0
- package/docs/app.md +523 -0
- package/docs/build.md +169 -0
- package/docs/ipc-ws.md +202 -0
- package/docs/ipc.md +323 -0
- package/docs/native.md +1066 -0
- package/docs/page.md +71 -0
- package/docs/settings-store.md +125 -0
- package/docs/webview-library.md +104 -0
- package/docs/window-chrome.md +221 -0
- package/docs/worker.md +72 -0
- package/file-imports.d.ts +19 -0
- package/package.json +66 -4
- package/src/app.ts +350 -0
- package/src/build.ts +118 -0
- package/src/html.ts +71 -0
- package/src/index.ts +8 -0
- package/src/ipc/bridge.ts +41 -0
- package/src/ipc/frame.ts +16 -0
- package/src/ipc/index.ts +16 -0
- package/src/ipc/memory.ts +33 -0
- package/src/ipc/service.ts +75 -0
- package/src/ipc-ws/client.ts +111 -0
- package/src/ipc-ws/index.ts +5 -0
- package/src/ipc-ws/server.ts +133 -0
- package/src/make-app.ts +93 -0
- package/src/messages.ts +16 -0
- package/src/native/app-icon.ts +23 -0
- package/src/native/app-menu.ts +18 -0
- package/src/native/darwin/app-icon.ts +10 -0
- package/src/native/darwin/app-menu.ts +88 -0
- package/src/native/darwin/folder-picker.ts +29 -0
- package/src/native/darwin/image.ts +47 -0
- package/src/native/darwin/menu.ts +89 -0
- package/src/native/darwin/notifier.ts +95 -0
- package/src/native/darwin/objc.ts +246 -0
- package/src/native/darwin/overlay.ts +352 -0
- package/src/native/darwin/tray.ts +129 -0
- package/src/native/darwin/window.ts +64 -0
- package/src/native/folder-picker.ts +15 -0
- package/src/native/hit-regions.ts +162 -0
- package/src/native/icon.ts +530 -0
- package/src/native/index.ts +38 -0
- package/src/native/menu.ts +43 -0
- package/src/native/notifications.ts +40 -0
- package/src/native/overlay.ts +182 -0
- package/src/native/platform.ts +25 -0
- package/src/native/tray.ts +41 -0
- package/src/native/win32/api.ts +234 -0
- package/src/native/win32/app-icon.ts +5 -0
- package/src/native/win32/app-menu.ts +4 -0
- package/src/native/win32/folder-picker.ts +121 -0
- package/src/native/win32/icon.ts +85 -0
- package/src/native/win32/menu.ts +107 -0
- package/src/native/win32/message-window.ts +72 -0
- package/src/native/win32/notifier.ts +28 -0
- package/src/native/win32/overlay.ts +499 -0
- package/src/native/win32/tray.ts +228 -0
- package/src/native/win32/window.ts +77 -0
- package/src/native/window.ts +40 -0
- package/src/page.ts +48 -0
- package/src/ring.ts +110 -0
- package/src/settings-store.ts +101 -0
- package/src/webview-lib.ts +60 -0
- package/src/window-chrome.ts +118 -0
- package/src/worker.ts +72 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// One module per feature. Import what you use.
|
|
2
|
+
export { createTray, type Tray, type TrayIcon, type TrayOptions } from "./tray";
|
|
3
|
+
export { createNotifier, type NotificationOptions, type Notifier, type NotifierOptions } from "./notifications";
|
|
4
|
+
export { nativeWindow, type NativeWindow, type WindowHandle } from "./window";
|
|
5
|
+
export {
|
|
6
|
+
createOverlayWindow,
|
|
7
|
+
makeOverlay,
|
|
8
|
+
type Overlay,
|
|
9
|
+
type OverlayOptions,
|
|
10
|
+
type OverlayWindowOptions,
|
|
11
|
+
type HitRefiner,
|
|
12
|
+
type Point,
|
|
13
|
+
type Rect,
|
|
14
|
+
type ResizeEdge,
|
|
15
|
+
} from "./overlay";
|
|
16
|
+
export { convexHull, createHitRelay, flattenRegions, hitRegions, packRegions, unpackRegions, type HitPolygon, type HitProbe, type HitRelay } from "./hit-regions";
|
|
17
|
+
export { pickFolder } from "./folder-picker";
|
|
18
|
+
export { installAppMenu, type AppMenuOptions } from "./app-menu";
|
|
19
|
+
export { setAppIcon } from "./app-icon";
|
|
20
|
+
export { resolveMenu, type MenuItem, type MenuSource, type MenuTemplate } from "./menu";
|
|
21
|
+
export {
|
|
22
|
+
decodePng,
|
|
23
|
+
dotIcns,
|
|
24
|
+
dotIcon,
|
|
25
|
+
dotPng,
|
|
26
|
+
encodeIcns,
|
|
27
|
+
encodeIco,
|
|
28
|
+
encodePng,
|
|
29
|
+
icnsFromPng,
|
|
30
|
+
icnsFromRgba,
|
|
31
|
+
icoFromPng,
|
|
32
|
+
icoFromRgba,
|
|
33
|
+
renderDot,
|
|
34
|
+
trimTransparent,
|
|
35
|
+
type DecodedPng,
|
|
36
|
+
type DotIconOptions,
|
|
37
|
+
type Icon,
|
|
38
|
+
} from "./icon";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Menus as data. A template describes items; each platform renders it with
|
|
2
|
+
// its own menu system when the menu opens.
|
|
3
|
+
|
|
4
|
+
import type { Icon } from "./icon";
|
|
5
|
+
|
|
6
|
+
export type MenuItem =
|
|
7
|
+
| {
|
|
8
|
+
type?: "item";
|
|
9
|
+
label: string;
|
|
10
|
+
/** Greyed out and unclickable when false. Default true. */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
/** Shows a check mark. */
|
|
13
|
+
checked?: boolean;
|
|
14
|
+
/** An image before the label: 16px from the .ico on Windows, a 16pt template image on macOS. */
|
|
15
|
+
icon?: Icon;
|
|
16
|
+
click?: () => void;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
type?: "submenu";
|
|
20
|
+
label: string;
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
icon?: Icon;
|
|
23
|
+
submenu: MenuTemplate;
|
|
24
|
+
}
|
|
25
|
+
| { type: "separator" };
|
|
26
|
+
|
|
27
|
+
export type MenuTemplate = MenuItem[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A template, or a function that produces one. The function form is called
|
|
31
|
+
* every time the menu is about to open, so labels, check marks and enabled
|
|
32
|
+
* state can reflect current app state without any bookkeeping.
|
|
33
|
+
*/
|
|
34
|
+
export type MenuSource = MenuTemplate | (() => MenuTemplate);
|
|
35
|
+
|
|
36
|
+
/** A template from a source: calls the function form fresh each time. */
|
|
37
|
+
export const resolveMenu = (source: MenuSource | undefined): MenuTemplate =>
|
|
38
|
+
source === undefined ? [] : typeof source === "function" ? source() : source;
|
|
39
|
+
|
|
40
|
+
export const isSeparator = (item: MenuItem): item is { type: "separator" } => item.type === "separator";
|
|
41
|
+
|
|
42
|
+
export const isSubmenu = (item: MenuItem): item is Extract<MenuItem, { submenu: MenuTemplate }> =>
|
|
43
|
+
!isSeparator(item) && "submenu" in item;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// System notifications.
|
|
2
|
+
//
|
|
3
|
+
// Windows: the tray icon's balloon, which Windows 10/11 shows as a toast with
|
|
4
|
+
// our icon, so create the tray first. macOS: UNUserNotificationCenter, which
|
|
5
|
+
// asks the user for permission on the first notification and needs a bundle
|
|
6
|
+
// identifier, so run the .app rather than the bare binary.
|
|
7
|
+
|
|
8
|
+
import { backend } from "./platform";
|
|
9
|
+
|
|
10
|
+
export type NotificationOptions = {
|
|
11
|
+
title: string;
|
|
12
|
+
body?: string;
|
|
13
|
+
/** No sound. Default false. */
|
|
14
|
+
silent?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type NotifierOptions = {
|
|
18
|
+
/** The user clicked a notification. */
|
|
19
|
+
onClick?: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface Notifier {
|
|
23
|
+
/**
|
|
24
|
+
* Shows a notification. Returns false when the platform cannot show one
|
|
25
|
+
* right now (macOS without a bundle id; Windows after the tray is gone), in
|
|
26
|
+
* which case nothing is displayed.
|
|
27
|
+
*/
|
|
28
|
+
notify(notification: NotificationOptions): boolean;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Backend = { createNotifier(options?: NotifierOptions): Notifier };
|
|
33
|
+
const impl = backend<Backend>(
|
|
34
|
+
() => require("./win32/notifier"),
|
|
35
|
+
() => require("./darwin/notifier"),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export function createNotifier(options: NotifierOptions = {}): Notifier {
|
|
39
|
+
return impl().createNotifier(options);
|
|
40
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Turn a window someone else created (the webview's) into an overlay: no
|
|
2
|
+
// frame, above everything, off the taskbar, never takes focus, and see-through
|
|
3
|
+
// where the page paints the colour key. Then move it around.
|
|
4
|
+
//
|
|
5
|
+
// Units: physical pixels on Windows (the process is per-monitor DPI aware),
|
|
6
|
+
// points on macOS; `scale()` says how many of them one CSS pixel of the page
|
|
7
|
+
// is. Origin top-left of the primary screen on both.
|
|
8
|
+
//
|
|
9
|
+
// The colour key (Windows only) is 1-bit transparency; `transparent` is real
|
|
10
|
+
// alpha on both platforms.
|
|
11
|
+
|
|
12
|
+
import type { HitPolygon } from "./hit-regions";
|
|
13
|
+
import type { Icon } from "./icon";
|
|
14
|
+
import type { WindowHandle } from "./window";
|
|
15
|
+
import { backend } from "./platform";
|
|
16
|
+
|
|
17
|
+
export type Rect = { x: number; y: number; width: number; height: number };
|
|
18
|
+
export type Point = { x: number; y: number };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Asked while the cursor is inside a hit polygon, with the cursor in
|
|
22
|
+
* window-local units; whoever knows the exact shape (the page, with a
|
|
23
|
+
* triangle pick) calls `answer` with whether that point is really solid.
|
|
24
|
+
* Until the answer arrives the polygon's own answer stands; an answer that
|
|
25
|
+
* arrives after the cursor has moved on is dropped.
|
|
26
|
+
*/
|
|
27
|
+
export type HitRefiner = (local: Point, answer: (solid: boolean) => void) => void;
|
|
28
|
+
|
|
29
|
+
/** An edge or corner of the window, for startResize. */
|
|
30
|
+
export type ResizeEdge = "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
31
|
+
|
|
32
|
+
export type OverlayOptions = {
|
|
33
|
+
/**
|
|
34
|
+
* Honour the page's alpha: wherever the page is transparent, the desktop
|
|
35
|
+
* shows through, anti-aliased edges and all. Needs the browser to paint a
|
|
36
|
+
* transparent background too (sweetview's `transparent: true`). Clicks on
|
|
37
|
+
* transparent pixels still land on the window; use setClickThrough for
|
|
38
|
+
* whole-window pass-through.
|
|
39
|
+
*/
|
|
40
|
+
transparent?: boolean;
|
|
41
|
+
/** `#rrggbb`; pixels of exactly this colour are transparent and click-through. 1-bit; not needed with `transparent`. */
|
|
42
|
+
colorKey?: string;
|
|
43
|
+
/** Whole-window opacity, 0..1. Combines with the colour key. */
|
|
44
|
+
opacity?: number;
|
|
45
|
+
/** Above every normal window. Default true. */
|
|
46
|
+
topmost?: boolean;
|
|
47
|
+
/** No taskbar button, no Alt+Tab entry. Default true. */
|
|
48
|
+
hideFromTaskbar?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Clicking it never takes focus from the app the user is in. Default true.
|
|
51
|
+
* `false` makes a frameless window that takes focus and keyboard input (a
|
|
52
|
+
* settings window drawing its own chrome); on macOS that needs the window
|
|
53
|
+
* created by `createOverlayWindow`, which gives it a class that can become key.
|
|
54
|
+
*/
|
|
55
|
+
noActivate?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface Overlay {
|
|
59
|
+
/** The HWND / NSWindow*, for handing to sweetview as the host window. */
|
|
60
|
+
readonly handle: WindowHandle;
|
|
61
|
+
/** Shows without taking focus. */
|
|
62
|
+
show(): void;
|
|
63
|
+
hide(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Windows: the title bar, taskbar and Alt+Tab icon from `icon.windows.ico`;
|
|
66
|
+
* undefined restores the stock one. Only a framed window shows it. macOS:
|
|
67
|
+
* no-op, a window has no icon of its own there; `setAppIcon` sets the Dock's.
|
|
68
|
+
*/
|
|
69
|
+
setIcon(icon: Icon | undefined): void;
|
|
70
|
+
/** Position and size in window units (see `scale()`). */
|
|
71
|
+
bounds(): Rect;
|
|
72
|
+
moveTo(x: number, y: number): void;
|
|
73
|
+
resize(width: number, height: number): void;
|
|
74
|
+
setBounds(rect: Rect): void;
|
|
75
|
+
/** Whole-window: when on, every click falls through to what is underneath. */
|
|
76
|
+
setClickThrough(on: boolean): void;
|
|
77
|
+
/**
|
|
78
|
+
* Which parts of the window take clicks: polygons in window-local units
|
|
79
|
+
* (see `scale()`); a click anywhere else falls through to whatever is
|
|
80
|
+
* underneath. `null` means the whole window takes clicks again. Meant to
|
|
81
|
+
* be resent as things move, tens of times a second if need be: nothing is
|
|
82
|
+
* repainted for it. The page computes the polygons from what it draws
|
|
83
|
+
* (the convex hull of each mesh's projected bounding-box corners is its
|
|
84
|
+
* exact silhouette for box-shaped meshes; `convexHull` in
|
|
85
|
+
* `@n1h/sweetview/native/hit-regions` is page-safe) and this side answers the
|
|
86
|
+
* cursor itself, so the page is never in the click path.
|
|
87
|
+
*
|
|
88
|
+
* How: a 10 ms timer, and every update, test the cursor against the
|
|
89
|
+
* polygons and flip the window's pass-through state (Windows:
|
|
90
|
+
* WS_EX_TRANSPARENT with WS_EX_LAYERED, the only thing the window manager
|
|
91
|
+
* honours across processes short of a window region, which forces a
|
|
92
|
+
* repaint of the browser each time it changes; macOS: ignoresMouseEvents).
|
|
93
|
+
* Nothing changes while a mouse button is down, so a drag survives leaving
|
|
94
|
+
* the polygons. While the cursor is outside the window the window passes
|
|
95
|
+
* clicks through, so entering onto empty space is right from the first
|
|
96
|
+
* pixel; entering onto a polygon takes one tick.
|
|
97
|
+
*
|
|
98
|
+
* The polygons are the coarse answer: a mesh's bounding-box hull includes
|
|
99
|
+
* the gap between a cat's ears. With `refine`, every time the cursor is
|
|
100
|
+
* inside a polygon (and whenever the polygons change under a still cursor)
|
|
101
|
+
* the refiner is asked for the exact answer and the pass-through state
|
|
102
|
+
* follows its reply, typically a few milliseconds later; outside every
|
|
103
|
+
* polygon nobody is asked. A refiner that never answers leaves polygon
|
|
104
|
+
* precision, so a stalled page degrades, never breaks.
|
|
105
|
+
*/
|
|
106
|
+
setHitRegions(polygons: HitPolygon[] | null, refine?: HitRefiner): void;
|
|
107
|
+
setTopmost(on: boolean): void;
|
|
108
|
+
/**
|
|
109
|
+
* Hand the mouse gesture in progress to the OS as a window move, as a
|
|
110
|
+
* title bar would: called from a UI-thread method on the page's
|
|
111
|
+
* pointer-down in its own title bar, the OS then moves the window with the
|
|
112
|
+
* cursor, with snapping and all, until the button is released. A page
|
|
113
|
+
* draws its own chrome this way in a frameless window.
|
|
114
|
+
*/
|
|
115
|
+
startDrag(): void;
|
|
116
|
+
/** The same for a resize from an edge or corner (the page's own resize handles). macOS: no-op; a frameless macOS overlay has no resizable mask, so it is not resized by the user at all. */
|
|
117
|
+
startResize(edge: ResizeEdge): void;
|
|
118
|
+
minimize(): void;
|
|
119
|
+
/** Between the work area and the previous bounds (Windows); zoom (macOS). */
|
|
120
|
+
toggleMaximize(): void;
|
|
121
|
+
maximized(): boolean;
|
|
122
|
+
/** Cursor position on the screen, window units. */
|
|
123
|
+
cursor(): Point;
|
|
124
|
+
/** The whole primary monitor, window units. The overlay draws over the taskbar or Dock there. */
|
|
125
|
+
screen(): Rect;
|
|
126
|
+
/** The whole monitor under a point, window units; the nearest one when the point is on none. A window dropped on another monitor finds its new bounds with this. */
|
|
127
|
+
screenAt(point: Point): Rect;
|
|
128
|
+
/** The primary monitor minus the taskbar or Dock, window units. */
|
|
129
|
+
workArea(): Rect;
|
|
130
|
+
/** Every monitor together, window units. */
|
|
131
|
+
virtualScreen(): Rect;
|
|
132
|
+
/** Window units per CSS pixel of the page: the DPI scale on Windows, 1 on macOS. */
|
|
133
|
+
scale(): number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type OverlayWindowOptions = OverlayOptions & {
|
|
137
|
+
title?: string;
|
|
138
|
+
/** Initial bounds, window units. Default: 320x240 at the work area's centre. */
|
|
139
|
+
bounds?: Rect;
|
|
140
|
+
/**
|
|
141
|
+
* A normal window instead of an overlay: title bar and close button, a
|
|
142
|
+
* taskbar or Dock presence, activates when clicked, not above other
|
|
143
|
+
* windows, opaque. Still created hidden, so `show()` is the first time it
|
|
144
|
+
* appears, and its close button hides it rather than ending the app. For
|
|
145
|
+
* a settings window next to an overlay, hosted by the same process and OS
|
|
146
|
+
* loop. On macOS the overlay options are ignored; on Windows `frame` only
|
|
147
|
+
* changes their defaults (not topmost, in the taskbar, activating, opaque),
|
|
148
|
+
* and an option given explicitly still applies.
|
|
149
|
+
*/
|
|
150
|
+
frame?: boolean;
|
|
151
|
+
/** Framed windows: dark title bar and frame (Windows 11's immersive dark mode; the dark appearance on macOS), for a dark page. */
|
|
152
|
+
dark?: boolean;
|
|
153
|
+
/** Windows: the title bar, taskbar and Alt+Tab icon (see `Overlay.setIcon`). macOS: ignored. */
|
|
154
|
+
icon?: Icon;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
type Backend = {
|
|
158
|
+
makeOverlay(handle: WindowHandle, options: OverlayOptions): Overlay;
|
|
159
|
+
createOverlayWindow(options: OverlayWindowOptions): Overlay;
|
|
160
|
+
};
|
|
161
|
+
const impl = backend<Backend>(
|
|
162
|
+
() => require("./win32/overlay"),
|
|
163
|
+
() => require("./darwin/overlay"),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
/** Restyle an existing window (the webview's) into an overlay. */
|
|
167
|
+
export function makeOverlay(handle: WindowHandle, options: OverlayOptions = {}): Overlay {
|
|
168
|
+
return impl().makeOverlay(handle, options);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create a hidden overlay window of our own, to hand to sweetview as the host
|
|
173
|
+
* (`createWebviewApp({ window: overlay.handle })`). Transparency set up before
|
|
174
|
+
* the browser is created is what WebView2 needs for a see-through page; it
|
|
175
|
+
* ignores a host that turned transparent later. Call `show()` once the page
|
|
176
|
+
* is set. On Windows the window's procedure ends the run loop when the
|
|
177
|
+
* window is destroyed and keeps the browser sized to the client area; on
|
|
178
|
+
* macOS neither happens here.
|
|
179
|
+
*/
|
|
180
|
+
export function createOverlayWindow(options: OverlayWindowOptions = {}): Overlay {
|
|
181
|
+
return impl().createOverlayWindow(options);
|
|
182
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Internal. Picks the implementation folder for the running OS, once, lazily.
|
|
2
|
+
//
|
|
3
|
+
// `require` rather than `import()` so every public function stays synchronous:
|
|
4
|
+
// the UI thread cannot await while the webview's run loop owns it, and the
|
|
5
|
+
// bundler still embeds both folders (the strings are static), evaluating only
|
|
6
|
+
// the one the process is on.
|
|
7
|
+
|
|
8
|
+
export function backend<T>(win32: () => T, darwin: () => T): () => T {
|
|
9
|
+
let loaded: T | undefined;
|
|
10
|
+
return () => {
|
|
11
|
+
if (loaded === undefined) {
|
|
12
|
+
switch (process.platform) {
|
|
13
|
+
case "win32":
|
|
14
|
+
loaded = win32();
|
|
15
|
+
break;
|
|
16
|
+
case "darwin":
|
|
17
|
+
loaded = darwin();
|
|
18
|
+
break;
|
|
19
|
+
default:
|
|
20
|
+
throw new Error(`sweetnative: ${process.platform} is not supported (win32 and darwin are)`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return loaded;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Tray icon (Windows notification area) / menu bar status item (macOS).
|
|
2
|
+
//
|
|
3
|
+
// Runs on the UI thread. Callbacks fire on that thread from inside the OS
|
|
4
|
+
// loop: return quickly, and do not await anything that needs Bun's event loop
|
|
5
|
+
// (it is blocked while webview.run() is on the stack).
|
|
6
|
+
|
|
7
|
+
import type { Icon } from "./icon";
|
|
8
|
+
import type { MenuSource } from "./menu";
|
|
9
|
+
import { backend } from "./platform";
|
|
10
|
+
|
|
11
|
+
/** The tray's icon: an `Icon`. Windows picks the 16px image for the tray and 32px for toasts; macOS shows the PNG at 18x18 points (36px for Retina). */
|
|
12
|
+
export type TrayIcon = Icon;
|
|
13
|
+
|
|
14
|
+
export type TrayOptions = {
|
|
15
|
+
tooltip?: string;
|
|
16
|
+
icon?: TrayIcon;
|
|
17
|
+
/** Windows: opened by right click. macOS: by any click, as menu bar items do. */
|
|
18
|
+
menu?: MenuSource;
|
|
19
|
+
/** Left single click. Fires only when the click did not turn into a double click. macOS: only when no menu is set. */
|
|
20
|
+
onClick?: () => void;
|
|
21
|
+
/** Left double click. macOS: only when no menu is set. */
|
|
22
|
+
onDoubleClick?: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface Tray {
|
|
26
|
+
setTooltip(text: string): void;
|
|
27
|
+
setIcon(icon: TrayIcon): void;
|
|
28
|
+
setMenu(menu: MenuSource | undefined): void;
|
|
29
|
+
/** Removes the icon. Safe to call twice. */
|
|
30
|
+
dispose(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Backend = { createTray(options: TrayOptions): Tray };
|
|
34
|
+
const impl = backend<Backend>(
|
|
35
|
+
() => require("./win32/tray"),
|
|
36
|
+
() => require("./darwin/tray"),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export function createTray(options: TrayOptions): Tray {
|
|
40
|
+
return impl().createTray(options);
|
|
41
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// The Win32 symbols this library needs, dlopened once. Handles travel as
|
|
2
|
+
// bigint (FFIType.u64) so a NULL handle is 0n, never a JS null.
|
|
3
|
+
|
|
4
|
+
import { FFIType, dlopen, type Pointer } from "bun:ffi";
|
|
5
|
+
|
|
6
|
+
export const user32 = dlopen("user32.dll", {
|
|
7
|
+
// (const WNDCLASSEXW*) -> ATOM
|
|
8
|
+
RegisterClassExW: { args: [FFIType.ptr], returns: FFIType.u16 },
|
|
9
|
+
// (DWORD exStyle, LPCWSTR class, LPCWSTR title, DWORD style, int x, int y, int w, int h,
|
|
10
|
+
// HWND parent, HMENU menu, HINSTANCE, LPVOID param) -> HWND
|
|
11
|
+
CreateWindowExW: {
|
|
12
|
+
args: [
|
|
13
|
+
FFIType.u32,
|
|
14
|
+
FFIType.ptr,
|
|
15
|
+
FFIType.ptr,
|
|
16
|
+
FFIType.u32,
|
|
17
|
+
FFIType.i32,
|
|
18
|
+
FFIType.i32,
|
|
19
|
+
FFIType.i32,
|
|
20
|
+
FFIType.i32,
|
|
21
|
+
FFIType.u64,
|
|
22
|
+
FFIType.u64,
|
|
23
|
+
FFIType.u64,
|
|
24
|
+
FFIType.ptr,
|
|
25
|
+
],
|
|
26
|
+
returns: FFIType.u64,
|
|
27
|
+
},
|
|
28
|
+
DestroyWindow: { args: [FFIType.u64], returns: FFIType.i32 },
|
|
29
|
+
// (HWND, UINT, WPARAM, LPARAM) -> LRESULT
|
|
30
|
+
DefWindowProcW: { args: [FFIType.u64, FFIType.u32, FFIType.u64, FFIType.i64], returns: FFIType.i64 },
|
|
31
|
+
PostMessageW: { args: [FFIType.u64, FFIType.u32, FFIType.u64, FFIType.i64], returns: FFIType.i32 },
|
|
32
|
+
RegisterWindowMessageW: { args: [FFIType.ptr], returns: FFIType.u32 },
|
|
33
|
+
|
|
34
|
+
ShowWindow: { args: [FFIType.u64, FFIType.i32], returns: FFIType.i32 },
|
|
35
|
+
IsWindowVisible: { args: [FFIType.u64], returns: FFIType.i32 },
|
|
36
|
+
IsIconic: { args: [FFIType.u64], returns: FFIType.i32 },
|
|
37
|
+
IsZoomed: { args: [FFIType.u64], returns: FFIType.i32 },
|
|
38
|
+
ReleaseCapture: { args: [], returns: FFIType.i32 },
|
|
39
|
+
SendMessageW: { args: [FFIType.u64, FFIType.u32, FFIType.u64, FFIType.i64], returns: FFIType.i64 },
|
|
40
|
+
SetForegroundWindow: { args: [FFIType.u64], returns: FFIType.i32 },
|
|
41
|
+
GetCursorPos: { args: [FFIType.ptr], returns: FFIType.i32 },
|
|
42
|
+
GetAsyncKeyState: { args: [FFIType.i32], returns: FFIType.i16 },
|
|
43
|
+
GetSystemMetrics: { args: [FFIType.i32], returns: FFIType.i32 },
|
|
44
|
+
// (const RECT*, DWORD flags) -> HMONITOR; the monitor with the largest overlap, or the nearest one with MONITOR_DEFAULTTONEAREST
|
|
45
|
+
MonitorFromRect: { args: [FFIType.ptr, FFIType.u32], returns: FFIType.u64 },
|
|
46
|
+
// (HMONITOR, MONITORINFO*) -> BOOL; cbSize set by the caller
|
|
47
|
+
GetMonitorInfoW: { args: [FFIType.u64, FFIType.ptr], returns: FFIType.i32 },
|
|
48
|
+
GetDoubleClickTime: { args: [], returns: FFIType.u32 },
|
|
49
|
+
// (HWND, UINT_PTR id, UINT ms, TIMERPROC) -> UINT_PTR
|
|
50
|
+
SetTimer: { args: [FFIType.u64, FFIType.u64, FFIType.u32, FFIType.ptr], returns: FFIType.u64 },
|
|
51
|
+
KillTimer: { args: [FFIType.u64, FFIType.u64], returns: FFIType.i32 },
|
|
52
|
+
|
|
53
|
+
CreatePopupMenu: { args: [], returns: FFIType.u64 },
|
|
54
|
+
// (HMENU, UINT flags, UINT_PTR idOrSubmenu, LPCWSTR text) -> BOOL
|
|
55
|
+
AppendMenuW: { args: [FFIType.u64, FFIType.u32, FFIType.u64, FFIType.ptr], returns: FFIType.i32 },
|
|
56
|
+
DestroyMenu: { args: [FFIType.u64], returns: FFIType.i32 },
|
|
57
|
+
// (HMENU, UINT item, BOOL byPosition, const MENUITEMINFOW*) -> BOOL
|
|
58
|
+
SetMenuItemInfoW: { args: [FFIType.u64, FFIType.u32, FFIType.i32, FFIType.ptr], returns: FFIType.i32 },
|
|
59
|
+
// (HMENU, UINT flags, int x, int y, int reserved, HWND owner, const RECT*) -> int
|
|
60
|
+
TrackPopupMenu: {
|
|
61
|
+
args: [FFIType.u64, FFIType.u32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr],
|
|
62
|
+
returns: FFIType.i32,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// (PBYTE bits, DWORD size, BOOL icon, DWORD version, int cx, int cy, UINT flags) -> HICON
|
|
66
|
+
CreateIconFromResourceEx: {
|
|
67
|
+
args: [FFIType.ptr, FFIType.u32, FFIType.i32, FFIType.u32, FFIType.i32, FFIType.i32, FFIType.u32],
|
|
68
|
+
returns: FFIType.u64,
|
|
69
|
+
},
|
|
70
|
+
LoadIconW: { args: [FFIType.u64, FFIType.u64], returns: FFIType.u64 },
|
|
71
|
+
DestroyIcon: { args: [FFIType.u64], returns: FFIType.i32 },
|
|
72
|
+
// Overlay windows: styles, placement, layered transparency, screen geometry
|
|
73
|
+
GetWindowLongPtrW: { args: [FFIType.u64, FFIType.i32], returns: FFIType.i64 },
|
|
74
|
+
SetWindowLongPtrW: { args: [FFIType.u64, FFIType.i32, FFIType.i64], returns: FFIType.i64 },
|
|
75
|
+
// (HWND, HWND insertAfter, x, y, cx, cy, flags) -> BOOL
|
|
76
|
+
SetWindowPos: { args: [FFIType.u64, FFIType.i64, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u32], returns: FFIType.i32 },
|
|
77
|
+
GetWindowRect: { args: [FFIType.u64, FFIType.ptr], returns: FFIType.i32 },
|
|
78
|
+
// (HWND, COLORREF key, BYTE alpha, DWORD flags) -> BOOL
|
|
79
|
+
SetLayeredWindowAttributes: { args: [FFIType.u64, FFIType.u32, FFIType.u8, FFIType.u32], returns: FFIType.i32 },
|
|
80
|
+
SystemParametersInfoW: { args: [FFIType.u32, FFIType.u32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
|
|
81
|
+
PostQuitMessage: { args: [FFIType.i32], returns: FFIType.void },
|
|
82
|
+
// (DPI_AWARENESS_CONTEXT) -> BOOL; the webview library does this only for windows it creates itself
|
|
83
|
+
SetProcessDpiAwarenessContext: { args: [FFIType.i64], returns: FFIType.i32 },
|
|
84
|
+
GetDpiForWindow: { args: [FFIType.u64], returns: FFIType.u32 },
|
|
85
|
+
// (HWND, UINT cmd) -> HWND
|
|
86
|
+
GetWindow: { args: [FFIType.u64, FFIType.u32], returns: FFIType.u64 },
|
|
87
|
+
GetClientRect: { args: [FFIType.u64, FFIType.ptr], returns: FFIType.i32 },
|
|
88
|
+
MoveWindow: { args: [FFIType.u64, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32], returns: FFIType.i32 },
|
|
89
|
+
// (HWND, HRGN, BOOL redraw) -> int; the system owns the region afterwards
|
|
90
|
+
SetWindowRgn: { args: [FFIType.u64, FFIType.u64, FFIType.i32], returns: FFIType.i32 },
|
|
91
|
+
}).symbols;
|
|
92
|
+
|
|
93
|
+
export const kernel32 = dlopen("kernel32.dll", {
|
|
94
|
+
GetModuleHandleW: { args: [FFIType.ptr], returns: FFIType.u64 },
|
|
95
|
+
GetLastError: { args: [], returns: FFIType.u32 },
|
|
96
|
+
}).symbols;
|
|
97
|
+
|
|
98
|
+
export const comctl32 = dlopen("comctl32.dll", {
|
|
99
|
+
// (HWND, SUBCLASSPROC, UINT_PTR id, DWORD_PTR refData) -> BOOL
|
|
100
|
+
SetWindowSubclass: { args: [FFIType.u64, FFIType.ptr, FFIType.u64, FFIType.u64], returns: FFIType.i32 },
|
|
101
|
+
RemoveWindowSubclass: { args: [FFIType.u64, FFIType.ptr, FFIType.u64], returns: FFIType.i32 },
|
|
102
|
+
// (HWND, UINT, WPARAM, LPARAM) -> LRESULT : the next procedure in the subclass chain
|
|
103
|
+
DefSubclassProc: { args: [FFIType.u64, FFIType.u32, FFIType.u64, FFIType.i64], returns: FFIType.i64 },
|
|
104
|
+
}).symbols;
|
|
105
|
+
|
|
106
|
+
export const ole32 = dlopen("ole32.dll", {
|
|
107
|
+
// (LPVOID reserved, DWORD coinit) -> HRESULT (S_OK, S_FALSE if already initialised, RPC_E_CHANGED_MODE if the other model)
|
|
108
|
+
CoInitializeEx: { args: [FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
|
|
109
|
+
}).symbols;
|
|
110
|
+
|
|
111
|
+
export const dwmapi = dlopen("dwmapi.dll", {
|
|
112
|
+
// (HWND, const DWM_BLURBEHIND*) -> HRESULT
|
|
113
|
+
DwmEnableBlurBehindWindow: { args: [FFIType.u64, FFIType.ptr], returns: FFIType.i32 },
|
|
114
|
+
DwmSetWindowAttribute: { args: [FFIType.u64, FFIType.u32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
|
|
115
|
+
}).symbols;
|
|
116
|
+
|
|
117
|
+
export const gdi32 = dlopen("gdi32.dll", {
|
|
118
|
+
CreateRectRgn: { args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32], returns: FFIType.u64 },
|
|
119
|
+
// (const XFORM*, DWORD bytes, const RGNDATA*) -> HRGN
|
|
120
|
+
ExtCreateRegion: { args: [FFIType.ptr, FFIType.u32, FFIType.ptr], returns: FFIType.u64 },
|
|
121
|
+
DeleteObject: { args: [FFIType.u64], returns: FFIType.i32 },
|
|
122
|
+
// (HDC, const BITMAPINFO*, UINT usage, void** bits, HANDLE section, DWORD offset) -> HBITMAP
|
|
123
|
+
CreateDIBSection: { args: [FFIType.u64, FFIType.ptr, FFIType.u32, FFIType.ptr, FFIType.u64, FFIType.u32], returns: FFIType.u64 },
|
|
124
|
+
}).symbols;
|
|
125
|
+
|
|
126
|
+
export const shell32 = dlopen("shell32.dll", {
|
|
127
|
+
// (DWORD message, PNOTIFYICONDATAW) -> BOOL
|
|
128
|
+
Shell_NotifyIconW: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.i32 },
|
|
129
|
+
}).symbols;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* NUL-terminated UTF-16LE for LPCWSTR arguments. Pass the array itself to an
|
|
133
|
+
* FFIType.ptr parameter (never `ptr(wstr(..))`): Bun keeps a TypedArray
|
|
134
|
+
* argument alive for the duration of the call, while a bare pointer can be
|
|
135
|
+
* collected by a GC triggered while the remaining arguments are evaluated.
|
|
136
|
+
*/
|
|
137
|
+
export const wstr = (s: string): Uint8Array => new Uint8Array(Buffer.from(s + "\0", "utf16le"));
|
|
138
|
+
|
|
139
|
+
/** A non-null pointer from a bigint/number out-parameter; throws on NULL. */
|
|
140
|
+
export const asPointer = (v: bigint | number | null): Pointer => {
|
|
141
|
+
if (v === null || v === 0 || v === 0n) throw new Error("sweetnative: unexpected NULL pointer");
|
|
142
|
+
return Number(v) as Pointer;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** A nullable pointer for a Win32 handle argument (HWND etc.) given as bigint, number or null. */
|
|
146
|
+
export const toPointer = (v: bigint | number | null): Pointer | null => (v === null || v === 0 || v === 0n ? null : (Number(v) as Pointer));
|
|
147
|
+
|
|
148
|
+
export const hInstance = (): bigint => kernel32.GetModuleHandleW(null);
|
|
149
|
+
|
|
150
|
+
export const lastError = () => kernel32.GetLastError();
|
|
151
|
+
|
|
152
|
+
// Window messages
|
|
153
|
+
export const WM_CLOSE = 0x0010;
|
|
154
|
+
export const WM_NCDESTROY = 0x0082;
|
|
155
|
+
export const WM_TIMER = 0x0113;
|
|
156
|
+
export const WM_LBUTTONUP = 0x0202;
|
|
157
|
+
export const WM_LBUTTONDBLCLK = 0x0203;
|
|
158
|
+
export const WM_RBUTTONUP = 0x0205;
|
|
159
|
+
export const WM_NULL = 0x0000;
|
|
160
|
+
export const WM_APP = 0x8000;
|
|
161
|
+
export const WM_SETICON = 0x0080;
|
|
162
|
+
export const ICON_SMALL = 0n;
|
|
163
|
+
export const ICON_BIG = 1n;
|
|
164
|
+
|
|
165
|
+
// ShowWindow
|
|
166
|
+
export const SW_HIDE = 0;
|
|
167
|
+
export const SW_SHOWNOACTIVATE = 4;
|
|
168
|
+
export const SW_SHOW = 5;
|
|
169
|
+
export const SW_RESTORE = 9;
|
|
170
|
+
export const SW_MINIMIZE = 6;
|
|
171
|
+
/** Non-client mouse down: with a hit-test code it starts the OS's own move (HTCAPTION) or size loop. */
|
|
172
|
+
export const WM_NCLBUTTONDOWN = 0x00a1;
|
|
173
|
+
export const HTCAPTION = 2;
|
|
174
|
+
|
|
175
|
+
// More window messages
|
|
176
|
+
export const WM_DESTROY = 0x0002;
|
|
177
|
+
export const WM_SIZE = 0x0005;
|
|
178
|
+
|
|
179
|
+
// GetWindow
|
|
180
|
+
export const GW_CHILD = 5;
|
|
181
|
+
|
|
182
|
+
// GetSystemMetrics
|
|
183
|
+
export const SM_CXICON = 11;
|
|
184
|
+
export const SM_CXSMICON = 49;
|
|
185
|
+
export const SM_CXSCREEN = 0;
|
|
186
|
+
export const SM_CYSCREEN = 1;
|
|
187
|
+
export const SM_XVIRTUALSCREEN = 76;
|
|
188
|
+
export const SM_YVIRTUALSCREEN = 77;
|
|
189
|
+
export const SM_CXVIRTUALSCREEN = 78;
|
|
190
|
+
export const SM_CYVIRTUALSCREEN = 79;
|
|
191
|
+
|
|
192
|
+
// MonitorFromRect
|
|
193
|
+
export const MONITOR_DEFAULTTONEAREST = 2;
|
|
194
|
+
/** MONITORINFO: cbSize, rcMonitor, rcWork, dwFlags; 40 bytes. */
|
|
195
|
+
export const MONITORINFO_SIZE = 40;
|
|
196
|
+
|
|
197
|
+
// Get/SetWindowLongPtr
|
|
198
|
+
export const GWL_STYLE = -16;
|
|
199
|
+
export const GWL_EXSTYLE = -20;
|
|
200
|
+
export const WS_POPUP = 0x80000000;
|
|
201
|
+
export const WS_CLIPCHILDREN = 0x02000000;
|
|
202
|
+
export const WS_OVERLAPPEDWINDOW = 0x00cf0000; // caption, sysmenu, thickframe, min/max boxes
|
|
203
|
+
export const WS_EX_TRANSPARENT = 0x00000020;
|
|
204
|
+
export const WS_EX_TOOLWINDOW = 0x00000080;
|
|
205
|
+
export const WS_EX_APPWINDOW = 0x00040000;
|
|
206
|
+
export const WS_EX_LAYERED = 0x00080000;
|
|
207
|
+
export const WS_EX_NOACTIVATE = 0x08000000;
|
|
208
|
+
|
|
209
|
+
// SetWindowPos
|
|
210
|
+
export const HWND_TOPMOST = -1n;
|
|
211
|
+
export const HWND_NOTOPMOST = -2n;
|
|
212
|
+
export const SWP_NOSIZE = 0x0001;
|
|
213
|
+
export const SWP_NOMOVE = 0x0002;
|
|
214
|
+
export const SWP_NOZORDER = 0x0004;
|
|
215
|
+
export const SWP_NOACTIVATE = 0x0010;
|
|
216
|
+
export const SWP_FRAMECHANGED = 0x0020;
|
|
217
|
+
|
|
218
|
+
// SetLayeredWindowAttributes
|
|
219
|
+
export const LWA_COLORKEY = 0x1;
|
|
220
|
+
export const LWA_ALPHA = 0x2;
|
|
221
|
+
|
|
222
|
+
// SystemParametersInfo
|
|
223
|
+
export const SPI_GETWORKAREA = 0x0030;
|
|
224
|
+
|
|
225
|
+
// CoInitializeEx
|
|
226
|
+
export const COINIT_APARTMENTTHREADED = 0x2;
|
|
227
|
+
export const RPC_E_CHANGED_MODE = -2147417850; // 0x80010106
|
|
228
|
+
|
|
229
|
+
// SetProcessDpiAwarenessContext
|
|
230
|
+
export const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4n;
|
|
231
|
+
|
|
232
|
+
// DWM_BLURBEHIND.dwFlags
|
|
233
|
+
export const DWM_BB_ENABLE = 0x1;
|
|
234
|
+
export const DWM_BB_BLURREGION = 0x2;
|