ispyx 0.0.0-stage → 1.0.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/README.md +183 -2
- package/config.example.json +15 -0
- package/package.json +47 -4
- package/src/core/RESIDENT.md +45 -0
- package/src/core/auth.js +129 -0
- package/src/core/guard.js +287 -0
- package/src/core/image.js +117 -0
- package/src/core/ipc.js +139 -0
- package/src/core/launcher.js +70 -0
- package/src/core/mode.js +17 -0
- package/src/core/motion.js +57 -0
- package/src/core/panel/README.md +70 -0
- package/src/core/panel/index.js +144 -0
- package/src/core/panel/page.html +434 -0
- package/src/core/resident.js +271 -0
- package/src/core/server.js +832 -0
- package/src/effects/appicon.js +46 -0
- package/src/effects/scan.js +850 -0
- package/src/macos-entry.js +16 -0
- package/src/platform/CONTRACT.md +131 -0
- package/src/platform/darwin/PROTOCOL.md +82 -0
- package/src/platform/darwin/hotkey.js +41 -0
- package/src/platform/darwin/index.js +118 -0
- package/src/platform/darwin/keys.js +49 -0
- package/src/platform/darwin/layer.js +87 -0
- package/src/platform/darwin/overlay.js +83 -0
- package/src/platform/darwin/panel.js +25 -0
- package/src/platform/darwin/rpc-worker.js +132 -0
- package/src/platform/darwin/rpc.js +146 -0
- package/src/platform/fake/index.js +162 -0
- package/src/platform/index.js +13 -0
- package/src/platform/win32/auth-dialog.ps1 +140 -0
- package/src/platform/win32/autostart.js +54 -0
- package/src/platform/win32/dialog.js +41 -0
- package/src/platform/win32/hotkey.js +77 -0
- package/src/platform/win32/index.js +31 -0
- package/src/platform/win32/keys.js +31 -0
- package/src/platform/win32/layer.js +120 -0
- package/src/platform/win32/overlay.js +265 -0
- package/src/platform/win32/pump.js +37 -0
- package/src/platform/win32/tray-worker.js +172 -0
- package/src/platform/win32/tray.js +103 -0
- package/src/platform/win32/win32.js +577 -0
- package/src/server.js +14 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Tray icon thread (see tray.js). A window belongs to the thread that created it, so this worker owns the hidden
|
|
2
|
+
// window and blocks in GetMessageW: no polling, 0 % CPU while idle, and the modal context menu blocks only this
|
|
3
|
+
// thread, never the main event loop. Talks to the main thread with parentPort messages (out) and a SharedArrayBuffer
|
|
4
|
+
// plus PostMessageW to the window (in), because a thread blocked in GetMessageW cannot run its own JS events.
|
|
5
|
+
import koffi from 'koffi';
|
|
6
|
+
import { workerData, parentPort } from 'node:worker_threads';
|
|
7
|
+
|
|
8
|
+
const user32 = koffi.load('user32.dll');
|
|
9
|
+
const gdi32 = koffi.load('gdi32.dll');
|
|
10
|
+
const kernel32 = koffi.load('kernel32.dll');
|
|
11
|
+
const shell32 = koffi.load('shell32.dll');
|
|
12
|
+
|
|
13
|
+
const POINT = koffi.struct('TR_POINT', { x: 'int32', y: 'int32' });
|
|
14
|
+
const RECT = koffi.struct('TR_RECT', { left: 'int32', top: 'int32', right: 'int32', bottom: 'int32' });
|
|
15
|
+
const GUID = koffi.struct('TR_GUID', { a: 'uint32', b: 'uint16', c: 'uint16', d: koffi.array('uint8', 8) });
|
|
16
|
+
const NOTIFYICONDATAW = koffi.struct('TR_NID', {
|
|
17
|
+
cbSize: 'uint32', hWnd: 'intptr_t', uID: 'uint32', uFlags: 'uint32', uCallbackMessage: 'uint32', hIcon: 'intptr_t',
|
|
18
|
+
szTip: koffi.array('char16', 128), dwState: 'uint32', dwStateMask: 'uint32', szInfo: koffi.array('char16', 256),
|
|
19
|
+
uVersion: 'uint32', szInfoTitle: koffi.array('char16', 64), dwInfoFlags: 'uint32', guidItem: GUID, hBalloonIcon: 'intptr_t',
|
|
20
|
+
});
|
|
21
|
+
const NOTIFYICONIDENTIFIER = koffi.struct('TR_NII', { cbSize: 'uint32', hWnd: 'intptr_t', uID: 'uint32', guidItem: GUID });
|
|
22
|
+
const BITMAPINFO = koffi.struct('TR_BITMAPINFO', {
|
|
23
|
+
biSize: 'uint32', biWidth: 'int32', biHeight: 'int32', biPlanes: 'uint16', biBitCount: 'uint16', biCompression: 'uint32',
|
|
24
|
+
biSizeImage: 'uint32', biXPels: 'int32', biYPels: 'int32', biClrUsed: 'uint32', biClrImportant: 'uint32', bmiColors: 'uint32',
|
|
25
|
+
});
|
|
26
|
+
const ICONINFO = koffi.struct('TR_ICONINFO', { fIcon: 'int32', xHotspot: 'uint32', yHotspot: 'uint32', hbmMask: 'intptr_t', hbmColor: 'intptr_t' });
|
|
27
|
+
koffi.struct('TR_MSG', { hwnd: 'intptr_t', message: 'uint32', wParam: 'uintptr_t', lParam: 'intptr_t', time: 'uint32', ptx: 'int32', pty: 'int32', lPrivate: 'uint32' });
|
|
28
|
+
const WndProc = koffi.proto('intptr_t TrWndProc(intptr_t h, uint32 msg, uintptr_t wp, intptr_t lp)');
|
|
29
|
+
const WNDCLASSEXW = koffi.struct('TR_WNDCLASSEXW', {
|
|
30
|
+
cbSize: 'uint32', style: 'uint32', lpfnWndProc: koffi.pointer(WndProc), cbClsExtra: 'int32', cbWndExtra: 'int32',
|
|
31
|
+
hInstance: 'intptr_t', hIcon: 'intptr_t', hCursor: 'intptr_t', hbrBackground: 'intptr_t',
|
|
32
|
+
lpszMenuName: 'str16', lpszClassName: 'str16', hIconSm: 'intptr_t',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const f = {
|
|
36
|
+
RegisterClassExW: user32.func('uint16 RegisterClassExW(TR_WNDCLASSEXW *c)'),
|
|
37
|
+
CreateWindowExW: user32.func('intptr_t CreateWindowExW(uint32 ex, str16 cls, str16 title, uint32 style, int x, int y, int w, int h, intptr_t parent, intptr_t menu, intptr_t inst, intptr_t param)'),
|
|
38
|
+
DefWindowProcW: user32.func('intptr_t DefWindowProcW(intptr_t h, uint32 msg, uintptr_t wp, intptr_t lp)'),
|
|
39
|
+
DestroyWindow: user32.func('bool DestroyWindow(intptr_t h)'),
|
|
40
|
+
RegisterWindowMessageW: user32.func('uint32 RegisterWindowMessageW(str16 s)'),
|
|
41
|
+
CreatePopupMenu: user32.func('intptr_t CreatePopupMenu()'),
|
|
42
|
+
AppendMenuW: user32.func('bool AppendMenuW(intptr_t m, uint32 flags, uintptr_t id, str16 text)'),
|
|
43
|
+
DestroyMenu: user32.func('bool DestroyMenu(intptr_t m)'),
|
|
44
|
+
SetMenuDefaultItem: user32.func('bool SetMenuDefaultItem(intptr_t m, uint32 item, uint32 byPos)'),
|
|
45
|
+
TrackPopupMenu: user32.func('int TrackPopupMenu(intptr_t m, uint32 flags, int x, int y, int reserved, intptr_t h, void *rc)'),
|
|
46
|
+
SetForegroundWindow: user32.func('bool SetForegroundWindow(intptr_t h)'),
|
|
47
|
+
PostMessageW: user32.func('bool PostMessageW(intptr_t h, uint32 msg, uintptr_t wp, intptr_t lp)'),
|
|
48
|
+
GetCursorPos: user32.func('bool GetCursorPos(_Out_ TR_POINT *p)'),
|
|
49
|
+
CreateIconIndirect: user32.func('intptr_t CreateIconIndirect(TR_ICONINFO *i)'),
|
|
50
|
+
DestroyIcon: user32.func('bool DestroyIcon(intptr_t i)'),
|
|
51
|
+
GetDC: user32.func('intptr_t GetDC(intptr_t h)'),
|
|
52
|
+
ReleaseDC: user32.func('int ReleaseDC(intptr_t h, intptr_t dc)'),
|
|
53
|
+
CreateDIBSection: gdi32.func('intptr_t CreateDIBSection(intptr_t dc, TR_BITMAPINFO *bi, uint32 usage, _Out_ void **bits, intptr_t section, uint32 offset)'),
|
|
54
|
+
CreateBitmap: gdi32.func('intptr_t CreateBitmap(int w, int h, uint32 planes, uint32 bpp, void *bits)'),
|
|
55
|
+
DeleteObject: gdi32.func('bool DeleteObject(intptr_t o)'),
|
|
56
|
+
GetModuleHandleW: kernel32.func('intptr_t GetModuleHandleW(str16 n)'),
|
|
57
|
+
Shell_NotifyIconW: shell32.func('bool Shell_NotifyIconW(uint32 msg, TR_NID *d)'),
|
|
58
|
+
Shell_NotifyIconGetRect: shell32.func('int32 Shell_NotifyIconGetRect(TR_NII *id, _Out_ TR_RECT *rc)'),
|
|
59
|
+
GetMessageW: user32.func('int GetMessageW(_Out_ TR_MSG *m, intptr_t h, uint32 min, uint32 max)'),
|
|
60
|
+
DispatchMessageW: user32.func('intptr_t DispatchMessageW(TR_MSG *m)'),
|
|
61
|
+
PostQuitMessage: user32.func('void PostQuitMessage(int code)'),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const NIM_ADD = 0, NIM_MODIFY = 1, NIM_DELETE = 2;
|
|
65
|
+
const NIF_MESSAGE = 1, NIF_ICON = 2, NIF_TIP = 4;
|
|
66
|
+
const WM_APP_TRAY = 0x8001, WM_APP_DESTROY = 0x8002, WM_APP_TIP = 0x8003;
|
|
67
|
+
const WM_LBUTTONUP = 0x202, WM_RBUTTONUP = 0x205, WM_CONTEXTMENU = 0x7b, WM_NULL = 0;
|
|
68
|
+
const ID_OPEN = 1, ID_QUIT = 3;
|
|
69
|
+
const MF_GRAYED = 1, MF_SEPARATOR = 0x800;
|
|
70
|
+
const TPM_RIGHTBUTTON = 2, TPM_NONOTIFY = 0x80, TPM_RETURNCMD = 0x100, TPM_BOTTOMALIGN = 0x20;
|
|
71
|
+
|
|
72
|
+
function createEyeIcon() {
|
|
73
|
+
const n = 32;
|
|
74
|
+
const screen = f.GetDC(0);
|
|
75
|
+
const out = [null];
|
|
76
|
+
const color = f.CreateDIBSection(screen, { biSize: 40, biWidth: n, biHeight: -n, biPlanes: 1, biBitCount: 32, biCompression: 0, biSizeImage: 0, biXPels: 0, biYPels: 0, biClrUsed: 0, biClrImportant: 0, bmiColors: 0 }, 0, out, 0, 0);
|
|
77
|
+
f.ReleaseDC(0, screen);
|
|
78
|
+
if (!color || !out[0]) throw new Error('CreateDIBSection failed');
|
|
79
|
+
new Uint8Array(koffi.view(out[0], n * n * 4)).set(workerData.pixels);
|
|
80
|
+
const mask = f.CreateBitmap(n, n, 1, 1, null);
|
|
81
|
+
const icon = f.CreateIconIndirect({ fIcon: 1, xHotspot: 0, yHotspot: 0, hbmMask: mask, hbmColor: color });
|
|
82
|
+
f.DeleteObject(color);
|
|
83
|
+
f.DeleteObject(mask);
|
|
84
|
+
if (!icon) throw new Error('CreateIconIndirect failed');
|
|
85
|
+
return icon;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- window + message loop (this thread only does this) ----
|
|
89
|
+
// Shared buffer layout: Int32 [0] = status length, [1] = tooltip length; UTF-16 text areas at unit 8 (status) and 264 (tooltip).
|
|
90
|
+
const SHARED = new Int32Array(workerData.sab);
|
|
91
|
+
const U16 = new Uint16Array(workerData.sab);
|
|
92
|
+
const STATUS_AT = 8, TIP_AT = 264;
|
|
93
|
+
const readText = (at, lenSlot) => String.fromCharCode(...U16.subarray(at, at + Math.min(Atomics.load(SHARED, lenSlot), 255)));
|
|
94
|
+
|
|
95
|
+
let hwnd = 0, icon = 0, tooltip = 'Ispyx', visible = false;
|
|
96
|
+
const CLASS = 'IspyxTrayWindow';
|
|
97
|
+
const send = (m) => parentPort.postMessage(m);
|
|
98
|
+
|
|
99
|
+
const data = (flags) => ({
|
|
100
|
+
cbSize: koffi.sizeof(NOTIFYICONDATAW), hWnd: hwnd, uID: 1, uFlags: flags, uCallbackMessage: WM_APP_TRAY, hIcon: icon,
|
|
101
|
+
szTip: Array.from((tooltip || '').slice(0, 127), (ch) => ch.charCodeAt(0)), dwState: 0, dwStateMask: 0, szInfo: [],
|
|
102
|
+
uVersion: 0, szInfoTitle: [], dwInfoFlags: 0, guidItem: { a: 0, b: 0, c: 0, d: [0, 0, 0, 0, 0, 0, 0, 0] }, hBalloonIcon: 0,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
function add() {
|
|
106
|
+
visible = !!f.Shell_NotifyIconW(NIM_ADD, data(NIF_MESSAGE | NIF_ICON | NIF_TIP));
|
|
107
|
+
send({ type: 'visible', visible });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function menu() {
|
|
111
|
+
const status = readText(STATUS_AT, 0);
|
|
112
|
+
const m = f.CreatePopupMenu();
|
|
113
|
+
if (!m) return;
|
|
114
|
+
try {
|
|
115
|
+
if (status) f.AppendMenuW(m, MF_GRAYED, 0, status.replace(/\s+/g, ' ').slice(0, 100));
|
|
116
|
+
f.AppendMenuW(m, 0, ID_OPEN, 'Open');
|
|
117
|
+
f.AppendMenuW(m, MF_SEPARATOR, 0, null);
|
|
118
|
+
f.AppendMenuW(m, 0, ID_QUIT, 'Quit');
|
|
119
|
+
f.SetMenuDefaultItem(m, ID_OPEN, 0);
|
|
120
|
+
const pt = {};
|
|
121
|
+
f.GetCursorPos(pt);
|
|
122
|
+
f.SetForegroundWindow(hwnd); // required so the menu closes when clicking elsewhere
|
|
123
|
+
const cmd = f.TrackPopupMenu(m, TPM_RETURNCMD | TPM_NONOTIFY | TPM_RIGHTBUTTON | TPM_BOTTOMALIGN, pt.x, pt.y, 0, hwnd, null);
|
|
124
|
+
f.PostMessageW(hwnd, WM_NULL, 0, 0);
|
|
125
|
+
if (cmd === ID_OPEN) send({ type: 'open' });
|
|
126
|
+
else if (cmd === ID_QUIT) send({ type: 'quit' });
|
|
127
|
+
} finally {
|
|
128
|
+
f.DestroyMenu(m);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const taskbarCreated = f.RegisterWindowMessageW('TaskbarCreated');
|
|
133
|
+
const proc = koffi.register((h, msg, wp, lp) => {
|
|
134
|
+
try {
|
|
135
|
+
if (msg === WM_APP_TRAY) {
|
|
136
|
+
const ev = Number(lp) & 0xffff;
|
|
137
|
+
if (ev === WM_LBUTTONUP) send({ type: 'open' });
|
|
138
|
+
else if (ev === WM_RBUTTONUP || ev === WM_CONTEXTMENU) menu();
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
if (msg === taskbarCreated) { add(); return 0; }
|
|
142
|
+
if (msg === WM_APP_TIP) { tooltip = readText(TIP_AT, 1); f.Shell_NotifyIconW(NIM_MODIFY, data(NIF_TIP)); return 0; }
|
|
143
|
+
if (msg === WM_APP_DESTROY) { f.PostQuitMessage(0); return 0; }
|
|
144
|
+
} catch { /* never throw into native code */ }
|
|
145
|
+
return f.DefWindowProcW(h, msg, wp, lp);
|
|
146
|
+
}, koffi.pointer(WndProc));
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
f.RegisterClassExW({
|
|
150
|
+
cbSize: koffi.sizeof(WNDCLASSEXW), style: 0, lpfnWndProc: proc, cbClsExtra: 0, cbWndExtra: 0, hInstance: f.GetModuleHandleW(null),
|
|
151
|
+
hIcon: 0, hCursor: 0, hbrBackground: 0, lpszMenuName: null, lpszClassName: CLASS, hIconSm: 0,
|
|
152
|
+
});
|
|
153
|
+
tooltip = workerData.tooltip || tooltip;
|
|
154
|
+
// Hidden top-level popup (not message-only: those do not receive the TaskbarCreated broadcast).
|
|
155
|
+
hwnd = f.CreateWindowExW(0x80 /* TOOLWINDOW */, CLASS, 'Ispyx tray', 0x80000000 /* WS_POPUP */, 0, 0, 0, 0, 0, 0, f.GetModuleHandleW(null), 0);
|
|
156
|
+
if (!hwnd) throw new Error('CreateWindowEx failed');
|
|
157
|
+
icon = createEyeIcon();
|
|
158
|
+
send({ type: 'ready', hwnd, icon });
|
|
159
|
+
add();
|
|
160
|
+
// Block until a message arrives: 0 % CPU while idle.
|
|
161
|
+
const msg = {};
|
|
162
|
+
while (f.GetMessageW(msg, 0, 0, 0) > 0) f.DispatchMessageW(msg);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
send({ type: 'error', message: String(e?.message ?? e) });
|
|
165
|
+
} finally {
|
|
166
|
+
try { f.Shell_NotifyIconW(NIM_DELETE, data(0)); } catch { /* already gone */ }
|
|
167
|
+
try { if (hwnd) f.DestroyWindow(hwnd); } catch { /* already gone */ }
|
|
168
|
+
try { if (icon) f.DestroyIcon(icon); } catch { /* already gone */ }
|
|
169
|
+
Atomics.store(SHARED, 2, 1); // lets destroy() on the main thread wait for the icon to be removed
|
|
170
|
+
Atomics.notify(SHARED, 2);
|
|
171
|
+
send({ type: 'closed' });
|
|
172
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Notification-area (tray) icon with a context menu. The icon lives on its own thread (tray-worker.js) that blocks in
|
|
2
|
+
// GetMessageW, so it costs no CPU while idle and the modal context menu never blocks the main event loop.
|
|
3
|
+
import koffi from 'koffi';
|
|
4
|
+
import { Worker } from 'node:worker_threads';
|
|
5
|
+
|
|
6
|
+
const user32 = koffi.load('user32.dll');
|
|
7
|
+
const PostMessageW = user32.func('bool PostMessageW(intptr_t h, uint32 msg, uintptr_t wp, intptr_t lp)');
|
|
8
|
+
const GUID = koffi.struct('TRF_GUID', { a: 'uint32', b: 'uint16', c: 'uint16', d: koffi.array('uint8', 8) });
|
|
9
|
+
const NII = koffi.struct('TRF_NII', { cbSize: 'uint32', hWnd: 'intptr_t', uID: 'uint32', guidItem: GUID });
|
|
10
|
+
const RECT = koffi.struct('TRF_RECT', { left: 'int32', top: 'int32', right: 'int32', bottom: 'int32' });
|
|
11
|
+
const Shell_NotifyIconGetRect = koffi.load('shell32.dll').func('int32 Shell_NotifyIconGetRect(TRF_NII *id, _Out_ TRF_RECT *rc)');
|
|
12
|
+
const WM_APP_DESTROY = 0x8002, WM_APP_TIP = 0x8003;
|
|
13
|
+
const STATUS_AT = 8, TIP_AT = 264;
|
|
14
|
+
const STATUS_REFRESH_MS = 5000;
|
|
15
|
+
|
|
16
|
+
export class Tray {
|
|
17
|
+
constructor({ tooltip = 'Ispyx', onOpen, onQuit, getStatus } = {}) {
|
|
18
|
+
this.onOpen = onOpen; this.onQuit = onQuit; this.getStatus = getStatus;
|
|
19
|
+
this.tooltip = tooltip;
|
|
20
|
+
this.visible = false; // whether the icon is in the notification area
|
|
21
|
+
this.destroyed = false;
|
|
22
|
+
this.hwnd = 0;
|
|
23
|
+
this._sab = new SharedArrayBuffer(1040);
|
|
24
|
+
this._shared = new Int32Array(this._sab);
|
|
25
|
+
this._text = new Uint16Array(this._sab);
|
|
26
|
+
this._setText(TIP_AT, 1, tooltip);
|
|
27
|
+
this.ready = this._start(tooltip);
|
|
28
|
+
this._refreshStatus();
|
|
29
|
+
if (getStatus) {
|
|
30
|
+
this._timer = setInterval(() => this._refreshStatus(), STATUS_REFRESH_MS);
|
|
31
|
+
this._timer.unref();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async _start(tooltip) {
|
|
36
|
+
// The same icon as the macOS app (appicon.js), as straight-alpha BGRA for CreateIconIndirect. Imported here
|
|
37
|
+
// because it pulls in the effects, which import the platform layer that exports this class.
|
|
38
|
+
const { renderIcon } = await import('../../effects/appicon.js');
|
|
39
|
+
const rgba = renderIcon(32, { tight: true });
|
|
40
|
+
const pixels = new Uint8Array(rgba.length);
|
|
41
|
+
for (let i = 0; i < rgba.length; i += 4) { pixels[i] = rgba[i + 2]; pixels[i + 1] = rgba[i + 1]; pixels[i + 2] = rgba[i]; pixels[i + 3] = rgba[i + 3]; }
|
|
42
|
+
if (this.destroyed) return false;
|
|
43
|
+
this._worker = new Worker(new URL('./tray-worker.js', import.meta.url), { workerData: { sab: this._sab, tooltip, pixels } });
|
|
44
|
+
this._worker.unref();
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
this._worker.on('message', (m) => {
|
|
47
|
+
if (m.type === 'ready') { this.hwnd = m.hwnd; resolve(true); }
|
|
48
|
+
else if (m.type === 'visible') this.visible = m.visible;
|
|
49
|
+
else if (m.type === 'open') this._fire('onOpen');
|
|
50
|
+
else if (m.type === 'quit') this._fire('onQuit');
|
|
51
|
+
else if (m.type === 'error') { resolve(false); }
|
|
52
|
+
});
|
|
53
|
+
this._worker.on('error', () => resolve(false));
|
|
54
|
+
this._worker.on('exit', () => { this.visible = false; resolve(false); });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_setText(at, lenSlot, s) {
|
|
59
|
+
const t = String(s ?? '').slice(0, 255);
|
|
60
|
+
for (let i = 0; i < t.length; i++) this._text[at + i] = t.charCodeAt(i);
|
|
61
|
+
Atomics.store(this._shared, lenSlot, t.length);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_refreshStatus() {
|
|
65
|
+
try {
|
|
66
|
+
const s = this.getStatus ? this.getStatus() : '';
|
|
67
|
+
this._setText(STATUS_AT, 0, typeof s === 'object' && s ? s.text : s);
|
|
68
|
+
} catch { /* no status line text */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setTooltip(s) {
|
|
72
|
+
this.tooltip = String(s ?? '');
|
|
73
|
+
if (this.destroyed) return false;
|
|
74
|
+
this._setText(TIP_AT, 1, this.tooltip);
|
|
75
|
+
return this.hwnd ? !!PostMessageW(this.hwnd, WM_APP_TIP, 0, 0) : true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Screen rect of the icon in the notification area, or null (e.g. hidden in the overflow). */
|
|
79
|
+
rect() {
|
|
80
|
+
if (!this.hwnd || this.destroyed) return null;
|
|
81
|
+
const rc = {};
|
|
82
|
+
const hr = Shell_NotifyIconGetRect({ cbSize: koffi.sizeof(NII), hWnd: this.hwnd, uID: 1, guidItem: { a: 0, b: 0, c: 0, d: [0, 0, 0, 0, 0, 0, 0, 0] } }, rc);
|
|
83
|
+
return hr === 0 ? rc : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_fire(name) {
|
|
87
|
+
const fn = this[name];
|
|
88
|
+
if (!fn) return;
|
|
89
|
+
setImmediate(() => { try { const r = fn(); if (r && r.catch) r.catch(() => {}); } catch { /* callbacks must not crash the process */ } });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Removes the icon and ends the thread; waits briefly so the icon is gone before the process exits. */
|
|
93
|
+
destroy() {
|
|
94
|
+
if (this.destroyed) return;
|
|
95
|
+
this.destroyed = true;
|
|
96
|
+
clearInterval(this._timer);
|
|
97
|
+
if (this.hwnd) {
|
|
98
|
+
PostMessageW(this.hwnd, WM_APP_DESTROY, 0, 0);
|
|
99
|
+
Atomics.wait(this._shared, 2, 0, 500);
|
|
100
|
+
}
|
|
101
|
+
this.visible = false;
|
|
102
|
+
}
|
|
103
|
+
}
|