@bobfrankston/msger 0.1.78 → 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,3 +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"] }
13
+
14
+ [target.'cfg(windows)'.dependencies]
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,100 +1,160 @@
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},
6
7
  keyboard::KeyCode,
7
8
  window::WindowBuilder,
9
+ platform::windows::WindowExtWindows,
8
10
  };
9
11
  use wry::{WebViewBuilder, WebContext};
12
+ use clap::Parser;
10
13
 
11
- // JavaScript API to inject into webviews
12
- const MSGER_JS_API: &str = r#"
13
- (function() {
14
- console.log('[MSGER] Initializing msger API...');
15
-
16
- // Check if IPC is available
17
- const hasIPC = typeof window.ipc !== 'undefined' && window.ipc.postMessage;
18
- console.log('[MSGER] IPC available:', hasIPC);
19
-
20
- // Helper to safely send IPC messages
21
- function safePostMessage(data) {
22
- try {
23
- if (!hasIPC) {
24
- console.error('msger API: window.ipc not available');
25
- return false;
26
- }
27
- window.ipc.postMessage(JSON.stringify(data));
28
- return true;
29
- } catch(e) {
30
- console.error('msger API error:', e);
31
- return false;
14
+ #[cfg(windows)]
15
+ use windows::Win32::UI::WindowsAndMessaging::{
16
+ GetSystemMenu, AppendMenuW, MF_STRING, MF_SEPARATOR, WM_SYSCOMMAND,
17
+ };
18
+ #[cfg(windows)]
19
+ use windows::Win32::UI::Shell::{
20
+ SetWindowSubclass, DefSubclassProc,
21
+ };
22
+ #[cfg(windows)]
23
+ use windows::Win32::Foundation::{HWND, LPARAM, WPARAM, LRESULT};
24
+ #[cfg(windows)]
25
+ use windows::core::PCWSTR;
26
+ #[cfg(windows)]
27
+ use std::sync::atomic::{AtomicBool, Ordering};
28
+
29
+ // Custom menu ID for About item (must be < 0xF000 to avoid conflicts with system commands)
30
+ #[cfg(windows)]
31
+ const IDM_ABOUT: u32 = 0x1000;
32
+
33
+ // Global flag to signal About menu was clicked
34
+ #[cfg(windows)]
35
+ static ABOUT_CLICKED: AtomicBool = AtomicBool::new(false);
36
+
37
+ // Window subclass procedure to intercept WM_SYSCOMMAND
38
+ #[cfg(windows)]
39
+ unsafe extern "system" fn subclass_proc(
40
+ hwnd: HWND,
41
+ msg: u32,
42
+ wparam: WPARAM,
43
+ lparam: LPARAM,
44
+ _uidsubclass: usize,
45
+ _dwrefdata: usize,
46
+ ) -> LRESULT {
47
+ if msg == WM_SYSCOMMAND {
48
+ let cmd = wparam.0 & 0xFFF0;
49
+ if cmd == IDM_ABOUT as usize {
50
+ // Set flag that About was clicked
51
+ ABOUT_CLICKED.store(true, Ordering::SeqCst);
52
+ return LRESULT(0);
32
53
  }
33
54
  }
55
+ DefSubclassProc(hwnd, msg, wparam, lparam)
56
+ }
34
57
 
35
- window.msger = {
36
- // Window control - only close() with window.close() workaround (IPC broken)
37
- close: function(result) {
38
- // IPC is broken in wry 0.47.2, so we use window.close() instead
39
- // Result cannot be returned to parent process
40
- try {
41
- window.close();
42
- return true;
43
- } catch(e) {
44
- console.error('window.close() failed:', e);
45
- return false;
46
- }
47
- },
48
- saveData: function(key, value) {
49
- try {
50
- localStorage.setItem('msger_' + key, JSON.stringify(value));
51
- return true;
52
- } catch(e) {
53
- console.error('Failed to save data:', e);
54
- return false;
55
- }
56
- },
57
- loadData: function(key, defaultValue) {
58
- try {
59
- const item = localStorage.getItem('msger_' + key);
60
- return item ? JSON.parse(item) : defaultValue;
61
- } catch(e) {
62
- console.error('Failed to load data:', e);
63
- return defaultValue;
64
- }
65
- },
66
- removeData: function(key) {
67
- try {
68
- localStorage.removeItem('msger_' + key);
69
- return true;
70
- } catch(e) {
71
- console.error('Failed to remove data:', e);
72
- return false;
73
- }
74
- },
75
- clearData: function() {
76
- try {
77
- const keys = Object.keys(localStorage);
78
- for (const key of keys) {
79
- if (key.startsWith('msger_')) {
80
- localStorage.removeItem(key);
81
- }
82
- }
83
- return true;
84
- } catch(e) {
85
- console.error('Failed to clear data:', e);
86
- return false;
87
- }
88
- },
89
- // Check if running in msger environment
90
- isAvailable: function() {
91
- return true;
92
- }
93
- };
58
+ // JavaScript API to inject into webviews
59
+ const MSGER_JS_API: &str = include_str!("msger-api.js");
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>,
94
97
 
95
- console.log('[MSGER] API initialized successfully! window.msger =', window.msger);
96
- })();
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
+ }
98
158
 
99
159
  #[derive(Deserialize, Debug)]
100
160
  #[serde(rename_all = "camelCase")]
@@ -280,15 +340,135 @@ fn escape_html(s: &str) -> String {
280
340
  .replace('\'', "&#39;")
281
341
  }
282
342
 
283
- fn main() {
284
- // Read JSON from stdin
285
- let mut input = String::new();
286
- io::stdin()
287
- .read_to_string(&mut input)
288
- .expect("Failed to read stdin");
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
+ }
289
421
 
290
- let options: MessageBoxOptions =
291
- serde_json::from_str(&input).expect("Failed to parse JSON input");
422
+ fn main() {
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
+ };
292
472
 
293
473
  // Create event loop and window
294
474
  let event_loop = EventLoop::new();
@@ -359,6 +539,22 @@ fn main() {
359
539
  .build(&event_loop)
360
540
  .expect("Failed to create window"));
361
541
 
542
+ // Add About item to Windows system menu and install subclass
543
+ #[cfg(windows)]
544
+ unsafe {
545
+ let hwnd = HWND(window.hwnd() as *mut _);
546
+ let hmenu = GetSystemMenu(hwnd, false);
547
+ if !hmenu.is_invalid() {
548
+ // Add separator
549
+ AppendMenuW(hmenu, MF_SEPARATOR, 0, PCWSTR::null()).ok();
550
+ // Add About item
551
+ let about_text: Vec<u16> = "About (Window Info)\0".encode_utf16().collect();
552
+ AppendMenuW(hmenu, MF_STRING, IDM_ABOUT as usize, PCWSTR(about_text.as_ptr())).ok();
553
+ }
554
+ // Install window subclass to intercept WM_SYSCOMMAND
555
+ SetWindowSubclass(hwnd, Some(subclass_proc), 1, 0).ok();
556
+ }
557
+
362
558
  // Generate HTML
363
559
  let html = generate_html(&options);
364
560
 
@@ -488,6 +684,55 @@ fn main() {
488
684
 
489
685
  // Run event loop
490
686
  event_loop.run(move |event, _,control_flow| {
687
+ // Check for About menu click
688
+ #[cfg(windows)]
689
+ if ABOUT_CLICKED.swap(false, Ordering::SeqCst) {
690
+ // Get window information
691
+ let position = window_clone.outer_position().unwrap_or_default();
692
+ let inner_size = window_clone.inner_size();
693
+ let outer_size = window_clone.outer_size();
694
+ let scale_factor = window_clone.scale_factor();
695
+ let is_maximized = window_clone.is_maximized();
696
+ let is_fullscreen = window_clone.fullscreen().is_some();
697
+ let is_always_on_top = window_clone.is_always_on_top();
698
+
699
+ // Calculate logical sizes (what you specify in the API)
700
+ let logical_inner_width = (inner_size.width as f64 / scale_factor) as i32;
701
+ let logical_inner_height = (inner_size.height as f64 / scale_factor) as i32;
702
+ let logical_outer_width = (outer_size.width as f64 / scale_factor) as i32;
703
+ let logical_outer_height = (outer_size.height as f64 / scale_factor) as i32;
704
+
705
+ let info = format!(
706
+ "Window Information\\n\\n\
707
+ Position: x={}, y={}\\n\\n\
708
+ Logical Size (API): {}x{} inner\\n\
709
+ Physical Size (Screen): {}x{} inner\\n\
710
+ Outer Size (Physical): {}x{}\\n\
711
+ Outer Size (Logical): {}x{}\\n\\n\
712
+ Scale Factor: {:.2} ({}% DPI)\\n\
713
+ Maximized: {}\\n\
714
+ Fullscreen: {}\\n\
715
+ Always On Top: {}\\n\\n\
716
+ Version: {}",
717
+ position.x, position.y,
718
+ logical_inner_width, logical_inner_height,
719
+ inner_size.width, inner_size.height,
720
+ outer_size.width, outer_size.height,
721
+ logical_outer_width, logical_outer_height,
722
+ scale_factor, (scale_factor * 100.0) as i32,
723
+ is_maximized,
724
+ is_fullscreen,
725
+ is_always_on_top,
726
+ env!("NPM_VERSION")
727
+ );
728
+
729
+ // Show the info in an alert
730
+ let script = format!("alert('{}')", info);
731
+ if let Err(e) = webview.evaluate_script(&script) {
732
+ eprintln!("Failed to show about dialog: {}", e);
733
+ }
734
+ }
735
+
491
736
  // Check timeout
492
737
  if let Some(deadline) = timeout_instant {
493
738
  if std::time::Instant::now() >= deadline {
@@ -0,0 +1,84 @@
1
+ (function() {
2
+ console.log('[MSGER] Initializing msger API...');
3
+
4
+ // Check if IPC is available
5
+ const hasIPC = typeof window.ipc !== 'undefined' && window.ipc.postMessage;
6
+ console.log('[MSGER] IPC available:', hasIPC);
7
+
8
+ // Helper to safely send IPC messages
9
+ function safePostMessage(data) {
10
+ try {
11
+ if (!hasIPC) {
12
+ console.error('msger API: window.ipc not available');
13
+ return false;
14
+ }
15
+ window.ipc.postMessage(JSON.stringify(data));
16
+ return true;
17
+ } catch(e) {
18
+ console.error('msger API error:', e);
19
+ return false;
20
+ }
21
+ }
22
+
23
+ window.msger = {
24
+ // Window control - only close() with window.close() workaround (IPC broken)
25
+ close: function(result) {
26
+ // IPC is broken in wry 0.47.2, so we use window.close() instead
27
+ // Result cannot be returned to parent process
28
+ try {
29
+ window.close();
30
+ return true;
31
+ } catch(e) {
32
+ console.error('window.close() failed:', e);
33
+ return false;
34
+ }
35
+ },
36
+ saveData: function(key, value) {
37
+ try {
38
+ localStorage.setItem('msger_' + key, JSON.stringify(value));
39
+ return true;
40
+ } catch(e) {
41
+ console.error('Failed to save data:', e);
42
+ return false;
43
+ }
44
+ },
45
+ loadData: function(key, defaultValue) {
46
+ try {
47
+ const item = localStorage.getItem('msger_' + key);
48
+ return item ? JSON.parse(item) : defaultValue;
49
+ } catch(e) {
50
+ console.error('Failed to load data:', e);
51
+ return defaultValue;
52
+ }
53
+ },
54
+ removeData: function(key) {
55
+ try {
56
+ localStorage.removeItem('msger_' + key);
57
+ return true;
58
+ } catch(e) {
59
+ console.error('Failed to remove data:', e);
60
+ return false;
61
+ }
62
+ },
63
+ clearData: function() {
64
+ try {
65
+ const keys = Object.keys(localStorage);
66
+ for (const key of keys) {
67
+ if (key.startsWith('msger_')) {
68
+ localStorage.removeItem(key);
69
+ }
70
+ }
71
+ return true;
72
+ } catch(e) {
73
+ console.error('Failed to clear data:', e);
74
+ return false;
75
+ }
76
+ },
77
+ // Check if running in msger environment
78
+ isAvailable: function() {
79
+ return true;
80
+ }
81
+ };
82
+
83
+ console.log('[MSGER] API initialized successfully! window.msger =', window.msger);
84
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/msger",
3
- "version": "0.1.78",
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",