@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 +9 -84
- package/TODO.md +14 -0
- package/msger-native/Cargo.toml +4 -0
- package/msger-native/README.md +84 -5
- package/msger-native/bin/msgernative.exe +0 -0
- package/msger-native/example-config.json +20 -0
- package/msger-native/src/main.rs +336 -91
- package/msger-native/src/msger-api.js +84 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -512,98 +512,23 @@ Notes:
|
|
|
512
512
|
- Runtime zoom: Ctrl+Wheel, Ctrl+Plus, Ctrl+Minus
|
|
513
513
|
```
|
|
514
514
|
|
|
515
|
-
##
|
|
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
|
-
###
|
|
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
|
-
###
|
|
521
|
+
### Technical Details & Known Issues
|
|
589
522
|
|
|
590
|
-
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
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
|
package/msger-native/Cargo.toml
CHANGED
|
@@ -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"] }
|
package/msger-native/README.md
CHANGED
|
@@ -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 (
|
|
52
|
+
### MessageBoxOptions (Complete Structure)
|
|
29
53
|
```json
|
|
30
54
|
{
|
|
31
|
-
"title": "
|
|
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
|
-
"
|
|
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
|
+
}
|
package/msger-native/src/main.rs
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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('\'', "'")
|
|
281
341
|
}
|
|
282
342
|
|
|
283
|
-
fn
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
+
})();
|