@bobfrankston/msger 0.1.170 → 0.1.173

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/cli-old.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export default function main(): Promise<void>;
package/cli-old.js ADDED
@@ -0,0 +1,645 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // TODO: Migrate to use @bobfrankston/msgcommon for argument parsing
4
+ // Example:
5
+ // import { parseCommonArgs } from '@bobfrankston/msgcommon';
6
+ // const { options, messageWords, showHelp, showVersion } = parseCommonArgs(args);
7
+ //
8
+ import { showMessageBox } from './shower.js';
9
+ import packageJson from './package.json' with { type: 'json' };
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { execSync } from 'child_process';
13
+ import JSON5 from 'json5';
14
+ // Uncomment when ready to migrate:
15
+ // import { parseCommonArgs } from '@bobfrankston/msgcommon';
16
+ export default async function main() {
17
+ const args = process.argv.slice(2);
18
+ // No arguments - show help
19
+ if (args.length === 0) {
20
+ showHelp();
21
+ process.exit(0);
22
+ }
23
+ // Parse command line arguments
24
+ let options, shouldShowHelp, shouldShowVersion, loadFile, saveFile, noshow, shouldPin;
25
+ try {
26
+ ({ options, showHelp: shouldShowHelp, showVersion: shouldShowVersion, loadFile, saveFile, noshow, pin: shouldPin } = parseCliArgs(args));
27
+ }
28
+ catch (error) {
29
+ console.error('Error:', error.message);
30
+ console.error('\nUse -help to see usage information.');
31
+ process.exit(1);
32
+ }
33
+ // Helper to add .json extension if no extension specified
34
+ const ensureJsonExt = (filename) => {
35
+ const ext = path.extname(filename);
36
+ return ext ? filename : `${filename}.json`;
37
+ };
38
+ // Handle -pin: create pinnable shortcut (Windows only)
39
+ if (shouldPin) {
40
+ console.error('Error: -pin is not yet implemented');
41
+ console.error('Taskbar pinning requires packaged .exe files, not Node.js scripts');
42
+ console.error('This feature will be available once msger is packaged as a standalone executable');
43
+ process.exit(1);
44
+ }
45
+ // Load config file if specified
46
+ let finalOptions = options;
47
+ if (loadFile) {
48
+ try {
49
+ const loadedConfig = loadConfigFile(ensureJsonExt(loadFile));
50
+ finalOptions = { ...loadedConfig };
51
+ if (options.title)
52
+ finalOptions.title = options.title;
53
+ if (options.message)
54
+ finalOptions.message = options.message;
55
+ if (options.html)
56
+ finalOptions.html = options.html;
57
+ if (options.url)
58
+ finalOptions.url = options.url;
59
+ if (options.hash)
60
+ finalOptions.hash = options.hash;
61
+ if (options.size)
62
+ finalOptions.size = options.size;
63
+ if (options.pos)
64
+ finalOptions.pos = options.pos;
65
+ if (options.buttons && options.buttons.length > 0)
66
+ finalOptions.buttons = options.buttons;
67
+ if (options.allowInput !== undefined)
68
+ finalOptions.allowInput = options.allowInput;
69
+ if (options.defaultValue)
70
+ finalOptions.defaultValue = options.defaultValue;
71
+ if (options.inputPlaceholder)
72
+ finalOptions.inputPlaceholder = options.inputPlaceholder;
73
+ if (options.timeout !== undefined)
74
+ finalOptions.timeout = options.timeout;
75
+ if (options.alwaysOnTop !== undefined)
76
+ finalOptions.alwaysOnTop = options.alwaysOnTop;
77
+ if (options.fullscreen !== undefined)
78
+ finalOptions.fullscreen = options.fullscreen;
79
+ if (options.zoom !== undefined)
80
+ finalOptions.zoom = options.zoom;
81
+ if (options.icon)
82
+ finalOptions.icon = options.icon;
83
+ if (options.dev !== undefined)
84
+ finalOptions.dev = options.dev;
85
+ if (options.debug !== undefined)
86
+ finalOptions.debug = options.debug;
87
+ if (options.detach !== undefined)
88
+ finalOptions.detach = options.detach;
89
+ // Generate AppUserModelID for pinning support
90
+ if (process.platform === 'win32') {
91
+ try {
92
+ const { getAppUserModelIdForConfig } = await import('@bobfrankston/msgcommon/pinning');
93
+ const appId = getAppUserModelIdForConfig('msger', path.resolve(ensureJsonExt(loadFile)));
94
+ if (appId) {
95
+ finalOptions.appUserModelId = appId;
96
+ }
97
+ }
98
+ catch (error) {
99
+ // Silently ignore if msgcommon is not available
100
+ }
101
+ }
102
+ }
103
+ catch (error) {
104
+ console.error('Error loading config:', error.message);
105
+ process.exit(1);
106
+ }
107
+ }
108
+ if (!finalOptions.buttons || finalOptions.buttons.length === 0) {
109
+ finalOptions.buttons = ['OK'];
110
+ }
111
+ // Help always takes precedence and exits
112
+ if (shouldShowHelp) {
113
+ showHelp();
114
+ process.exit(0);
115
+ }
116
+ // Version only exits if there's nothing else meaningful to do
117
+ // (no actual message/url/html content was provided - the default message doesn't count)
118
+ const hasRealContent = args.some(arg => !arg.startsWith('-') ||
119
+ arg === '-message' ||
120
+ arg === '-html' ||
121
+ arg === '-url');
122
+ if (shouldShowVersion && !hasRealContent) {
123
+ showVersion();
124
+ process.exit(0);
125
+ }
126
+ // If version was requested along with other options, add it to title
127
+ if (shouldShowVersion && hasRealContent) {
128
+ finalOptions.showVersion = true;
129
+ }
130
+ if (saveFile) {
131
+ try {
132
+ let saveFilePath;
133
+ if (saveFile === true) {
134
+ if (!loadFile) {
135
+ console.error('Error: -save without filename requires -load to be specified');
136
+ process.exit(1);
137
+ }
138
+ saveFilePath = ensureJsonExt(loadFile);
139
+ }
140
+ else {
141
+ saveFilePath = ensureJsonExt(saveFile);
142
+ }
143
+ const toSave = saveFile === true ? finalOptions : options;
144
+ saveConfigFile(saveFilePath, toSave);
145
+ console.log(`Configuration saved to: ${saveFilePath}`);
146
+ if (noshow) {
147
+ process.exit(0);
148
+ }
149
+ if (!finalOptions.message && !finalOptions.url && !finalOptions.html) {
150
+ process.exit(0);
151
+ }
152
+ }
153
+ catch (error) {
154
+ console.error('Error saving config:', error.message);
155
+ process.exit(1);
156
+ }
157
+ }
158
+ // If no actual message/content was provided, show help
159
+ if (!finalOptions.message && !finalOptions.url && !finalOptions.html) {
160
+ showHelp();
161
+ process.exit(0);
162
+ }
163
+ try {
164
+ const result = await showMessageBox(finalOptions);
165
+ // If detached, exit immediately without printing result
166
+ if (finalOptions.detach) {
167
+ process.exit(0);
168
+ }
169
+ console.log(JSON.stringify(result, null, 2));
170
+ // Exit with code based on result
171
+ if (result.dismissed || result.closed) {
172
+ process.exit(1);
173
+ }
174
+ }
175
+ catch (error) {
176
+ console.error('Error:', error.message);
177
+ process.exit(1);
178
+ }
179
+ }
180
+ const HELP_TEXT = `
181
+ msger v${packageJson.version} - Fast, lightweight, cross-platform message box (Rust-powered)
182
+
183
+ Usage:
184
+ msger [options] [message words...]
185
+ msger -message "Your message"
186
+ echo '{"message":"text"}' | msger
187
+
188
+ Options:
189
+ -message <text> Specify message text (use quotes for multi-word)
190
+ Supports ANSI color escape sequences (e.g., \\x1b[31mRed\\x1b[0m)
191
+ -title <text> Set window title
192
+ -html <html> Display HTML formatted message (inline)
193
+ -htmlfrom <file|url> Fetch HTML from file/URL and embed in template (has buttons & msger API)
194
+ -url <url> Load URL directly in webview (no template, no buttons)
195
+ -hash <fragment> Hash fragment to append to URL (requires -url). Leading # optional.
196
+ -buttons <label...> Specify button labels (e.g., -buttons Yes No Cancel)
197
+ -ok Add OK button
198
+ -cancel Add Cancel button
199
+ -input [placeholder] Include an input field with optional placeholder text
200
+ -default <text> Default value for input field
201
+ -size <width,height> Set window size (e.g., -size 800,600)
202
+ -zoom <percent> Set zoom level as percentage (e.g., -zoom 150 for 150%)
203
+ -pos <x,y> Set window position (e.g., -pos 100,200)
204
+ -screen <number> [Windows only] Screen index for multi-monitor (0=primary, 1=second, etc.)
205
+ -icon <path> Set window icon (PNG file). Default: icon.png in current directory
206
+ -timeout <seconds> Auto-close after specified seconds
207
+ -detach Leave window open after app exits (parent process returns immediately)
208
+ -fullscreen Start window in fullscreen mode (F11 to toggle, Escape to exit)
209
+ -no-escape-closes Prevent Escape key from closing window (still exits fullscreen)
210
+ -reset Clear localStorage on startup (useful for resetting web content state)
211
+ -ontop Keep window always on top of other windows
212
+ -load <file> Load options from JSON file (supports comments)
213
+ -save <file> Save current options to JSON file
214
+ -noshow Save config without showing message box (use with -save)
215
+ -pin [Not yet implemented] Create pinnable taskbar shortcut
216
+ -dev Open DevTools (F12) automatically on startup (for debugging)
217
+ -debug Return debug info (HTML, size) in result
218
+ -v, -version, --version Show version number (alone: print and exit; with options: show in title)
219
+ -help, -?, --help Show this help message
220
+
221
+ Examples:
222
+ msger Hello World
223
+ msger -message "Save changes?" -buttons Yes No Cancel
224
+ msger -message "Enter your name:" -input "Your name" -default "John Doe"
225
+ msger -html "<h1>Title</h1><p>Formatted content</p>"
226
+ msger -htmlfrom page.html -buttons OK Cancel
227
+ msger -htmlfrom "https://example.com" -buttons OK
228
+ msger -url "https://example.com"
229
+ msger -url "https://example.com" -hash section1
230
+ msger -url "https://example.com" -detach
231
+ msger -title "Alert" -message "Operation complete"
232
+ echo -e "\\x1b[31mError:\\x1b[0m Something failed" | msger
233
+ echo '{"message":"Test","buttons":["OK"]}' | msger
234
+
235
+ Notes:
236
+ - If no options are provided, all non-option arguments are concatenated as the message
237
+ - Message text supports ANSI color codes (automatically converted to HTML)
238
+ - Press ESC to dismiss the dialog (returns button: "dismissed")
239
+ - Press Enter to click the last (default) button
240
+ - Default button is OK if no buttons specified
241
+ - Much faster than Electron-based msgview (~50-200ms vs ~2-3s startup)
242
+
243
+ Security Note:
244
+ - msger is designed for displaying trusted, friendly content (local apps, your own HTML)
245
+ - It is NOT a secure sandbox for untrusted/hostile web content
246
+ - Use -htmlfrom and -url with trusted sources only
247
+ `;
248
+ function parseCliArgs(args) {
249
+ const cli = {
250
+ buttons: [],
251
+ input: false,
252
+ help: false
253
+ };
254
+ let i = 0;
255
+ const messageWords = [];
256
+ while (i < args.length) {
257
+ const arg = args[i];
258
+ if (arg === '-help' || arg === '--help' || arg === '-?' || arg === '/?') {
259
+ cli.help = true;
260
+ i++;
261
+ }
262
+ else if (arg === '-v' || arg === '-version' || arg === '--version') {
263
+ cli.version = true;
264
+ i++;
265
+ }
266
+ else if (arg === '-message' || arg === '--message') {
267
+ if (i + 1 >= args.length) {
268
+ throw new Error('-message requires a text argument');
269
+ }
270
+ cli.message = args[++i];
271
+ i++;
272
+ }
273
+ else if (arg === '-title' || arg === '--title') {
274
+ if (i + 1 >= args.length) {
275
+ throw new Error('-title requires a text argument');
276
+ }
277
+ cli.title = args[++i];
278
+ i++;
279
+ }
280
+ else if (arg === '-html' || arg === '--html') {
281
+ if (i + 1 >= args.length) {
282
+ throw new Error('-html requires an HTML string argument');
283
+ }
284
+ cli.html = args[++i];
285
+ i++;
286
+ }
287
+ else if (arg === '-htmlfrom' || arg === '--htmlfrom') {
288
+ if (i + 1 >= args.length) {
289
+ throw new Error('-htmlfrom requires a file path or URL argument');
290
+ }
291
+ cli.htmlFrom = args[++i];
292
+ i++;
293
+ }
294
+ else if (arg === '-url' || arg === '--url') {
295
+ if (i + 1 >= args.length) {
296
+ throw new Error('-url requires a URL argument');
297
+ }
298
+ cli.url = args[++i];
299
+ i++;
300
+ }
301
+ else if (arg === '-hash' || arg === '--hash') {
302
+ if (i + 1 >= args.length) {
303
+ throw new Error('-hash requires a fragment argument');
304
+ }
305
+ cli.hash = args[++i];
306
+ i++;
307
+ }
308
+ else if (arg === '-buttons' || arg === '--buttons') {
309
+ i++;
310
+ while (i < args.length && !args[i].startsWith('-')) {
311
+ cli.buttons.push(args[i]);
312
+ i++;
313
+ }
314
+ }
315
+ else if (arg === '-ok' || arg === '--ok') {
316
+ if (!cli.buttons.includes('OK')) {
317
+ cli.buttons.push('OK');
318
+ }
319
+ i++;
320
+ }
321
+ else if (arg === '-cancel' || arg === '--cancel') {
322
+ if (!cli.buttons.includes('Cancel')) {
323
+ cli.buttons.push('Cancel');
324
+ }
325
+ i++;
326
+ }
327
+ else if (arg === '-input' || arg === '--input') {
328
+ cli.input = true;
329
+ if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
330
+ cli.inputPlaceholder = args[++i];
331
+ }
332
+ i++;
333
+ }
334
+ else if (arg === '-default' || arg === '--default') {
335
+ if (i + 1 >= args.length) {
336
+ throw new Error('-default requires a text argument');
337
+ }
338
+ cli.defaultValue = args[++i];
339
+ i++;
340
+ }
341
+ else if (arg === '-timeout' || arg === '--timeout') {
342
+ if (i + 1 >= args.length) {
343
+ throw new Error('-timeout requires a number argument');
344
+ }
345
+ cli.timeout = parseInt(args[++i], 10);
346
+ i++;
347
+ }
348
+ else if (arg === '-detach' || arg === '--detach') {
349
+ cli.detach = true;
350
+ i++;
351
+ }
352
+ else if (arg === '-fullscreen' || arg === '--fullscreen') {
353
+ cli.fullscreen = true;
354
+ i++;
355
+ }
356
+ else if (arg === '-full' || arg === '--full') {
357
+ cli.full = true;
358
+ i++;
359
+ }
360
+ else if (arg === '-no-escape-closes' || arg === '--no-escape-closes') {
361
+ cli.escapeCloses = false;
362
+ i++;
363
+ }
364
+ else if (arg === '-reset' || arg === '--reset') {
365
+ cli.reset = true;
366
+ i++;
367
+ }
368
+ else if (arg === '-ontop' || arg === '--ontop' || arg === '-alwaysontop' || arg === '--alwaysontop') {
369
+ cli.alwaysOnTop = true;
370
+ i++;
371
+ }
372
+ else if (arg === '-size' || arg === '--size') {
373
+ if (i + 1 >= args.length) {
374
+ throw new Error('-size requires a width,height argument');
375
+ }
376
+ const size = args[++i];
377
+ const [w, h] = size.split(',').map(s => parseInt(s.trim(), 10));
378
+ if (w)
379
+ cli.width = w;
380
+ if (h)
381
+ cli.height = h;
382
+ i++;
383
+ }
384
+ else if (arg === '-zoom' || arg === '--zoom') {
385
+ if (i + 1 >= args.length) {
386
+ throw new Error('-zoom requires a percentage argument');
387
+ }
388
+ cli.zoom = parseFloat(args[++i]);
389
+ i++;
390
+ }
391
+ else if (arg === '-pos' || arg === '--pos') {
392
+ if (i + 1 >= args.length) {
393
+ throw new Error('-pos requires an x,y or x,y,screen argument');
394
+ }
395
+ const pos = args[++i];
396
+ const parts = pos.split(',').map(s => parseInt(s.trim(), 10));
397
+ if (!isNaN(parts[0]))
398
+ cli.posX = parts[0];
399
+ if (!isNaN(parts[1]))
400
+ cli.posY = parts[1];
401
+ if (!isNaN(parts[2]))
402
+ cli.screen = parts[2];
403
+ i++;
404
+ }
405
+ else if (arg === '-screen' || arg === '--screen') {
406
+ if (i + 1 >= args.length) {
407
+ throw new Error('-screen requires a number argument');
408
+ }
409
+ cli.screen = parseInt(args[++i], 10);
410
+ i++;
411
+ }
412
+ else if (arg === '-icon' || arg === '--icon') {
413
+ if (i + 1 >= args.length) {
414
+ throw new Error('-icon requires a file path argument');
415
+ }
416
+ cli.icon = args[++i];
417
+ i++;
418
+ }
419
+ else if (arg === '-dev' || arg === '--dev') {
420
+ cli.dev = true;
421
+ i++;
422
+ }
423
+ else if (arg === '-debug' || arg === '--debug') {
424
+ cli.debug = true;
425
+ i++;
426
+ }
427
+ else if (arg === '-load' || arg === '--load') {
428
+ if (i + 1 >= args.length) {
429
+ throw new Error('-load requires a file path argument');
430
+ }
431
+ cli.load = args[++i];
432
+ i++;
433
+ }
434
+ else if (arg === '-save' || arg === '--save') {
435
+ const nextArg = args[i + 1];
436
+ if (nextArg && !nextArg.startsWith('-')) {
437
+ cli.save = nextArg;
438
+ i += 2;
439
+ }
440
+ else {
441
+ cli.save = true;
442
+ i++;
443
+ }
444
+ }
445
+ else if (arg === '-noshow' || arg === '--noshow') {
446
+ cli.noshow = true;
447
+ i++;
448
+ }
449
+ else if (arg === '-pin' || arg === '--pin') {
450
+ cli.pin = true;
451
+ i++;
452
+ }
453
+ else if (!arg.startsWith('-')) {
454
+ messageWords.push(arg);
455
+ i++;
456
+ }
457
+ else {
458
+ console.warn(`Unknown option: ${arg}`);
459
+ i++;
460
+ }
461
+ }
462
+ if (cli.full && cli.url) {
463
+ throw new Error('-full and -url cannot be used together. Did you mean -fullscreen?');
464
+ }
465
+ let message;
466
+ let html;
467
+ let url;
468
+ if (cli.url) {
469
+ message = cli.url;
470
+ url = cli.url;
471
+ }
472
+ else if (cli.htmlFrom) {
473
+ if (cli.htmlFrom.startsWith('http://') || cli.htmlFrom.startsWith('https://')) {
474
+ try {
475
+ const fetchCommand = `node --input-type=module -e "const res = await fetch('${cli.htmlFrom}'); console.log(await res.text())"`;
476
+ const htmlContent = execSync(fetchCommand, { encoding: 'utf-8' });
477
+ message = htmlContent;
478
+ html = htmlContent;
479
+ }
480
+ catch (error) {
481
+ throw new Error(`Failed to fetch HTML from URL: ${error.message}`);
482
+ }
483
+ }
484
+ else {
485
+ const htmlPath = path.resolve(cli.htmlFrom);
486
+ if (!fs.existsSync(htmlPath)) {
487
+ throw new Error(`HTML file not found: ${htmlPath}`);
488
+ }
489
+ try {
490
+ const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
491
+ message = htmlContent;
492
+ html = htmlContent;
493
+ }
494
+ catch (error) {
495
+ throw new Error(`Failed to read HTML file: ${error.message}`);
496
+ }
497
+ }
498
+ }
499
+ else if (cli.html) {
500
+ message = cli.html;
501
+ html = cli.html;
502
+ }
503
+ else if (cli.message) {
504
+ message = cli.message;
505
+ }
506
+ else if (messageWords.length > 0) {
507
+ message = messageWords.join(' ');
508
+ }
509
+ else {
510
+ message = '';
511
+ }
512
+ const options = {
513
+ title: cli.title,
514
+ message,
515
+ html,
516
+ url,
517
+ hash: cli.hash,
518
+ buttons: cli.buttons,
519
+ allowInput: cli.input,
520
+ defaultValue: cli.defaultValue,
521
+ inputPlaceholder: cli.inputPlaceholder,
522
+ timeout: cli.timeout,
523
+ detach: cli.detach,
524
+ fullscreen: cli.fullscreen,
525
+ alwaysOnTop: cli.alwaysOnTop,
526
+ zoom: cli.zoom,
527
+ icon: cli.icon,
528
+ dev: cli.dev,
529
+ debug: cli.debug
530
+ };
531
+ if (cli.width !== undefined || cli.height !== undefined) {
532
+ const defaultWidth = url ? 1024 : 600;
533
+ const defaultHeight = url ? 768 : 400;
534
+ options.size = {
535
+ width: cli.width !== undefined ? cli.width : defaultWidth,
536
+ height: cli.height !== undefined ? cli.height : defaultHeight,
537
+ };
538
+ }
539
+ else if (url) {
540
+ options.size = {
541
+ width: 1024,
542
+ height: 768,
543
+ };
544
+ }
545
+ if (cli.posX !== undefined && cli.posY !== undefined) {
546
+ options.pos = { x: cli.posX, y: cli.posY };
547
+ if (cli.screen !== undefined) {
548
+ options.pos.screen = cli.screen;
549
+ }
550
+ }
551
+ return {
552
+ options,
553
+ showHelp: cli.help || false,
554
+ showVersion: cli.version || false,
555
+ loadFile: cli.load,
556
+ saveFile: cli.save,
557
+ noshow: cli.noshow || false,
558
+ pin: cli.pin || false
559
+ };
560
+ }
561
+ function loadConfigFile(filePath) {
562
+ const absolutePath = path.resolve(filePath);
563
+ if (!fs.existsSync(absolutePath)) {
564
+ throw new Error(`Config file not found: ${absolutePath}`);
565
+ }
566
+ const fileContent = fs.readFileSync(absolutePath, 'utf-8');
567
+ try {
568
+ const config = JSON5.parse(fileContent);
569
+ if (config.pos && typeof config.pos === 'string') {
570
+ const parts = config.pos.split(',').map((s) => parseInt(s.trim(), 10));
571
+ const posObj = { x: parts[0], y: parts[1] };
572
+ if (!isNaN(parts[2])) {
573
+ posObj.screen = parts[2];
574
+ }
575
+ config.pos = posObj;
576
+ }
577
+ // Resolve icon path relative to JSON directory
578
+ if (config.icon && !path.isAbsolute(config.icon)) {
579
+ const jsonDir = path.dirname(absolutePath);
580
+ config.icon = path.resolve(jsonDir, config.icon);
581
+ }
582
+ return config;
583
+ }
584
+ catch (error) {
585
+ throw new Error(`Failed to parse config file ${filePath}: ${error.message}`);
586
+ }
587
+ }
588
+ function saveConfigFile(filePath, options) {
589
+ const absolutePath = path.resolve(filePath);
590
+ const config = {};
591
+ if (options.title && options.title !== 'Message')
592
+ config.title = options.title;
593
+ if (options.message)
594
+ config.message = options.message;
595
+ if (options.html)
596
+ config.html = options.html;
597
+ // Save URL without hash fragment (hash is specified via -hash option)
598
+ if (options.url)
599
+ config.url = options.url.split('#')[0];
600
+ if (options.hash)
601
+ config.hash = options.hash;
602
+ if (options.size)
603
+ config.size = options.size;
604
+ if (options.pos) {
605
+ config.pos = { x: options.pos.x, y: options.pos.y };
606
+ if (options.pos.screen !== undefined) {
607
+ config.pos.screen = options.pos.screen;
608
+ }
609
+ }
610
+ if (options.buttons && options.buttons.length > 0 && JSON.stringify(options.buttons) !== '["OK"]') {
611
+ config.buttons = options.buttons;
612
+ }
613
+ if (options.allowInput)
614
+ config.allowInput = options.allowInput;
615
+ if (options.defaultValue)
616
+ config.defaultValue = options.defaultValue;
617
+ if (options.inputPlaceholder)
618
+ config.inputPlaceholder = options.inputPlaceholder;
619
+ if (options.timeout)
620
+ config.timeout = options.timeout;
621
+ if (options.alwaysOnTop)
622
+ config.alwaysOnTop = options.alwaysOnTop;
623
+ if (options.fullscreen)
624
+ config.fullscreen = options.fullscreen;
625
+ if (options.zoom && options.zoom !== 100)
626
+ config.zoom = options.zoom;
627
+ if (options.icon)
628
+ config.icon = options.icon;
629
+ if (options.dev)
630
+ config.dev = options.dev;
631
+ if (options.debug)
632
+ config.debug = options.debug;
633
+ const jsonContent = JSON.stringify(config, null, 4) + '\n';
634
+ fs.writeFileSync(absolutePath, jsonContent, 'utf-8');
635
+ }
636
+ function showHelp() {
637
+ console.log(HELP_TEXT);
638
+ }
639
+ function showVersion() {
640
+ console.log(packageJson.version);
641
+ }
642
+ // Run main when file is executed (either directly or as a bin script)
643
+ if (import.meta.main) {
644
+ await main();
645
+ }
package/cli.d.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  #!/usr/bin/env node
2
- export default function main(): Promise<void>;
2
+ /**
3
+ * msger CLI - uses universal CLI from msgcommon
4
+ */
5
+ export {};
package/cli.js CHANGED
@@ -1,162 +1,10 @@
1
1
  #!/usr/bin/env node
2
- //
3
- // TODO: Migrate to use @bobfrankston/msgcommon for argument parsing
4
- // Example:
5
- // import { parseCommonArgs } from '@bobfrankston/msgcommon';
6
- // const { options, messageWords, showHelp, showVersion } = parseCommonArgs(args);
7
- //
2
+ /**
3
+ * msger CLI - uses universal CLI from msgcommon
4
+ */
5
+ import { runCli } from '@bobfrankston/msgcommon';
8
6
  import { showMessageBox } from './shower.js';
9
7
  import packageJson from './package.json' with { type: 'json' };
10
- import fs from 'fs';
11
- import path from 'path';
12
- import { execSync } from 'child_process';
13
- import JSON5 from 'json5';
14
- // Uncomment when ready to migrate:
15
- // import { parseCommonArgs } from '@bobfrankston/msgcommon';
16
- export default async function main() {
17
- const args = process.argv.slice(2);
18
- // No arguments - show help
19
- if (args.length === 0) {
20
- showHelp();
21
- process.exit(0);
22
- }
23
- // Parse command line arguments
24
- let options, shouldShowHelp, shouldShowVersion, loadFile, saveFile, noshow;
25
- try {
26
- ({ options, showHelp: shouldShowHelp, showVersion: shouldShowVersion, loadFile, saveFile, noshow } = parseCliArgs(args));
27
- }
28
- catch (error) {
29
- console.error('Error:', error.message);
30
- console.error('\nUse -help to see usage information.');
31
- process.exit(1);
32
- }
33
- // Helper to add .json extension if no extension specified
34
- const ensureJsonExt = (filename) => {
35
- const ext = path.extname(filename);
36
- return ext ? filename : `${filename}.json`;
37
- };
38
- // Load config file if specified
39
- let finalOptions = options;
40
- if (loadFile) {
41
- try {
42
- const loadedConfig = loadConfigFile(ensureJsonExt(loadFile));
43
- finalOptions = { ...loadedConfig };
44
- if (options.title)
45
- finalOptions.title = options.title;
46
- if (options.message)
47
- finalOptions.message = options.message;
48
- if (options.html)
49
- finalOptions.html = options.html;
50
- if (options.url)
51
- finalOptions.url = options.url;
52
- if (options.hash)
53
- finalOptions.hash = options.hash;
54
- if (options.size)
55
- finalOptions.size = options.size;
56
- if (options.pos)
57
- finalOptions.pos = options.pos;
58
- if (options.buttons && options.buttons.length > 0)
59
- finalOptions.buttons = options.buttons;
60
- if (options.allowInput !== undefined)
61
- finalOptions.allowInput = options.allowInput;
62
- if (options.defaultValue)
63
- finalOptions.defaultValue = options.defaultValue;
64
- if (options.inputPlaceholder)
65
- finalOptions.inputPlaceholder = options.inputPlaceholder;
66
- if (options.timeout !== undefined)
67
- finalOptions.timeout = options.timeout;
68
- if (options.alwaysOnTop !== undefined)
69
- finalOptions.alwaysOnTop = options.alwaysOnTop;
70
- if (options.fullscreen !== undefined)
71
- finalOptions.fullscreen = options.fullscreen;
72
- if (options.zoom !== undefined)
73
- finalOptions.zoom = options.zoom;
74
- if (options.icon)
75
- finalOptions.icon = options.icon;
76
- if (options.dev !== undefined)
77
- finalOptions.dev = options.dev;
78
- if (options.debug !== undefined)
79
- finalOptions.debug = options.debug;
80
- if (options.detach !== undefined)
81
- finalOptions.detach = options.detach;
82
- }
83
- catch (error) {
84
- console.error('Error loading config:', error.message);
85
- process.exit(1);
86
- }
87
- }
88
- if (!finalOptions.buttons || finalOptions.buttons.length === 0) {
89
- finalOptions.buttons = ['OK'];
90
- }
91
- // Help always takes precedence and exits
92
- if (shouldShowHelp) {
93
- showHelp();
94
- process.exit(0);
95
- }
96
- // Version only exits if there's nothing else meaningful to do
97
- // (no actual message/url/html content was provided - the default message doesn't count)
98
- const hasRealContent = args.some(arg => !arg.startsWith('-') ||
99
- arg === '-message' ||
100
- arg === '-html' ||
101
- arg === '-url');
102
- if (shouldShowVersion && !hasRealContent) {
103
- showVersion();
104
- process.exit(0);
105
- }
106
- // If version was requested along with other options, add it to title
107
- if (shouldShowVersion && hasRealContent) {
108
- finalOptions.showVersion = true;
109
- }
110
- if (saveFile) {
111
- try {
112
- let saveFilePath;
113
- if (saveFile === true) {
114
- if (!loadFile) {
115
- console.error('Error: -save without filename requires -load to be specified');
116
- process.exit(1);
117
- }
118
- saveFilePath = ensureJsonExt(loadFile);
119
- }
120
- else {
121
- saveFilePath = ensureJsonExt(saveFile);
122
- }
123
- const toSave = saveFile === true ? finalOptions : options;
124
- saveConfigFile(saveFilePath, toSave);
125
- console.log(`Configuration saved to: ${saveFilePath}`);
126
- if (noshow) {
127
- process.exit(0);
128
- }
129
- if (!finalOptions.message && !finalOptions.url && !finalOptions.html) {
130
- process.exit(0);
131
- }
132
- }
133
- catch (error) {
134
- console.error('Error saving config:', error.message);
135
- process.exit(1);
136
- }
137
- }
138
- // If no actual message/content was provided, show help
139
- if (!finalOptions.message && !finalOptions.url && !finalOptions.html) {
140
- showHelp();
141
- process.exit(0);
142
- }
143
- try {
144
- const result = await showMessageBox(finalOptions);
145
- // If detached, exit immediately without printing result
146
- if (finalOptions.detach) {
147
- process.exit(0);
148
- }
149
- console.log(JSON.stringify(result, null, 2));
150
- // Exit with code based on result
151
- if (result.dismissed || result.closed) {
152
- process.exit(1);
153
- }
154
- }
155
- catch (error) {
156
- console.error('Error:', error.message);
157
- process.exit(1);
158
- }
159
- }
160
8
  const HELP_TEXT = `
161
9
  msger v${packageJson.version} - Fast, lightweight, cross-platform message box (Rust-powered)
162
10
 
@@ -192,6 +40,7 @@ Options:
192
40
  -load <file> Load options from JSON file (supports comments)
193
41
  -save <file> Save current options to JSON file
194
42
  -noshow Save config without showing message box (use with -save)
43
+ -pin [Not yet implemented] Create pinnable taskbar shortcut
195
44
  -dev Open DevTools (F12) automatically on startup (for debugging)
196
45
  -debug Return debug info (HTML, size) in result
197
46
  -v, -version, --version Show version number (alone: print and exit; with options: show in title)
@@ -224,391 +73,9 @@ Security Note:
224
73
  - It is NOT a secure sandbox for untrusted/hostile web content
225
74
  - Use -htmlfrom and -url with trusted sources only
226
75
  `;
227
- function parseCliArgs(args) {
228
- const cli = {
229
- buttons: [],
230
- input: false,
231
- help: false
232
- };
233
- let i = 0;
234
- const messageWords = [];
235
- while (i < args.length) {
236
- const arg = args[i];
237
- if (arg === '-help' || arg === '--help' || arg === '-?' || arg === '/?') {
238
- cli.help = true;
239
- i++;
240
- }
241
- else if (arg === '-v' || arg === '-version' || arg === '--version') {
242
- cli.version = true;
243
- i++;
244
- }
245
- else if (arg === '-message' || arg === '--message') {
246
- if (i + 1 >= args.length) {
247
- throw new Error('-message requires a text argument');
248
- }
249
- cli.message = args[++i];
250
- i++;
251
- }
252
- else if (arg === '-title' || arg === '--title') {
253
- if (i + 1 >= args.length) {
254
- throw new Error('-title requires a text argument');
255
- }
256
- cli.title = args[++i];
257
- i++;
258
- }
259
- else if (arg === '-html' || arg === '--html') {
260
- if (i + 1 >= args.length) {
261
- throw new Error('-html requires an HTML string argument');
262
- }
263
- cli.html = args[++i];
264
- i++;
265
- }
266
- else if (arg === '-htmlfrom' || arg === '--htmlfrom') {
267
- if (i + 1 >= args.length) {
268
- throw new Error('-htmlfrom requires a file path or URL argument');
269
- }
270
- cli.htmlFrom = args[++i];
271
- i++;
272
- }
273
- else if (arg === '-url' || arg === '--url') {
274
- if (i + 1 >= args.length) {
275
- throw new Error('-url requires a URL argument');
276
- }
277
- cli.url = args[++i];
278
- i++;
279
- }
280
- else if (arg === '-hash' || arg === '--hash') {
281
- if (i + 1 >= args.length) {
282
- throw new Error('-hash requires a fragment argument');
283
- }
284
- cli.hash = args[++i];
285
- i++;
286
- }
287
- else if (arg === '-buttons' || arg === '--buttons') {
288
- i++;
289
- while (i < args.length && !args[i].startsWith('-')) {
290
- cli.buttons.push(args[i]);
291
- i++;
292
- }
293
- }
294
- else if (arg === '-ok' || arg === '--ok') {
295
- if (!cli.buttons.includes('OK')) {
296
- cli.buttons.push('OK');
297
- }
298
- i++;
299
- }
300
- else if (arg === '-cancel' || arg === '--cancel') {
301
- if (!cli.buttons.includes('Cancel')) {
302
- cli.buttons.push('Cancel');
303
- }
304
- i++;
305
- }
306
- else if (arg === '-input' || arg === '--input') {
307
- cli.input = true;
308
- if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
309
- cli.inputPlaceholder = args[++i];
310
- }
311
- i++;
312
- }
313
- else if (arg === '-default' || arg === '--default') {
314
- if (i + 1 >= args.length) {
315
- throw new Error('-default requires a text argument');
316
- }
317
- cli.defaultValue = args[++i];
318
- i++;
319
- }
320
- else if (arg === '-timeout' || arg === '--timeout') {
321
- if (i + 1 >= args.length) {
322
- throw new Error('-timeout requires a number argument');
323
- }
324
- cli.timeout = parseInt(args[++i], 10);
325
- i++;
326
- }
327
- else if (arg === '-detach' || arg === '--detach') {
328
- cli.detach = true;
329
- i++;
330
- }
331
- else if (arg === '-fullscreen' || arg === '--fullscreen') {
332
- cli.fullscreen = true;
333
- i++;
334
- }
335
- else if (arg === '-full' || arg === '--full') {
336
- cli.full = true;
337
- i++;
338
- }
339
- else if (arg === '-no-escape-closes' || arg === '--no-escape-closes') {
340
- cli.escapeCloses = false;
341
- i++;
342
- }
343
- else if (arg === '-reset' || arg === '--reset') {
344
- cli.reset = true;
345
- i++;
346
- }
347
- else if (arg === '-ontop' || arg === '--ontop' || arg === '-alwaysontop' || arg === '--alwaysontop') {
348
- cli.alwaysOnTop = true;
349
- i++;
350
- }
351
- else if (arg === '-size' || arg === '--size') {
352
- if (i + 1 >= args.length) {
353
- throw new Error('-size requires a width,height argument');
354
- }
355
- const size = args[++i];
356
- const [w, h] = size.split(',').map(s => parseInt(s.trim(), 10));
357
- if (w)
358
- cli.width = w;
359
- if (h)
360
- cli.height = h;
361
- i++;
362
- }
363
- else if (arg === '-zoom' || arg === '--zoom') {
364
- if (i + 1 >= args.length) {
365
- throw new Error('-zoom requires a percentage argument');
366
- }
367
- cli.zoom = parseFloat(args[++i]);
368
- i++;
369
- }
370
- else if (arg === '-pos' || arg === '--pos') {
371
- if (i + 1 >= args.length) {
372
- throw new Error('-pos requires an x,y or x,y,screen argument');
373
- }
374
- const pos = args[++i];
375
- const parts = pos.split(',').map(s => parseInt(s.trim(), 10));
376
- if (!isNaN(parts[0]))
377
- cli.posX = parts[0];
378
- if (!isNaN(parts[1]))
379
- cli.posY = parts[1];
380
- if (!isNaN(parts[2]))
381
- cli.screen = parts[2];
382
- i++;
383
- }
384
- else if (arg === '-screen' || arg === '--screen') {
385
- if (i + 1 >= args.length) {
386
- throw new Error('-screen requires a number argument');
387
- }
388
- cli.screen = parseInt(args[++i], 10);
389
- i++;
390
- }
391
- else if (arg === '-icon' || arg === '--icon') {
392
- if (i + 1 >= args.length) {
393
- throw new Error('-icon requires a file path argument');
394
- }
395
- cli.icon = args[++i];
396
- i++;
397
- }
398
- else if (arg === '-dev' || arg === '--dev') {
399
- cli.dev = true;
400
- i++;
401
- }
402
- else if (arg === '-debug' || arg === '--debug') {
403
- cli.debug = true;
404
- i++;
405
- }
406
- else if (arg === '-load' || arg === '--load') {
407
- if (i + 1 >= args.length) {
408
- throw new Error('-load requires a file path argument');
409
- }
410
- cli.load = args[++i];
411
- i++;
412
- }
413
- else if (arg === '-save' || arg === '--save') {
414
- const nextArg = args[i + 1];
415
- if (nextArg && !nextArg.startsWith('-')) {
416
- cli.save = nextArg;
417
- i += 2;
418
- }
419
- else {
420
- cli.save = true;
421
- i++;
422
- }
423
- }
424
- else if (arg === '-noshow' || arg === '--noshow') {
425
- cli.noshow = true;
426
- i++;
427
- }
428
- else if (!arg.startsWith('-')) {
429
- messageWords.push(arg);
430
- i++;
431
- }
432
- else {
433
- console.warn(`Unknown option: ${arg}`);
434
- i++;
435
- }
436
- }
437
- if (cli.full && cli.url) {
438
- throw new Error('-full and -url cannot be used together. Did you mean -fullscreen?');
439
- }
440
- let message;
441
- let html;
442
- let url;
443
- if (cli.url) {
444
- message = cli.url;
445
- url = cli.url;
446
- }
447
- else if (cli.htmlFrom) {
448
- if (cli.htmlFrom.startsWith('http://') || cli.htmlFrom.startsWith('https://')) {
449
- try {
450
- const fetchCommand = `node --input-type=module -e "const res = await fetch('${cli.htmlFrom}'); console.log(await res.text())"`;
451
- const htmlContent = execSync(fetchCommand, { encoding: 'utf-8' });
452
- message = htmlContent;
453
- html = htmlContent;
454
- }
455
- catch (error) {
456
- throw new Error(`Failed to fetch HTML from URL: ${error.message}`);
457
- }
458
- }
459
- else {
460
- const htmlPath = path.resolve(cli.htmlFrom);
461
- if (!fs.existsSync(htmlPath)) {
462
- throw new Error(`HTML file not found: ${htmlPath}`);
463
- }
464
- try {
465
- const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
466
- message = htmlContent;
467
- html = htmlContent;
468
- }
469
- catch (error) {
470
- throw new Error(`Failed to read HTML file: ${error.message}`);
471
- }
472
- }
473
- }
474
- else if (cli.html) {
475
- message = cli.html;
476
- html = cli.html;
477
- }
478
- else if (cli.message) {
479
- message = cli.message;
480
- }
481
- else if (messageWords.length > 0) {
482
- message = messageWords.join(' ');
483
- }
484
- else {
485
- message = '';
486
- }
487
- const options = {
488
- title: cli.title,
489
- message,
490
- html,
491
- url,
492
- hash: cli.hash,
493
- buttons: cli.buttons,
494
- allowInput: cli.input,
495
- defaultValue: cli.defaultValue,
496
- inputPlaceholder: cli.inputPlaceholder,
497
- timeout: cli.timeout,
498
- detach: cli.detach,
499
- fullscreen: cli.fullscreen,
500
- alwaysOnTop: cli.alwaysOnTop,
501
- zoom: cli.zoom,
502
- icon: cli.icon,
503
- dev: cli.dev,
504
- debug: cli.debug
505
- };
506
- if (cli.width !== undefined || cli.height !== undefined) {
507
- const defaultWidth = url ? 1024 : 600;
508
- const defaultHeight = url ? 768 : 400;
509
- options.size = {
510
- width: cli.width !== undefined ? cli.width : defaultWidth,
511
- height: cli.height !== undefined ? cli.height : defaultHeight,
512
- };
513
- }
514
- else if (url) {
515
- options.size = {
516
- width: 1024,
517
- height: 768,
518
- };
519
- }
520
- if (cli.posX !== undefined && cli.posY !== undefined) {
521
- options.pos = { x: cli.posX, y: cli.posY };
522
- if (cli.screen !== undefined) {
523
- options.pos.screen = cli.screen;
524
- }
525
- }
526
- return {
527
- options,
528
- showHelp: cli.help || false,
529
- showVersion: cli.version || false,
530
- loadFile: cli.load,
531
- saveFile: cli.save,
532
- noshow: cli.noshow || false
533
- };
534
- }
535
- function loadConfigFile(filePath) {
536
- const absolutePath = path.resolve(filePath);
537
- if (!fs.existsSync(absolutePath)) {
538
- throw new Error(`Config file not found: ${absolutePath}`);
539
- }
540
- const fileContent = fs.readFileSync(absolutePath, 'utf-8');
541
- try {
542
- const config = JSON5.parse(fileContent);
543
- if (config.pos && typeof config.pos === 'string') {
544
- const parts = config.pos.split(',').map((s) => parseInt(s.trim(), 10));
545
- const posObj = { x: parts[0], y: parts[1] };
546
- if (!isNaN(parts[2])) {
547
- posObj.screen = parts[2];
548
- }
549
- config.pos = posObj;
550
- }
551
- return config;
552
- }
553
- catch (error) {
554
- throw new Error(`Failed to parse config file ${filePath}: ${error.message}`);
555
- }
556
- }
557
- function saveConfigFile(filePath, options) {
558
- const absolutePath = path.resolve(filePath);
559
- const config = {};
560
- if (options.title && options.title !== 'Message')
561
- config.title = options.title;
562
- if (options.message)
563
- config.message = options.message;
564
- if (options.html)
565
- config.html = options.html;
566
- // Save URL without hash fragment (hash is specified via -hash option)
567
- if (options.url)
568
- config.url = options.url.split('#')[0];
569
- if (options.hash)
570
- config.hash = options.hash;
571
- if (options.size)
572
- config.size = options.size;
573
- if (options.pos) {
574
- config.pos = { x: options.pos.x, y: options.pos.y };
575
- if (options.pos.screen !== undefined) {
576
- config.pos.screen = options.pos.screen;
577
- }
578
- }
579
- if (options.buttons && options.buttons.length > 0 && JSON.stringify(options.buttons) !== '["OK"]') {
580
- config.buttons = options.buttons;
581
- }
582
- if (options.allowInput)
583
- config.allowInput = options.allowInput;
584
- if (options.defaultValue)
585
- config.defaultValue = options.defaultValue;
586
- if (options.inputPlaceholder)
587
- config.inputPlaceholder = options.inputPlaceholder;
588
- if (options.timeout)
589
- config.timeout = options.timeout;
590
- if (options.alwaysOnTop)
591
- config.alwaysOnTop = options.alwaysOnTop;
592
- if (options.fullscreen)
593
- config.fullscreen = options.fullscreen;
594
- if (options.zoom && options.zoom !== 100)
595
- config.zoom = options.zoom;
596
- if (options.icon)
597
- config.icon = options.icon;
598
- if (options.dev)
599
- config.dev = options.dev;
600
- if (options.debug)
601
- config.debug = options.debug;
602
- const jsonContent = JSON.stringify(config, null, 4) + '\n';
603
- fs.writeFileSync(absolutePath, jsonContent, 'utf-8');
604
- }
605
- function showHelp() {
606
- console.log(HELP_TEXT);
607
- }
608
- function showVersion() {
609
- console.log(packageJson.version);
610
- }
611
- // Run main when file is executed (either directly or as a bin script)
612
- if (import.meta.main) {
613
- await main();
614
- }
76
+ await runCli({
77
+ appName: 'msger',
78
+ version: packageJson.version,
79
+ helpText: HELP_TEXT,
80
+ showMessage: showMessageBox
81
+ });
package/index.js CHANGED
@@ -3,7 +3,6 @@ export { showMessageBox } 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) {
6
- // Import and run the CLI handler
7
- const { default: main } = await import('./cli.js');
8
- await main();
6
+ // Import CLI - it runs automatically via top-level await
7
+ import('./cli.js');
9
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/msger",
3
- "version": "0.1.170",
3
+ "version": "0.1.173",
4
4
  "description": "Fast, lightweight, cross-platform message box - Rust-powered alternative to msgview",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -50,7 +50,7 @@
50
50
  "@types/node": "^24.9.1"
51
51
  },
52
52
  "dependencies": {
53
- "@bobfrankston/msgcommon": "^0.1.0",
53
+ "@bobfrankston/msgcommon": "^0.1.6",
54
54
  "ansi-to-html": "^0.7.2",
55
55
  "json5": "^2.2.3"
56
56
  },
package/shower.d.ts CHANGED
@@ -40,6 +40,7 @@ export interface MessageBoxOptions {
40
40
  dev?: boolean; /** Open DevTools automatically on startup (default: false) */
41
41
  debug?: boolean; /** Return debug information (HTML, size) in result (default: false) */
42
42
  showVersion?: boolean; /** Show version in window title (default: false) */
43
+ appUserModelId?: string; /** Windows AppUserModelID for taskbar pinning (internal use) */
43
44
  }
44
45
  export interface MessageBoxResult {
45
46
  button: string;