@bobfrankston/msger 0.1.341 → 0.1.342
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/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/shower.d.ts +7 -2
- package/shower.js +156 -14
package/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { showMessageBox, showService, ServiceHandle, MessageBoxOptions, MessageBoxResult, setAppName } from "./shower.js";
|
|
1
|
+
export { showMessageBox, showService, ServiceHandle, MessageBoxOptions, MessageBoxResult, setAppName, setAppIcon } from "./shower.js";
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Export showMessageBox and types for use as a library
|
|
2
|
-
export { showMessageBox, showService, ServiceHandle, setAppName } from "./shower.js";
|
|
2
|
+
export { showMessageBox, showService, ServiceHandle, setAppName, setAppIcon } from "./shower.js";
|
|
3
3
|
// If run directly (e.g., `node .` or `node index.js`), run the CLI
|
|
4
4
|
// @ts-ignore - import.meta.main is available in Node.js 20+
|
|
5
5
|
if (import.meta.main) {
|
package/package.json
CHANGED
package/shower.d.ts
CHANGED
|
@@ -61,9 +61,14 @@ export interface MessageBoxResult {
|
|
|
61
61
|
autoSize: boolean;
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
/** Set the app name used for the per-user
|
|
65
|
-
* Call before showService/showMessageBox — e.g. `setAppName("mailx")
|
|
64
|
+
/** Set the app name used for the per-user bin dir AND the per-app exe filename.
|
|
65
|
+
* Call before showService/showMessageBox — e.g. `setAppName("mailx")` makes
|
|
66
|
+
* the running process appear as `mailx.exe` in Task Manager and the taskbar. */
|
|
66
67
|
export declare function setAppName(name: string): void;
|
|
68
|
+
/** Set a per-app icon (PNG or ICO). Copied next to the per-app exe at first
|
|
69
|
+
* provision so Rust's window icon search picks it up. Per-call `icon` in
|
|
70
|
+
* `MessageBoxOptions` still takes precedence. */
|
|
71
|
+
export declare function setAppIcon(iconPath: string): void;
|
|
67
72
|
/**
|
|
68
73
|
* Show a message box dialog using native Rust implementation (extended version)
|
|
69
74
|
* @param options Message box configuration
|
package/shower.js
CHANGED
|
@@ -53,27 +53,149 @@ export function closeMessageBox(pid) {
|
|
|
53
53
|
* exe inside it is locked. Keeping timestamped copies here means npm only
|
|
54
54
|
* manages the stable `msgernative.exe` (which isn't the one being executed),
|
|
55
55
|
* so it's never locked. */
|
|
56
|
-
/** App name for per-user bin dir. Set via `setAppName()`
|
|
57
|
-
* msger gets its own binary path
|
|
58
|
-
*
|
|
56
|
+
/** App name for per-user bin dir + per-app exe filename. Set via `setAppName()`
|
|
57
|
+
* so each app using msger gets its own binary path
|
|
58
|
+
* (`%LOCALAPPDATA%\<appName>\bin\<appName>.exe` on Windows) and thus its own
|
|
59
|
+
* taskbar identity, Task Manager process name, and pin target. Defaults to
|
|
60
|
+
* "msger" for standalone use, which preserves the original `msgernative.exe`
|
|
61
|
+
* behavior. */
|
|
59
62
|
let _appName = "msger";
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
63
|
+
/** Optional per-app icon path. Set via `setAppIcon()`. When set, msger copies
|
|
64
|
+
* it next to the per-app exe so Rust's default-icon search picks it up — no
|
|
65
|
+
* need to pass `icon` on every spawn call. PNG and ICO accepted. */
|
|
66
|
+
let _appIcon;
|
|
67
|
+
/** Set the app name used for the per-user bin dir AND the per-app exe filename.
|
|
68
|
+
* Call before showService/showMessageBox — e.g. `setAppName("mailx")` makes
|
|
69
|
+
* the running process appear as `mailx.exe` in Task Manager and the taskbar. */
|
|
62
70
|
export function setAppName(name) { _appName = name; }
|
|
63
|
-
|
|
71
|
+
/** Set a per-app icon (PNG or ICO). Copied next to the per-app exe at first
|
|
72
|
+
* provision so Rust's window icon search picks it up. Per-call `icon` in
|
|
73
|
+
* `MessageBoxOptions` still takes precedence. */
|
|
74
|
+
export function setAppIcon(iconPath) { _appIcon = iconPath; }
|
|
75
|
+
function getUserBinDirFor(appName) {
|
|
64
76
|
const isWindows = platform() === 'win32';
|
|
65
77
|
if (isWindows) {
|
|
66
78
|
const base = process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || '.', 'AppData', 'Local');
|
|
67
|
-
return path.join(base,
|
|
79
|
+
return path.join(base, appName, 'bin');
|
|
68
80
|
}
|
|
69
81
|
const xdg = process.env.XDG_DATA_HOME;
|
|
70
82
|
const base = xdg || path.join(process.env.HOME || '.', '.local', 'share');
|
|
71
|
-
return path.join(base,
|
|
83
|
+
return path.join(base, appName, 'bin');
|
|
72
84
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
*
|
|
85
|
+
function getUserBinDir() { return getUserBinDirFor(_appName); }
|
|
86
|
+
/** Locate the msger-managed source binary that per-app exes hardlink to.
|
|
87
|
+
* Prefers `%LOCALAPPDATA%\msger\bin\msgernative.exe` (postinstall's stable
|
|
88
|
+
* name), falls back to a per-user timestamped copy, then to the package's
|
|
89
|
+
* bundled binary. Returns null only if msger truly isn't installed. */
|
|
90
|
+
function findMsgerSource() {
|
|
91
|
+
const isWindows = platform() === 'win32';
|
|
92
|
+
const arch = process.arch;
|
|
93
|
+
const ext = isWindows ? '.exe' : '';
|
|
94
|
+
const baseName = (!isWindows && arch === 'arm64')
|
|
95
|
+
? 'msgernative-linux-aarch64'
|
|
96
|
+
: 'msgernative';
|
|
97
|
+
const msgerBin = getUserBinDirFor('msger');
|
|
98
|
+
const pkgBin = path.join(import.meta.dirname, 'msger-native', 'bin');
|
|
99
|
+
const stable = path.join(msgerBin, `${baseName}${ext}`);
|
|
100
|
+
if (fs.existsSync(stable))
|
|
101
|
+
return stable;
|
|
102
|
+
const pattern = new RegExp(`^${baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-(\\d+)${ext.replace('.', '\\.')}$`);
|
|
103
|
+
for (const dir of [msgerBin, pkgBin]) {
|
|
104
|
+
try {
|
|
105
|
+
const matches = fs.readdirSync(dir).filter(f => pattern.test(f)).sort().reverse();
|
|
106
|
+
if (matches.length > 0)
|
|
107
|
+
return path.join(dir, matches[0]);
|
|
108
|
+
}
|
|
109
|
+
catch { /* dir doesn't exist — keep looking */ }
|
|
110
|
+
}
|
|
111
|
+
const bundled = path.join(pkgBin, `${baseName}${ext}`);
|
|
112
|
+
if (fs.existsSync(bundled))
|
|
113
|
+
return bundled;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/** Ensure a per-app exe is present at `perAppExe` and points at the same inode
|
|
117
|
+
* as `sourceExe`. On Windows, hardlinks share inodes so a `stat().ino`
|
|
118
|
+
* comparison detects when msger's postinstall has rename-asided the source
|
|
119
|
+
* and written a new file (which leaves our hardlink dangling on the OLD
|
|
120
|
+
* inode). Re-link in that case, using the same rename-aside trick to avoid
|
|
121
|
+
* EBUSY when the per-app exe is itself being executed by another instance. */
|
|
122
|
+
function ensureFresh(perAppExe, sourceExe) {
|
|
123
|
+
if (!fs.existsSync(perAppExe)) {
|
|
124
|
+
fs.mkdirSync(path.dirname(perAppExe), { recursive: true });
|
|
125
|
+
try {
|
|
126
|
+
fs.linkSync(sourceExe, perAppExe);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
fs.copyFileSync(sourceExe, perAppExe); /* cross-volume fallback */
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (fs.statSync(perAppExe).ino === fs.statSync(sourceExe).ino)
|
|
134
|
+
return;
|
|
135
|
+
// Stale hardlink — source was rename-asided + replaced. Rename-aside the
|
|
136
|
+
// per-app exe (a running mailx keeps its file handle to the OLD inode via
|
|
137
|
+
// the aside name) and re-link to the new source.
|
|
138
|
+
const aside = `${perAppExe}.old-${Date.now()}`;
|
|
139
|
+
try {
|
|
140
|
+
fs.renameSync(perAppExe, aside);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return; /* held open by another instance — retry next launch */
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
fs.linkSync(sourceExe, perAppExe);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
fs.copyFileSync(sourceExe, perAppExe);
|
|
150
|
+
}
|
|
151
|
+
// Best-effort sweep of `.old-*` siblings older than 24 h. Same pattern as
|
|
152
|
+
// postinstall.js. Skip silently — they'll get cleaned up next time.
|
|
153
|
+
try {
|
|
154
|
+
const dir = path.dirname(perAppExe);
|
|
155
|
+
const base = path.basename(perAppExe);
|
|
156
|
+
const oldPat = new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.old-(\\d+)$`);
|
|
157
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
158
|
+
for (const f of fs.readdirSync(dir)) {
|
|
159
|
+
const m = oldPat.exec(f);
|
|
160
|
+
if (!m)
|
|
161
|
+
continue;
|
|
162
|
+
if (Number(m[1]) > cutoff)
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
fs.unlinkSync(path.join(dir, f));
|
|
166
|
+
}
|
|
167
|
+
catch { /* still locked, try next time */ }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch { /* fall through */ }
|
|
171
|
+
}
|
|
172
|
+
/** Copy the per-app icon next to the per-app exe so Rust's default-icon
|
|
173
|
+
* search (`load_icon` in main.rs) picks it up. Rust looks for `msger.ico`
|
|
174
|
+
* / `msger.png` adjacent to the exe — we drop our icon under that name so
|
|
175
|
+
* no Rust changes are needed. Only re-copies when source is newer. */
|
|
176
|
+
function provisionAppIcon(perAppDir) {
|
|
177
|
+
if (!_appIcon || !fs.existsSync(_appIcon))
|
|
178
|
+
return;
|
|
179
|
+
const iconExt = path.extname(_appIcon).toLowerCase();
|
|
180
|
+
if (iconExt !== '.png' && iconExt !== '.ico')
|
|
181
|
+
return;
|
|
182
|
+
const dest = path.join(perAppDir, `msger${iconExt}`);
|
|
183
|
+
try {
|
|
184
|
+
if (fs.existsSync(dest) && fs.statSync(dest).mtimeMs >= fs.statSync(_appIcon).mtimeMs)
|
|
185
|
+
return;
|
|
186
|
+
fs.copyFileSync(_appIcon, dest);
|
|
187
|
+
}
|
|
188
|
+
catch { /* non-fatal — caller can still pass icon per-call */ }
|
|
189
|
+
}
|
|
190
|
+
/** Resolve the native binary path. Two modes:
|
|
191
|
+
* - `_appName === "msger"` (default, standalone msger use): unchanged from
|
|
192
|
+
* the original — find `msgernative.exe` in user bin, fall back to legacy
|
|
193
|
+
* timestamped copies, finally to the package-bundled binary.
|
|
194
|
+
* - `_appName !== "msger"` (per-app, e.g. mailx): provision a hardlink at
|
|
195
|
+
* `%LOCALAPPDATA%\<_appName>\bin\<_appName>.exe` pointing at the msger
|
|
196
|
+
* source binary, refresh the link if msger has been updated, copy the
|
|
197
|
+
* icon next to it, return the per-app path. This is what gives mailx its
|
|
198
|
+
* own taskbar identity. */
|
|
77
199
|
function resolveBinaryPath() {
|
|
78
200
|
const isWindows = platform() === 'win32';
|
|
79
201
|
const arch = process.arch;
|
|
@@ -93,8 +215,28 @@ function resolveBinaryPath() {
|
|
|
93
215
|
baseName = 'msgernative';
|
|
94
216
|
ext = '';
|
|
95
217
|
}
|
|
96
|
-
//
|
|
97
|
-
//
|
|
218
|
+
// Per-app mode: provision and use a renamed hardlink so taskbar / Task
|
|
219
|
+
// Manager / pinned shortcuts see the app's own identity.
|
|
220
|
+
if (_appName !== 'msger') {
|
|
221
|
+
const source = findMsgerSource();
|
|
222
|
+
if (source) {
|
|
223
|
+
const perAppExe = path.join(userBinDir, `${_appName}${ext}`);
|
|
224
|
+
try {
|
|
225
|
+
ensureFresh(perAppExe, source);
|
|
226
|
+
provisionAppIcon(userBinDir);
|
|
227
|
+
if (fs.existsSync(perAppExe))
|
|
228
|
+
return perAppExe;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Provisioning failed — fall through to the source binary so
|
|
232
|
+
// the app at least launches, just with msger's identity.
|
|
233
|
+
}
|
|
234
|
+
return source;
|
|
235
|
+
}
|
|
236
|
+
// No source found — fall through to the legacy lookup below, which
|
|
237
|
+
// will return the package-bundled binary as a last resort.
|
|
238
|
+
}
|
|
239
|
+
// Default msger-itself path (or per-app fallback if findMsgerSource failed).
|
|
98
240
|
const stableLink = path.join(userBinDir, `${baseName}${ext}`);
|
|
99
241
|
if (fs.existsSync(stableLink))
|
|
100
242
|
return stableLink;
|