elit 3.5.6 → 3.5.7

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.
Files changed (113) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +1 -1
  3. package/desktop/build.rs +83 -0
  4. package/desktop/icon.rs +106 -0
  5. package/desktop/lib.rs +2 -0
  6. package/desktop/main.rs +235 -0
  7. package/desktop/native_main.rs +128 -0
  8. package/desktop/native_renderer/action_widgets.rs +184 -0
  9. package/desktop/native_renderer/app_models.rs +171 -0
  10. package/desktop/native_renderer/app_runtime.rs +140 -0
  11. package/desktop/native_renderer/container_rendering.rs +610 -0
  12. package/desktop/native_renderer/content_widgets.rs +634 -0
  13. package/desktop/native_renderer/css_models.rs +371 -0
  14. package/desktop/native_renderer/embedded_surfaces.rs +414 -0
  15. package/desktop/native_renderer/form_controls.rs +516 -0
  16. package/desktop/native_renderer/interaction_dispatch.rs +89 -0
  17. package/desktop/native_renderer/runtime_support.rs +135 -0
  18. package/desktop/native_renderer/utilities.rs +495 -0
  19. package/desktop/native_renderer/vector_drawing.rs +491 -0
  20. package/desktop/native_renderer.rs +4122 -0
  21. package/desktop/runtime/external.rs +422 -0
  22. package/desktop/runtime/mod.rs +67 -0
  23. package/desktop/runtime/quickjs.rs +106 -0
  24. package/desktop/window.rs +383 -0
  25. package/package.json +6 -3
  26. package/dist/build.d.mts +0 -20
  27. package/dist/chokidar.d.mts +0 -134
  28. package/dist/cli.d.mts +0 -81
  29. package/dist/config.d.mts +0 -254
  30. package/dist/coverage.d.mts +0 -85
  31. package/dist/database.d.mts +0 -52
  32. package/dist/desktop.d.mts +0 -68
  33. package/dist/dom.d.mts +0 -87
  34. package/dist/el.d.mts +0 -208
  35. package/dist/fs.d.mts +0 -255
  36. package/dist/hmr.d.mts +0 -38
  37. package/dist/http.d.mts +0 -169
  38. package/dist/https.d.mts +0 -108
  39. package/dist/index.d.mts +0 -13
  40. package/dist/mime-types.d.mts +0 -48
  41. package/dist/native.d.mts +0 -136
  42. package/dist/path.d.mts +0 -163
  43. package/dist/router.d.mts +0 -49
  44. package/dist/runtime.d.mts +0 -97
  45. package/dist/server-D0Dp4R5z.d.mts +0 -449
  46. package/dist/server.d.mts +0 -7
  47. package/dist/state.d.mts +0 -117
  48. package/dist/style.d.mts +0 -232
  49. package/dist/test-reporter.d.mts +0 -77
  50. package/dist/test-runtime.d.mts +0 -122
  51. package/dist/test.d.mts +0 -39
  52. package/dist/types.d.mts +0 -586
  53. package/dist/universal.d.mts +0 -21
  54. package/dist/ws.d.mts +0 -200
  55. package/dist/wss.d.mts +0 -108
  56. package/src/build.ts +0 -362
  57. package/src/chokidar.ts +0 -427
  58. package/src/cli.ts +0 -1162
  59. package/src/config.ts +0 -509
  60. package/src/coverage.ts +0 -1479
  61. package/src/database.ts +0 -1410
  62. package/src/desktop-auto-render.ts +0 -317
  63. package/src/desktop-cli.ts +0 -1533
  64. package/src/desktop.ts +0 -99
  65. package/src/dev-build.ts +0 -340
  66. package/src/dom.ts +0 -901
  67. package/src/el.ts +0 -183
  68. package/src/fs.ts +0 -609
  69. package/src/hmr.ts +0 -149
  70. package/src/http.ts +0 -856
  71. package/src/https.ts +0 -411
  72. package/src/index.ts +0 -16
  73. package/src/mime-types.ts +0 -222
  74. package/src/mobile-cli.ts +0 -2313
  75. package/src/native-background.ts +0 -444
  76. package/src/native-border.ts +0 -343
  77. package/src/native-canvas.ts +0 -260
  78. package/src/native-cli.ts +0 -414
  79. package/src/native-color.ts +0 -904
  80. package/src/native-estimation.ts +0 -194
  81. package/src/native-grid.ts +0 -590
  82. package/src/native-interaction.ts +0 -1289
  83. package/src/native-layout.ts +0 -568
  84. package/src/native-link.ts +0 -76
  85. package/src/native-render-support.ts +0 -361
  86. package/src/native-spacing.ts +0 -231
  87. package/src/native-state.ts +0 -318
  88. package/src/native-strings.ts +0 -46
  89. package/src/native-transform.ts +0 -120
  90. package/src/native-types.ts +0 -439
  91. package/src/native-typography.ts +0 -254
  92. package/src/native-units.ts +0 -441
  93. package/src/native-vector.ts +0 -910
  94. package/src/native.ts +0 -5606
  95. package/src/path.ts +0 -493
  96. package/src/pm-cli.ts +0 -2498
  97. package/src/preview-build.ts +0 -294
  98. package/src/render-context.ts +0 -138
  99. package/src/router.ts +0 -260
  100. package/src/runtime.ts +0 -97
  101. package/src/server.ts +0 -2294
  102. package/src/state.ts +0 -556
  103. package/src/style.ts +0 -1790
  104. package/src/test-globals.d.ts +0 -184
  105. package/src/test-reporter.ts +0 -609
  106. package/src/test-runtime.ts +0 -1359
  107. package/src/test.ts +0 -368
  108. package/src/types.ts +0 -381
  109. package/src/universal.ts +0 -81
  110. package/src/wapk-cli.ts +0 -3213
  111. package/src/workspace-package.ts +0 -102
  112. package/src/ws.ts +0 -648
  113. package/src/wss.ts +0 -241
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "elit-desktop"
3
- version = "3.5.6"
3
+ version = "3.5.7"
4
4
  edition = "2021"
5
5
  build = "desktop/build.rs"
6
6
 
package/README.md CHANGED
@@ -955,7 +955,7 @@ The package also exports `elit/test`, `elit/test-runtime`, and `elit/test-report
955
955
 
956
956
  Latest release notes live in [CHANGELOG.md](CHANGELOG.md).
957
957
 
958
- Highlights in `v3.5.6`:
958
+ Highlights in `v3.5.7`:
959
959
 
960
960
  - Added `elit pm` for detached background process management of shell commands, file targets, and WAPK apps.
961
961
  - Added `pm.apps[]` and `pm.dataDir` in `elit.config.*` for config-first process manager workflows.
@@ -0,0 +1,83 @@
1
+ #[cfg(windows)]
2
+ fn main() {
3
+ use std::env;
4
+ use std::fs::File;
5
+ use std::io::BufWriter;
6
+ use std::path::{Path, PathBuf};
7
+
8
+ #[path = "icon.rs"]
9
+ mod icon;
10
+
11
+ use image::ImageFormat;
12
+ use icon::load_icon_bitmap;
13
+
14
+ println!("cargo:rerun-if-env-changed=ELIT_DESKTOP_EXE_ICON");
15
+ println!("cargo:rerun-if-env-changed=WAPK_EXE_ICON");
16
+
17
+ let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string()));
18
+
19
+ let mut icon_path = env::var("ELIT_DESKTOP_EXE_ICON")
20
+ .ok()
21
+ .or_else(|| env::var("WAPK_EXE_ICON").ok())
22
+ .map(PathBuf::from);
23
+
24
+ if icon_path.is_none() {
25
+ let ico = manifest_dir.join("icon.ico");
26
+ let png = manifest_dir.join("icon.png");
27
+ let svg = manifest_dir.join("icon.svg");
28
+ if ico.exists() {
29
+ icon_path = Some(ico);
30
+ } else if png.exists() {
31
+ icon_path = Some(png);
32
+ } else if svg.exists() {
33
+ icon_path = Some(svg);
34
+ }
35
+ }
36
+
37
+ let Some(icon_path) = icon_path else {
38
+ return;
39
+ };
40
+
41
+ println!("cargo:rerun-if-changed={}", icon_path.display());
42
+
43
+ let resource_icon = match icon_path.extension().and_then(|s| s.to_str()).map(|s| s.to_ascii_lowercase()) {
44
+ Some(ext) if ext == "ico" => Some(icon_path.clone()),
45
+ Some(ext) if ext == "png" || ext == "svg" => {
46
+ let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap_or_else(|_| ".".to_string()));
47
+ let out_ico = out_dir.join("wapk-auto-icon.ico");
48
+ if let Err(err) = convert_icon_to_ico(&icon_path, &out_ico) {
49
+ println!("cargo:warning=Failed to convert icon to ICO: {err}");
50
+ None
51
+ } else {
52
+ Some(out_ico)
53
+ }
54
+ }
55
+ _ => {
56
+ println!(
57
+ "cargo:warning=Unsupported icon format for ELIT_DESKTOP_EXE_ICON: {}",
58
+ icon_path.display()
59
+ );
60
+ None
61
+ }
62
+ };
63
+
64
+ if let Some(icon_file) = resource_icon {
65
+ let mut res = winres::WindowsResource::new();
66
+ res.set_icon(icon_file.to_string_lossy().as_ref());
67
+ if let Err(err) = res.compile() {
68
+ println!("cargo:warning=Failed to embed exe icon resource: {err}");
69
+ }
70
+ }
71
+
72
+ fn convert_icon_to_ico(input_icon: &Path, output_ico: &Path) -> Result<(), String> {
73
+ let rgba = load_icon_bitmap(input_icon)?.into_dynamic_image()?;
74
+ let file = File::create(output_ico).map_err(|e| format!("create ICO failed: {e}"))?;
75
+ let mut writer = BufWriter::new(file);
76
+ rgba.write_to(&mut writer, ImageFormat::Ico)
77
+ .map_err(|e| format!("write ICO failed: {e}"))?;
78
+ Ok(())
79
+ }
80
+ }
81
+
82
+ #[cfg(not(windows))]
83
+ fn main() {}
@@ -0,0 +1,106 @@
1
+ use std::path::Path;
2
+
3
+ use image::{DynamicImage, RgbaImage};
4
+
5
+ const DEFAULT_ICON_SIDE: u32 = 256;
6
+
7
+ pub struct IconBitmap {
8
+ rgba: Vec<u8>,
9
+ width: u32,
10
+ height: u32,
11
+ }
12
+
13
+ impl IconBitmap {
14
+ #[allow(dead_code)]
15
+ pub fn into_rgba(self) -> (Vec<u8>, u32, u32) {
16
+ (self.rgba, self.width, self.height)
17
+ }
18
+
19
+ #[allow(dead_code)]
20
+ pub fn into_dynamic_image(self) -> Result<DynamicImage, String> {
21
+ let Some(image) = RgbaImage::from_raw(self.width, self.height, self.rgba) else {
22
+ return Err(String::from("icon decode produced an invalid RGBA buffer"));
23
+ };
24
+ Ok(DynamicImage::ImageRgba8(image))
25
+ }
26
+ }
27
+
28
+ pub fn load_icon_bitmap(path: &Path) -> Result<IconBitmap, String> {
29
+ match icon_extension(path).as_deref() {
30
+ Some("svg") => load_svg_icon_bitmap(path),
31
+ _ => load_raster_icon_bitmap(path),
32
+ }
33
+ }
34
+
35
+ fn icon_extension(path: &Path) -> Option<String> {
36
+ path.extension()
37
+ .and_then(|ext| ext.to_str())
38
+ .map(|ext| ext.to_ascii_lowercase())
39
+ }
40
+
41
+ fn load_raster_icon_bitmap(path: &Path) -> Result<IconBitmap, String> {
42
+ let image = image::open(path)
43
+ .map_err(|err| format!("decode icon failed for {}: {err}", path.display()))?;
44
+ bitmap_from_dynamic_image(image)
45
+ }
46
+
47
+ fn load_svg_icon_bitmap(path: &Path) -> Result<IconBitmap, String> {
48
+ use resvg::{tiny_skia, usvg};
49
+
50
+ let svg = std::fs::read(path)
51
+ .map_err(|err| format!("read SVG failed for {}: {err}", path.display()))?;
52
+ let options = usvg::Options::default();
53
+ let tree = usvg::Tree::from_data(&svg, &options)
54
+ .map_err(|err| format!("parse SVG failed for {}: {err}", path.display()))?;
55
+
56
+ let size = tree.size();
57
+ let width = size.width().ceil().max(1.0) as u32;
58
+ let height = size.height().ceil().max(1.0) as u32;
59
+ let side = width.max(height).max(DEFAULT_ICON_SIDE);
60
+
61
+ let mut pixmap = tiny_skia::Pixmap::new(side, side)
62
+ .ok_or_else(|| format!("allocate SVG pixmap failed for {}", path.display()))?;
63
+
64
+ let scale = (side as f32 / width as f32).min(side as f32 / height as f32);
65
+ let scaled_width = width as f32 * scale;
66
+ let scaled_height = height as f32 * scale;
67
+ let translate_x = ((side as f32 - scaled_width) / 2.0).max(0.0);
68
+ let translate_y = ((side as f32 - scaled_height) / 2.0).max(0.0);
69
+ let transform = tiny_skia::Transform::from_translate(translate_x, translate_y)
70
+ .post_scale(scale, scale);
71
+
72
+ resvg::render(&tree, transform, &mut pixmap.as_mut());
73
+
74
+ Ok(IconBitmap {
75
+ rgba: pixmap.take(),
76
+ width: side,
77
+ height: side,
78
+ })
79
+ }
80
+
81
+ fn bitmap_from_dynamic_image(image: DynamicImage) -> Result<IconBitmap, String> {
82
+ let squared = to_square_rgba(image);
83
+ let width = squared.width();
84
+ let height = squared.height();
85
+
86
+ Ok(IconBitmap {
87
+ rgba: squared.into_raw(),
88
+ width,
89
+ height,
90
+ })
91
+ }
92
+
93
+ fn to_square_rgba(image: DynamicImage) -> RgbaImage {
94
+ let rgba = image.to_rgba8();
95
+ let (width, height) = rgba.dimensions();
96
+ if width == height {
97
+ return rgba;
98
+ }
99
+
100
+ let side = width.max(height);
101
+ let mut square = RgbaImage::new(side, side);
102
+ let x = (side - width) / 2;
103
+ let y = (side - height) / 2;
104
+ image::imageops::overlay(&mut square, &rgba, i64::from(x), i64::from(y));
105
+ square
106
+ }
package/desktop/lib.rs ADDED
@@ -0,0 +1,2 @@
1
+ pub mod icon;
2
+ pub mod native_renderer;
@@ -0,0 +1,235 @@
1
+ #![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
2
+
3
+ mod runtime;
4
+ mod icon;
5
+ mod window;
6
+
7
+ use std::env;
8
+
9
+ const EMBED_MAGIC_V1: &[u8; 8] = b"WAPKJS\x00\x01";
10
+ const EMBED_MAGIC_V2: &[u8; 8] = b"WAPKRT\x00\x02";
11
+
12
+ struct EmbeddedApp {
13
+ runtime: String,
14
+ code: String,
15
+ }
16
+
17
+ fn embedded_runtime_name(code: u8) -> Option<&'static str> {
18
+ match code {
19
+ 1 => Some("quickjs"),
20
+ 2 => Some("bun"),
21
+ 3 => Some("node"),
22
+ 4 => Some("deno"),
23
+ _ => None,
24
+ }
25
+ }
26
+
27
+ fn read_embedded_js(
28
+ file: &mut std::fs::File,
29
+ file_size: u64,
30
+ js_size: usize,
31
+ trailer_size: usize,
32
+ ) -> Option<String> {
33
+ use std::io::{Read, Seek, SeekFrom};
34
+
35
+ let js_offset = (file_size as usize).checked_sub(trailer_size + js_size)?;
36
+ file.seek(SeekFrom::Start(js_offset as u64)).ok()?;
37
+
38
+ let mut js_bytes = vec![0u8; js_size];
39
+ file.read_exact(&mut js_bytes).ok()?;
40
+ String::from_utf8(js_bytes).ok()
41
+ }
42
+
43
+ /// Read JS embedded in this binary (appended after the PE).
44
+ /// V1 layout: [exe bytes][js bytes][js_size: u64 LE][EMBED_MAGIC_V1: 8 bytes]
45
+ /// V2 layout: [exe bytes][js bytes][js_size: u64 LE][runtime_code: u8][EMBED_MAGIC_V2: 8 bytes]
46
+ fn try_embedded_app() -> Option<EmbeddedApp> {
47
+ use std::io::{Read, Seek, SeekFrom};
48
+
49
+ let exe_path = std::env::current_exe().ok()?;
50
+ let mut f = std::fs::File::open(&exe_path).ok()?;
51
+ let file_size = f.metadata().ok()?.len();
52
+
53
+ if file_size >= 17 {
54
+ f.seek(SeekFrom::End(-17)).ok()?;
55
+ let mut trailer = [0u8; 17];
56
+ f.read_exact(&mut trailer).ok()?;
57
+
58
+ if &trailer[9..17] == EMBED_MAGIC_V2 {
59
+ let js_size = u64::from_le_bytes(trailer[0..8].try_into().ok()?) as usize;
60
+ let runtime = embedded_runtime_name(trailer[8])?.to_string();
61
+ let code = read_embedded_js(&mut f, file_size, js_size, 17)?;
62
+ return Some(EmbeddedApp { runtime, code });
63
+ }
64
+ }
65
+
66
+ if file_size >= 16 {
67
+ f.seek(SeekFrom::End(-16)).ok()?;
68
+ let mut trailer = [0u8; 16];
69
+ f.read_exact(&mut trailer).ok()?;
70
+
71
+ if &trailer[8..16] == EMBED_MAGIC_V1 {
72
+ let js_size = u64::from_le_bytes(trailer[0..8].try_into().ok()?) as usize;
73
+ let code = read_embedded_js(&mut f, file_size, js_size, 16)?;
74
+ return Some(EmbeddedApp {
75
+ runtime: String::from("quickjs"),
76
+ code,
77
+ });
78
+ }
79
+ }
80
+
81
+ None
82
+ }
83
+
84
+ fn print_help() {
85
+ eprintln!(
86
+ r#"elit-desktop — native WebView runtime for Elit
87
+
88
+ Usage:
89
+ elit-desktop [OPTIONS] <script.js>
90
+
91
+ Options:
92
+ --runtime <name> JS runtime to use (default: quickjs)
93
+ Choices: quickjs | bun | node | deno
94
+ --help Show this help
95
+
96
+ Examples:
97
+ elit-desktop app.js
98
+ elit-desktop --runtime bun app.js
99
+ elit-desktop --runtime node app.js
100
+ "#
101
+ );
102
+ }
103
+
104
+ /// Look for `<exe-stem>.js` next to the running binary (enables standalone app.exe).
105
+ fn auto_detect_script() -> Option<String> {
106
+ let exe = std::env::current_exe().ok()?;
107
+ let stem = exe.file_stem()?.to_str()?.to_string();
108
+ let dir = exe.parent()?;
109
+ let candidate = dir.join(format!("{}.js", stem));
110
+ if candidate.exists() {
111
+ Some(candidate.to_string_lossy().into_owned())
112
+ } else {
113
+ None
114
+ }
115
+ }
116
+
117
+ fn parse_args() -> (String, String) {
118
+ let args: Vec<String> = env::args().skip(1).collect();
119
+
120
+ // No args → try auto-detect bundled script, otherwise show help
121
+ if args.is_empty() {
122
+ if let Some(script) = auto_detect_script() {
123
+ return (String::from("quickjs"), script);
124
+ }
125
+ print_help();
126
+ std::process::exit(0);
127
+ }
128
+
129
+ if args.iter().any(|a| a == "--help" || a == "-h") {
130
+ print_help();
131
+ std::process::exit(0);
132
+ }
133
+
134
+ let mut runtime = String::from("quickjs");
135
+ let mut script = String::new();
136
+ let mut i = 0;
137
+
138
+ while i < args.len() {
139
+ match args[i].as_str() {
140
+ "--runtime" | "-r" => {
141
+ i += 1;
142
+ if i < args.len() {
143
+ runtime = args[i].clone();
144
+ }
145
+ }
146
+ arg if !arg.starts_with('-') => {
147
+ script = arg.to_string();
148
+ }
149
+ _ => {}
150
+ }
151
+ i += 1;
152
+ }
153
+
154
+ if script.is_empty() {
155
+ if let Some(auto) = auto_detect_script() {
156
+ return (runtime, auto);
157
+ }
158
+ eprintln!("Error: no script file specified.");
159
+ print_help();
160
+ std::process::exit(1);
161
+ }
162
+
163
+ (runtime, script)
164
+ }
165
+
166
+ fn main() {
167
+ // ── Embedded JS (single-file distribution) ──────────────────────────────
168
+ if let Some(EmbeddedApp { runtime, code }) = try_embedded_app() {
169
+ match runtime.as_str() {
170
+ #[cfg(feature = "runtime-quickjs")]
171
+ "quickjs" => {
172
+ window::run(move |proxy, ipc_rx| {
173
+ runtime::quickjs::run(&code, proxy, ipc_rx);
174
+ });
175
+ }
176
+
177
+ #[cfg(feature = "runtime-external")]
178
+ rt @ ("bun" | "node" | "nodejs" | "deno") => {
179
+ let name = rt.to_string();
180
+ window::run(move |proxy, ipc_rx| {
181
+ runtime::external::run_embedded(&name, &code, proxy, ipc_rx);
182
+ });
183
+ }
184
+
185
+ other => {
186
+ eprintln!(
187
+ "Error: embedded runtime '{}' is not available in this build.",
188
+ other
189
+ );
190
+ std::process::exit(1);
191
+ }
192
+ }
193
+ return;
194
+ }
195
+
196
+ let (runtime_name, script_path) = parse_args();
197
+
198
+ // Validate file exists
199
+ if !std::path::Path::new(&script_path).exists() {
200
+ eprintln!("Error: '{}' not found.", script_path);
201
+ std::process::exit(1);
202
+ }
203
+
204
+ match runtime_name.as_str() {
205
+ // ── Embedded QuickJS (default) ──────────────────────────────────────
206
+ #[cfg(feature = "runtime-quickjs")]
207
+ "quickjs" => {
208
+ let code = std::fs::read_to_string(&script_path)
209
+ .unwrap_or_else(|_| panic!("Cannot read '{}'", script_path));
210
+
211
+ window::run(move |proxy, ipc_rx| {
212
+ runtime::quickjs::run(&code, proxy, ipc_rx);
213
+ });
214
+ }
215
+
216
+ // ── External runtime (Bun / Node / Deno) ────────────────────────────
217
+ #[cfg(feature = "runtime-external")]
218
+ rt @ ("bun" | "node" | "nodejs" | "deno") => {
219
+ let path = script_path.clone();
220
+ let name = rt.to_string();
221
+
222
+ window::run(move |proxy, ipc_rx| {
223
+ runtime::external::run(&name, &path, proxy, ipc_rx);
224
+ });
225
+ }
226
+
227
+ other => {
228
+ eprintln!(
229
+ "Error: unknown runtime '{}'.\nAvailable: quickjs, bun, node, deno",
230
+ other
231
+ );
232
+ std::process::exit(1);
233
+ }
234
+ }
235
+ }
@@ -0,0 +1,128 @@
1
+ #![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
2
+
3
+ use std::env;
4
+
5
+ use elit_desktop::native_renderer;
6
+
7
+ const EMBED_NATIVE_MAGIC_V1: &[u8; 8] = b"ELITNUI1";
8
+
9
+ fn read_embedded_payload(
10
+ file: &mut std::fs::File,
11
+ file_size: u64,
12
+ payload_size: usize,
13
+ trailer_size: usize,
14
+ ) -> Option<String> {
15
+ use std::io::{Read, Seek, SeekFrom};
16
+
17
+ let payload_offset = (file_size as usize).checked_sub(trailer_size + payload_size)?;
18
+ file.seek(SeekFrom::Start(payload_offset as u64)).ok()?;
19
+
20
+ let mut payload_bytes = vec![0u8; payload_size];
21
+ file.read_exact(&mut payload_bytes).ok()?;
22
+ String::from_utf8(payload_bytes).ok()
23
+ }
24
+
25
+ fn try_embedded_payload() -> Option<String> {
26
+ use std::io::{Read, Seek, SeekFrom};
27
+
28
+ let exe_path = std::env::current_exe().ok()?;
29
+ let mut file = std::fs::File::open(&exe_path).ok()?;
30
+ let file_size = file.metadata().ok()?.len();
31
+
32
+ if file_size < 16 {
33
+ return None;
34
+ }
35
+
36
+ file.seek(SeekFrom::End(-16)).ok()?;
37
+ let mut trailer = [0u8; 16];
38
+ file.read_exact(&mut trailer).ok()?;
39
+
40
+ if &trailer[8..16] != EMBED_NATIVE_MAGIC_V1 {
41
+ return None;
42
+ }
43
+
44
+ let payload_size = u64::from_le_bytes(trailer[0..8].try_into().ok()?) as usize;
45
+ read_embedded_payload(&mut file, file_size, payload_size, 16)
46
+ }
47
+
48
+ fn print_help() {
49
+ eprintln!(
50
+ r#"elit-desktop-native — native UI runtime for Elit
51
+
52
+ Usage:
53
+ elit-desktop-native [payload.json]
54
+
55
+ Options:
56
+ --help Show this help
57
+
58
+ Examples:
59
+ elit-desktop-native app.json
60
+ "#
61
+ );
62
+ }
63
+
64
+ fn auto_detect_payload() -> Option<String> {
65
+ let exe = std::env::current_exe().ok()?;
66
+ let stem = exe.file_stem()?.to_str()?.to_string();
67
+ let dir = exe.parent()?;
68
+ let candidate = dir.join(format!("{}.json", stem));
69
+ if candidate.exists() {
70
+ Some(candidate.to_string_lossy().into_owned())
71
+ } else {
72
+ None
73
+ }
74
+ }
75
+
76
+ fn parse_payload_path() -> String {
77
+ let args: Vec<String> = env::args().skip(1).collect();
78
+
79
+ if args.iter().any(|arg| arg == "--help" || arg == "-h") {
80
+ print_help();
81
+ std::process::exit(0);
82
+ }
83
+
84
+ if args.is_empty() {
85
+ if let Some(payload) = auto_detect_payload() {
86
+ return payload;
87
+ }
88
+
89
+ print_help();
90
+ std::process::exit(0);
91
+ }
92
+
93
+ let payload_path = args
94
+ .iter()
95
+ .find(|arg| !arg.starts_with('-'))
96
+ .cloned();
97
+
98
+ if let Some(payload_path) = payload_path {
99
+ return payload_path;
100
+ }
101
+
102
+ if let Some(payload) = auto_detect_payload() {
103
+ return payload;
104
+ }
105
+
106
+ eprintln!("Error: no payload file specified.");
107
+ print_help();
108
+ std::process::exit(1);
109
+ }
110
+
111
+ fn main() {
112
+ let payload_json = if let Some(embedded_payload) = try_embedded_payload() {
113
+ embedded_payload
114
+ } else {
115
+ let payload_path = parse_payload_path();
116
+ std::fs::read_to_string(&payload_path).unwrap_or_else(|error| {
117
+ eprintln!("Error: failed to read native desktop payload '{}': {}", payload_path, error);
118
+ std::process::exit(1);
119
+ })
120
+ };
121
+
122
+ let payload: native_renderer::NativeDesktopPayload = serde_json::from_str(&payload_json).unwrap_or_else(|error| {
123
+ eprintln!("Error: failed to parse native desktop payload: {}", error);
124
+ std::process::exit(1);
125
+ });
126
+
127
+ native_renderer::run(payload);
128
+ }