@bobfrankston/msger 0.1.79 → 0.1.80

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/README.md CHANGED
@@ -512,98 +512,23 @@ Notes:
512
512
  - Runtime zoom: Ctrl+Wheel, Ctrl+Plus, Ctrl+Minus
513
513
  ```
514
514
 
515
- ## Current Status & Known Issues (v0.1.74)
516
-
517
- ### What Works ✅
518
-
519
- **Template Mode** (using `-message`, `-html`, `-htmlfrom`):
520
- - ✅ OK/Cancel buttons work perfectly
521
- - ✅ Button results returned to parent process via IPC
522
- - ✅ Custom buttons, input fields, forms all work
523
- - ✅ Native browser localStorage (recommended - works everywhere)
524
- - ✅ IPC communication functional
525
-
526
- **URL Mode** (using `-url`):
527
- - ✅ Direct webpage loading
528
- - ✅ PWA support (Service Workers, IndexedDB, Cache API)
529
- - ✅ Full web functionality
530
- - ✅ Native browser APIs work perfectly
531
-
532
- **General Features:**
533
- - ✅ Fast native window creation (~50-200ms)
534
- - ✅ Isolated storage (%LOCALAPPDATA%\msger\webview2\)
535
- - ✅ F11 HTML5 fullscreen (content fullscreen, not native window)
536
- - ✅ `-fullscreen` CLI flag for native window fullscreen
537
- - ✅ DevTools (press F12)
538
- - ✅ Esc key to close windows
539
- - ✅ Auto-close with `-timeout`
540
- - ✅ Detached mode with `-detach`
515
+ ## Important Notes
541
516
 
542
- ### msger JavaScript API
543
-
544
- **Minimal API** - Most functionality uses native browser APIs:
545
- - `msger.isAvailable()` - Feature detection (returns true in msger)
546
- - `msger.close()` - Same as `window.close()` (native API preferred)
547
- - **localStorage functions** - Optional convenience wrappers (native localStorage recommended)
548
-
549
- **Recommendation:** Use native browser APIs (`localStorage`, `window.close()`) for compatibility. Code will work in any browser context, not just msger.
550
-
551
- ### Known Limitations ❌
552
-
553
- **URL Mode IPC (wry 0.47.2 bug):**
554
- - ❌ All IPC calls crash in `-url` mode with injected scripts
555
- - ❌ No window control buttons work in URL mode
556
- - **Workaround:** Use template mode (`-htmlfrom` instead of `-url`) if you need buttons
557
-
558
- **F11 Fullscreen:**
559
- - ❌ F11 triggers HTML5 fullscreen (content only), not native window fullscreen
560
- - ❌ WebView2 captures F11 before Rust window handler
561
- - **Workaround:** Use `-fullscreen` CLI flag for native fullscreen
562
-
563
- **-dev Flag:**
564
- - ❌ Can't auto-open DevTools (wry 0.47.2 doesn't expose open_devtools() method)
565
- - **Workaround:** Press F12 manually (DevTools always enabled)
566
-
567
- **-htmlfrom with External Resources:**
568
- - ⚠️ External CSS/JS/images may be blocked by CSP when embedding HTML
569
- - **Workaround:** Use `-url` for pages with external resources, or use self-contained HTML
570
-
571
- ### Mode Comparison
572
-
573
- | Feature | `-message/-html` | `-htmlfrom` | `-url` |
574
- |---------|------------------|-------------|--------|
575
- | OK/Cancel buttons | ✅ | ✅ | ❌ |
576
- | Button results via IPC | ✅ | ✅ | ❌ (crashes) |
577
- | Native localStorage | ✅ | ✅ | ✅ |
578
- | Native window.close() | ✅ | ✅ | ✅ |
579
- | External resources | N/A | ⚠️ CSP blocks | ✅ |
580
- | PWA features | Limited | Limited | ✅ |
581
- | msger API needed? | No (native APIs work) | No (native APIs work) | No (IPC broken) |
582
- | Use case | Simple dialogs | Embedded HTML | Full web pages |
583
-
584
- ### Security Note
517
+ ### Security
585
518
 
586
519
  ⚠️ **msger is designed for displaying trusted, friendly content** (local apps, your HTML files). It is NOT a secure sandbox for untrusted/hostile web content. Use `-htmlfrom` and `-url` with trusted sources only.
587
520
 
588
- ### Future Improvements
521
+ ### Technical Details & Known Issues
589
522
 
590
- 1. **File System API** - Native file access for platform apps
591
- - **File dialogs** (always safe): `msger.selectFile()`, `msger.selectFiles()`, `msger.saveFileAs()`, `msger.selectFolder()`
592
- - **Direct path access** (requires `--allowFs` flag): `msger.fs.read(path)`, `msger.fs.write(path, content)`, `msger.fs.list(path)`, etc.
593
- - Returns actual file paths (better than `<input type="file">`)
594
- - Possible in template mode using bidirectional IPC (same mechanism as button clicks)
595
- - See SESSION-NOTES.md for full implementation approach
523
+ For detailed technical information, known limitations, current status, and planned features, see [TODO.md](TODO.md).
596
524
 
597
- 2. **Window Manipulation APIs** - Runtime window control in template mode
598
- - `msger.setSize()`, `msger.setPosition()`, `msger.setTitle()`, etc.
599
- - IPC works in template mode, these were disabled prematurely
600
- - Can be re-enabled with bidirectional IPC pattern
525
+ ### msger JavaScript API
601
526
 
602
- 3. **Periodic wry updates** - Check if URL mode IPC bug fixed in newer versions
603
- - Would enable file/window APIs in URL mode too
604
- - Currently works only in template mode
527
+ **Minimal API** - Most functionality uses native browser APIs:
528
+ - `msger.isAvailable()` - Feature detection (returns true in msger)
529
+ - `msger.close()` - Same as `window.close()` (native API preferred)
605
530
 
606
- 4. Cross-compile support for Raspberry Pi on Windows
531
+ **Recommendation:** Use native browser APIs (`localStorage`, `window.close()`) for compatibility. Code will work in any browser context, not just msger.
607
532
 
608
533
  ## License
609
534
 
package/TODO.md CHANGED
@@ -261,6 +261,20 @@ None currently known!
261
261
 
262
262
  ## Completed ✅
263
263
 
264
+ - ✅ JSON configuration file support (2025-11-11)
265
+ - Load from file: `msgernative config.json` or `-json config.json`
266
+ - CLI arguments override JSON values
267
+ - Pure CLI mode works without JSON
268
+ - Added `clap` crate for argument parsing
269
+ - ✅ Windows system menu integration (2025-11-11)
270
+ - "About (Window Info)" entry in system menu
271
+ - Shows window position, size, DPI scaling details
272
+ - Uses Windows API (GetSystemMenu, AppendMenuW)
273
+ - Window subclassing for WM_SYSCOMMAND handling
274
+ - ✅ Code refactoring (2025-11-11)
275
+ - Extracted JavaScript API to msger-api.js
276
+ - Uses include_str! for compile-time embedding
277
+ - Cleaner separation of concerns
264
278
  - ✅ Multi-monitor positioning fix - Rust side (2025-11-06)
265
279
  - ✅ Multi-monitor positioning fix - CLI parsing bug (2025-11-06)
266
280
  - Fixed: `-pos 10,10,1` now correctly parses third parameter as screen number
@@ -9,6 +9,7 @@ tao = "0.30"
9
9
  serde = { version = "1.0", features = ["derive"] }
10
10
  serde_json = "1.0"
11
11
  image = "0.24"
12
+ clap = { version = "4.5", features = ["derive"] }
12
13
 
13
14
  [target.'cfg(windows)'.dependencies]
14
15
  windows = { version = "0.58", features = ["Win32_UI_WindowsAndMessaging", "Win32_Foundation", "Win32_UI_Shell"] }
@@ -12,7 +12,7 @@ This is a native implementation alternative to `@bobfrankston/msgview` (Electron
12
12
 
13
13
  ```
14
14
  Node.js (msgview CLI)
15
- ↓ spawns with JSON input via stdin
15
+ ↓ spawns with JSON input via stdin OR JSON file + CLI args
16
16
  Rust msger executable
17
17
  ↓ creates native window with webview
18
18
  ↓ displays HTML message with buttons
@@ -21,24 +21,93 @@ Rust msger executable
21
21
  Node.js reads and returns result
22
22
  ```
23
23
 
24
+ ## Usage Modes
25
+
26
+ ### 1. Stdin (Original)
27
+ ```bash
28
+ echo '{"message":"Hello"}' | msgernative
29
+ ```
30
+
31
+ ### 2. JSON File
32
+ ```bash
33
+ msgernative config.json
34
+ # or
35
+ msgernative -json config.json
36
+ ```
37
+
38
+ ### 3. JSON File + CLI Overrides
39
+ ```bash
40
+ msgernative config.json --title "New Title" --width 800
41
+ ```
42
+
43
+ ### 4. Pure CLI (No JSON)
44
+ ```bash
45
+ msgernative --message "Hello World" --buttons "OK,Cancel"
46
+ ```
47
+
24
48
  ## API Compatibility
25
49
 
26
50
  This implementation is 100% compatible with `@bobfrankston/msgview` API:
27
51
 
28
- ### MessageBoxOptions (input via stdin)
52
+ ### MessageBoxOptions (Complete Structure)
29
53
  ```json
30
54
  {
31
- "title": "Message",
55
+ "title": "Window Title",
32
56
  "message": "Plain text message",
33
57
  "html": "<p>Optional HTML content</p>",
58
+ "url": "https://example.com",
34
59
  "width": 600,
35
60
  "height": 400,
61
+ "pos": {
62
+ "x": 100,
63
+ "y": 100,
64
+ "screen": 0
65
+ },
36
66
  "buttons": ["Cancel", "OK"],
37
67
  "defaultValue": "default input",
38
- "allowInput": false
68
+ "inputPlaceholder": "Enter text...",
69
+ "allowInput": false,
70
+ "timeout": 30,
71
+ "autoSize": false,
72
+ "alwaysOnTop": false,
73
+ "fullscreen": false,
74
+ "zoomPercent": 100,
75
+ "debug": false,
76
+ "icon": "path/to/icon.png",
77
+ "dev": false
39
78
  }
40
79
  ```
41
80
 
81
+ ### Command-Line Arguments
82
+
83
+ All JSON options can be overridden via command-line:
84
+
85
+ ```
86
+ OPTIONS:
87
+ -j, --json <FILE> JSON file to load defaults from
88
+ --title <TITLE> Window title
89
+ --message <MESSAGE> Plain text message
90
+ --html <HTML> HTML content
91
+ --url <URL> URL to load
92
+ --width <WIDTH> Window width
93
+ --height <HEIGHT> Window height
94
+ --x <X> Window X position
95
+ --y <Y> Window Y position
96
+ --screen <SCREEN> Screen number (multi-monitor)
97
+ --buttons <BUTTONS>... Button labels (comma-separated)
98
+ --default-value <VALUE> Default input value
99
+ --input-placeholder <TEXT> Input placeholder
100
+ --allow-input Allow text input
101
+ --timeout <SECONDS> Auto-close timeout
102
+ --auto-size Auto-size window to content
103
+ --always-on-top Keep window on top
104
+ --fullscreen Start in fullscreen
105
+ --zoom-percent <PERCENT> Zoom level
106
+ --debug Enable debug mode
107
+ --icon <PATH> Window icon path
108
+ --dev Enable developer mode
109
+ ```
110
+
42
111
  ### MessageBoxResult (output to stdout)
43
112
  ```json
44
113
  {
@@ -148,11 +217,21 @@ if (existsSync(nativeBinary)) {
148
217
 
149
218
  - ✅ Cross-platform native webview
150
219
  - ✅ Full HTML/CSS support
220
+ - ✅ URL loading support
151
221
  - ✅ Customizable buttons
152
222
  - ✅ Input field support
153
- - ✅ Keyboard shortcuts (Enter, Escape)
223
+ - ✅ Keyboard shortcuts (Enter, Escape, F11)
154
224
  - ✅ Window close detection
155
225
  - ✅ JSON stdin/stdout communication
226
+ - ✅ JSON file configuration support
227
+ - ✅ Command-line argument overrides
228
+ - ✅ Multi-monitor positioning
229
+ - ✅ Window icon support
230
+ - ✅ Auto-size and fullscreen modes
231
+ - ✅ Timeout support
232
+ - ✅ System menu integration (Windows)
233
+ - About dialog showing window info
234
+ - Position, size, DPI scaling details
156
235
  - ✅ Small binary size (~2-5MB)
157
236
  - ✅ Fast startup (~50-200ms)
158
237
 
Binary file
@@ -0,0 +1,20 @@
1
+ {
2
+ "title": "Example Message Box",
3
+ "message": "This is a sample message box configuration.\n\nYou can override any of these settings via command-line arguments.",
4
+ "width": 600,
5
+ "height": 400,
6
+ "pos": {
7
+ "x": 100,
8
+ "y": 100,
9
+ "screen": 0
10
+ },
11
+ "buttons": [
12
+ "Cancel",
13
+ "OK"
14
+ ],
15
+ "allowInput": false,
16
+ "alwaysOnTop": false,
17
+ "autoSize": false,
18
+ "fullscreen": false,
19
+ "zoomPercent": 100
20
+ }
@@ -1,5 +1,6 @@
1
1
  use serde::{Deserialize, Serialize};
2
2
  use std::io::{self, Read};
3
+ use std::fs;
3
4
  use tao::{
4
5
  event::{Event, WindowEvent, ElementState, KeyEvent},
5
6
  event_loop::{ControlFlow, EventLoop},
@@ -8,6 +9,7 @@ use tao::{
8
9
  platform::windows::WindowExtWindows,
9
10
  };
10
11
  use wry::{WebViewBuilder, WebContext};
12
+ use clap::Parser;
11
13
 
12
14
  #[cfg(windows)]
13
15
  use windows::Win32::UI::WindowsAndMessaging::{
@@ -56,6 +58,104 @@ unsafe extern "system" fn subclass_proc(
56
58
  // JavaScript API to inject into webviews
57
59
  const MSGER_JS_API: &str = include_str!("msger-api.js");
58
60
 
61
+ /// Message box application with webview support
62
+ #[derive(Parser, Debug)]
63
+ #[command(name = "msgernative")]
64
+ #[command(about = "Display message boxes with HTML/URL content", long_about = None)]
65
+ struct Cli {
66
+ /// JSON file to load default options from
67
+ #[arg(short, long, value_name = "FILE")]
68
+ json: Option<String>,
69
+
70
+ /// Window title
71
+ #[arg(long)]
72
+ title: Option<String>,
73
+
74
+ /// Plain text message to display
75
+ #[arg(long)]
76
+ message: Option<String>,
77
+
78
+ /// HTML content to display
79
+ #[arg(long)]
80
+ html: Option<String>,
81
+
82
+ /// URL to load
83
+ #[arg(long)]
84
+ url: Option<String>,
85
+
86
+ /// Window width
87
+ #[arg(long)]
88
+ width: Option<i32>,
89
+
90
+ /// Window height
91
+ #[arg(long)]
92
+ height: Option<i32>,
93
+
94
+ /// Window X position
95
+ #[arg(long)]
96
+ x: Option<i32>,
97
+
98
+ /// Window Y position
99
+ #[arg(long)]
100
+ y: Option<i32>,
101
+
102
+ /// Screen number (for multi-monitor setups)
103
+ #[arg(long)]
104
+ screen: Option<i32>,
105
+
106
+ /// Button labels (comma-separated)
107
+ #[arg(long, value_delimiter = ',')]
108
+ buttons: Option<Vec<String>>,
109
+
110
+ /// Default input value
111
+ #[arg(long)]
112
+ default_value: Option<String>,
113
+
114
+ /// Input placeholder text
115
+ #[arg(long)]
116
+ input_placeholder: Option<String>,
117
+
118
+ /// Allow text input
119
+ #[arg(long)]
120
+ allow_input: bool,
121
+
122
+ /// Timeout in seconds
123
+ #[arg(long)]
124
+ timeout: Option<u64>,
125
+
126
+ /// Auto-size window to content
127
+ #[arg(long)]
128
+ auto_size: bool,
129
+
130
+ /// Keep window always on top
131
+ #[arg(long)]
132
+ always_on_top: bool,
133
+
134
+ /// Start in fullscreen mode
135
+ #[arg(long)]
136
+ fullscreen: bool,
137
+
138
+ /// Zoom percentage
139
+ #[arg(long)]
140
+ zoom_percent: Option<f64>,
141
+
142
+ /// Enable debug mode
143
+ #[arg(long)]
144
+ debug: bool,
145
+
146
+ /// Path to window icon
147
+ #[arg(long)]
148
+ icon: Option<String>,
149
+
150
+ /// Enable developer mode
151
+ #[arg(long)]
152
+ dev: bool,
153
+
154
+ /// Positional argument (if ends with .json, treated as JSON file)
155
+ #[arg(value_name = "FILE_OR_JSON")]
156
+ file: Option<String>,
157
+ }
158
+
59
159
  #[derive(Deserialize, Debug)]
60
160
  #[serde(rename_all = "camelCase")]
61
161
  struct Position {
@@ -240,15 +340,135 @@ fn escape_html(s: &str) -> String {
240
340
  .replace('\'', "&#39;")
241
341
  }
242
342
 
343
+ fn load_options_from_json(json_str: &str) -> Result<MessageBoxOptions, serde_json::Error> {
344
+ serde_json::from_str(json_str)
345
+ }
346
+
347
+ fn merge_cli_with_options(cli: &Cli, mut options: MessageBoxOptions) -> MessageBoxOptions {
348
+ // Override with CLI arguments if provided
349
+ if let Some(ref title) = cli.title {
350
+ options.title = title.clone();
351
+ }
352
+ if let Some(ref message) = cli.message {
353
+ options.message = Some(message.clone());
354
+ }
355
+ if let Some(ref html) = cli.html {
356
+ options.html = Some(html.clone());
357
+ }
358
+ if let Some(ref url) = cli.url {
359
+ options.url = Some(url.clone());
360
+ }
361
+ if let Some(width) = cli.width {
362
+ options.width = width;
363
+ }
364
+ if let Some(height) = cli.height {
365
+ options.height = height;
366
+ }
367
+
368
+ // Handle position
369
+ if cli.x.is_some() || cli.y.is_some() || cli.screen.is_some() {
370
+ let existing_pos = options.pos.take().unwrap_or(Position {
371
+ x: 0,
372
+ y: 0,
373
+ screen: None,
374
+ });
375
+ options.pos = Some(Position {
376
+ x: cli.x.unwrap_or(existing_pos.x),
377
+ y: cli.y.unwrap_or(existing_pos.y),
378
+ screen: cli.screen.or(existing_pos.screen),
379
+ });
380
+ }
381
+
382
+ if let Some(ref buttons) = cli.buttons {
383
+ options.buttons = buttons.clone();
384
+ }
385
+ if let Some(ref default_value) = cli.default_value {
386
+ options.default_value = Some(default_value.clone());
387
+ }
388
+ if let Some(ref input_placeholder) = cli.input_placeholder {
389
+ options.input_placeholder = Some(input_placeholder.clone());
390
+ }
391
+ if cli.allow_input {
392
+ options.allow_input = true;
393
+ }
394
+ if let Some(timeout) = cli.timeout {
395
+ options.timeout = Some(timeout);
396
+ }
397
+ if cli.auto_size {
398
+ options.auto_size = true;
399
+ }
400
+ if cli.always_on_top {
401
+ options.always_on_top = true;
402
+ }
403
+ if cli.fullscreen {
404
+ options.fullscreen = true;
405
+ }
406
+ if let Some(zoom_percent) = cli.zoom_percent {
407
+ options.zoom_percent = Some(zoom_percent);
408
+ }
409
+ if cli.debug {
410
+ options.debug = true;
411
+ }
412
+ if let Some(ref icon) = cli.icon {
413
+ options.icon = Some(icon.clone());
414
+ }
415
+ if cli.dev {
416
+ options.dev = true;
417
+ }
418
+
419
+ options
420
+ }
421
+
243
422
  fn main() {
244
- // Read JSON from stdin
245
- let mut input = String::new();
246
- io::stdin()
247
- .read_to_string(&mut input)
248
- .expect("Failed to read stdin");
249
-
250
- let options: MessageBoxOptions =
251
- serde_json::from_str(&input).expect("Failed to parse JSON input");
423
+ // Parse command-line arguments
424
+ let cli = Cli::parse();
425
+
426
+ // Determine JSON source and load options
427
+ let options = if let Some(ref json_file) = cli.json {
428
+ // Explicit -json flag
429
+ let json_str = fs::read_to_string(json_file)
430
+ .unwrap_or_else(|e| panic!("Failed to read JSON file '{}': {}", json_file, e));
431
+ let base_options = load_options_from_json(&json_str)
432
+ .expect("Failed to parse JSON input");
433
+ merge_cli_with_options(&cli, base_options)
434
+ } else if let Some(ref file) = cli.file {
435
+ // First positional argument - check if it's a .json file
436
+ if file.ends_with(".json") {
437
+ let json_str = fs::read_to_string(file)
438
+ .unwrap_or_else(|e| panic!("Failed to read JSON file '{}': {}", file, e));
439
+ let base_options = load_options_from_json(&json_str)
440
+ .expect("Failed to parse JSON input");
441
+ merge_cli_with_options(&cli, base_options)
442
+ } else {
443
+ // Not a JSON file, read from stdin
444
+ let mut input = String::new();
445
+ io::stdin()
446
+ .read_to_string(&mut input)
447
+ .expect("Failed to read stdin");
448
+ let base_options = load_options_from_json(&input)
449
+ .expect("Failed to parse JSON input");
450
+ merge_cli_with_options(&cli, base_options)
451
+ }
452
+ } else {
453
+ // Check if stdin has data (piped input)
454
+ use std::io::IsTerminal;
455
+ if !io::stdin().is_terminal() {
456
+ // Data piped via stdin
457
+ let mut input = String::new();
458
+ io::stdin()
459
+ .read_to_string(&mut input)
460
+ .expect("Failed to read stdin");
461
+ let base_options = load_options_from_json(&input)
462
+ .expect("Failed to parse JSON input");
463
+ merge_cli_with_options(&cli, base_options)
464
+ } else {
465
+ // No JSON file, no stdin - create default options and apply CLI args
466
+ let default_json = "{}";
467
+ let base_options = load_options_from_json(default_json)
468
+ .expect("Failed to create default options");
469
+ merge_cli_with_options(&cli, base_options)
470
+ }
471
+ };
252
472
 
253
473
  // Create event loop and window
254
474
  let event_loop = EventLoop::new();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/msger",
3
- "version": "0.1.79",
3
+ "version": "0.1.80",
4
4
  "description": "Fast, lightweight, cross-platform message box - Rust-powered alternative to msgview",
5
5
  "type": "module",
6
6
  "main": "./index.js",