@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/msger",
3
- "version": "0.1.341",
3
+ "version": "0.1.342",
4
4
  "description": "Fast, lightweight, cross-platform message box - Rust-powered alternative to msgview",
5
5
  "type": "module",
6
6
  "main": "./index.js",
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 binary directory.
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()` so each app using
57
- * msger gets its own binary path (%LOCALAPPDATA%\<appName>\bin\) and thus
58
- * its own taskbar identity. Defaults to "msger" for standalone use. */
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
- /** Set the app name used for the per-user binary directory.
61
- * Call before showService/showMessageBox e.g. `setAppName("mailx")`. */
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
- function getUserBinDir() {
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, _appName, 'bin');
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, _appName, 'bin');
83
+ return path.join(base, appName, 'bin');
72
84
  }
73
- /** Resolve the native binary path. Prefers a timestamped copy in the per-user
74
- * bin dir (immune to npm-upgrade locks). Falls back to the package's bundled
75
- * unversioned binary when no per-user copy exists yet — first launch after a
76
- * fresh install before postinstall has copied one out. */
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
- // Prefer the stable-name hardlink in the user dir same path every time,
97
- // so taskbar pins and AUMID grouping don't break on updates.
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;