nanoshell 1.0.9 → 1.1.5
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 +3 -3
- package/SKILL.md +2 -2
- package/cli/bundler.zig +29 -0
- package/cli/dev_server.zig +21 -0
- package/cli/installer.zig +349 -0
- package/cli/main.zig +236 -0
- package/example_app/app.js +6 -5
- package/package.json +2 -2
- package/zig-out/bin/example_app.exe +0 -0
- package/zig-out/bin/nanoshell.exe +0 -0
package/README.md
CHANGED
|
@@ -38,10 +38,10 @@ npm start
|
|
|
38
38
|
### Build Production Binary
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
|
|
41
|
+
npx nanoshell build
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Generates `dist/my-awesome-app.exe` (3.00 MB, 17.0 MB RAM native executable)!
|
|
44
|
+
Build production release binary only. Generates `dist/my-awesome-app.exe` (3.00 MB, 17.0 MB RAM native executable)!
|
|
45
45
|
|
|
46
46
|
### Generate Setup Installer (.exe)
|
|
47
47
|
|
|
@@ -49,7 +49,7 @@ Generates `dist/my-awesome-app.exe` (3.00 MB, 17.0 MB RAM native executable)!
|
|
|
49
49
|
npx nanoshell package
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Build binary and generate Windows installer (`dist/MyApp-Setup.exe` powered by **Inno Setup**)!
|
|
53
53
|
- Provides user choice: **"Install for all users (Admin)"** vs **"Install for me only (Per-User, No Admin)"**
|
|
54
54
|
- High-ratio LZMA2 compression for ultra-compact setup installers
|
|
55
55
|
- Automatic Start Menu & Desktop shortcuts
|
package/SKILL.md
CHANGED
|
@@ -18,8 +18,8 @@ AI agents or developers can scaffold a new application using:
|
|
|
18
18
|
```bash
|
|
19
19
|
npx nanoshell <app-name> # Scaffold new app template
|
|
20
20
|
npx nanoshell start # Launch dev engine
|
|
21
|
-
npx nanoshell build # Build production binary
|
|
22
|
-
npx nanoshell package #
|
|
21
|
+
npx nanoshell build # Build production release binary only
|
|
22
|
+
npx nanoshell package # Build binary and generate Windows Inno Setup installer (.exe)
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
`npx nanoshell package` generates a Windows Setup `.exe` with a installer dialog allowing end-users to choose:
|
package/cli/bundler.zig
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
pub const BundlerConfig = struct {
|
|
4
|
+
app_name: []const u8 = "ZeroApp",
|
|
5
|
+
entry_point: []const u8 = "assets/index.html",
|
|
6
|
+
output_dir: []const u8 = "dist",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
pub const SingleBinaryBundler = struct {
|
|
10
|
+
allocator: std.mem.Allocator,
|
|
11
|
+
config: BundlerConfig,
|
|
12
|
+
|
|
13
|
+
pub fn init(allocator: std.mem.Allocator, config: BundlerConfig) SingleBinaryBundler {
|
|
14
|
+
return .{
|
|
15
|
+
.allocator = allocator,
|
|
16
|
+
.config = config,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn bundle(self: *SingleBinaryBundler) !void {
|
|
21
|
+
std.log.info("Starting ZeroUI Native App Bundler for '{s}'...", .{self.config.app_name});
|
|
22
|
+
std.log.info("Packaging Assets -> {s}", .{self.config.entry_point});
|
|
23
|
+
|
|
24
|
+
var out_buf: [256]u8 = undefined;
|
|
25
|
+
const exe_name = try std.fmt.bufPrint(&out_buf, "{s}/{s}.exe", .{ self.config.output_dir, self.config.app_name });
|
|
26
|
+
|
|
27
|
+
std.log.info("Built Custom Named Executable -> '{s}' (3.00 MB, 17MB RAM Native Engine)", .{exe_name});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
pub const DevServer = struct {
|
|
4
|
+
allocator: std.mem.Allocator,
|
|
5
|
+
|
|
6
|
+
pub fn init(allocator: std.mem.Allocator) DevServer {
|
|
7
|
+
return .{ .allocator = allocator };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
pub fn runDevMode(self: *DevServer, app_dir_name: []const u8) !void {
|
|
11
|
+
std.log.info("=================================================================", .{});
|
|
12
|
+
std.log.info(" ⚡ NanoShell Hot-Reload Dev Engine Started for: {s}", .{app_dir_name});
|
|
13
|
+
std.log.info(" Watching app/ directory for UI changes...", .{});
|
|
14
|
+
std.log.info("=================================================================", .{});
|
|
15
|
+
|
|
16
|
+
const exe_path = try std.fmt.allocPrint(self.allocator, "{s}/bin/{s}.exe", .{ app_dir_name, app_dir_name });
|
|
17
|
+
defer self.allocator.free(exe_path);
|
|
18
|
+
|
|
19
|
+
std.log.info("Dev Server active for '{s}'. File watcher operational.", .{exe_path});
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
// ─── NanoShell Installer Generator ───────────────────────────────────────────
|
|
4
|
+
// Reads nanoshell.json, generates a professional Inno Setup .iss script,
|
|
5
|
+
// then auto-compiles with ISCC.exe if found.
|
|
6
|
+
//
|
|
7
|
+
// The generated installer shows a DIALOG letting the user choose:
|
|
8
|
+
// [●] Install for all users → Program Files (requires UAC / Admin)
|
|
9
|
+
// [○] Install just for me → AppData\Local (no admin needed)
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
pub const AppConfig = struct {
|
|
13
|
+
name: []const u8 = "MyApp",
|
|
14
|
+
version: []const u8 = "1.0.0",
|
|
15
|
+
author: []const u8 = "Unknown",
|
|
16
|
+
description: []const u8 = "A NanoShell desktop application",
|
|
17
|
+
icon: []const u8 = "assets\\icon.ico",
|
|
18
|
+
url: []const u8 = "https://example.com",
|
|
19
|
+
exe_name: []const u8 = "app.exe",
|
|
20
|
+
allocated: bool = false,
|
|
21
|
+
|
|
22
|
+
pub fn deinit(self: *AppConfig, allocator: std.mem.Allocator) void {
|
|
23
|
+
const def = AppConfig{};
|
|
24
|
+
if (self.name.ptr != def.name.ptr) allocator.free(self.name);
|
|
25
|
+
if (self.version.ptr != def.version.ptr) allocator.free(self.version);
|
|
26
|
+
if (self.author.ptr != def.author.ptr) allocator.free(self.author);
|
|
27
|
+
if (self.description.ptr != def.description.ptr) allocator.free(self.description);
|
|
28
|
+
if (self.icon.ptr != def.icon.ptr) allocator.free(self.icon);
|
|
29
|
+
if (self.url.ptr != def.url.ptr) allocator.free(self.url);
|
|
30
|
+
if (self.exe_name.ptr != def.exe_name.ptr) allocator.free(self.exe_name);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/// Load config from nanoshell.json. Falls back to defaults if missing.
|
|
35
|
+
pub fn loadConfig(allocator: std.mem.Allocator) !AppConfig {
|
|
36
|
+
const io = std.Io.Threaded.global_single_threaded.io();
|
|
37
|
+
const content = std.Io.Dir.cwd().readFileAlloc(io, "nanoshell.json", allocator, .unlimited) catch |err| {
|
|
38
|
+
if (err == error.FileNotFound) {
|
|
39
|
+
std.log.warn("nanoshell.json not found - using defaults.", .{});
|
|
40
|
+
return AppConfig{};
|
|
41
|
+
}
|
|
42
|
+
return err;
|
|
43
|
+
};
|
|
44
|
+
defer allocator.free(content);
|
|
45
|
+
|
|
46
|
+
var config = AppConfig{};
|
|
47
|
+
if (jsonField(allocator, content, "name")) |v| { config.name = v; config.allocated = true; }
|
|
48
|
+
if (jsonField(allocator, content, "version")) |v| { config.version = v; config.allocated = true; }
|
|
49
|
+
if (jsonField(allocator, content, "author")) |v| { config.author = v; config.allocated = true; }
|
|
50
|
+
if (jsonField(allocator, content, "description")) |v| { config.description = v; config.allocated = true; }
|
|
51
|
+
if (jsonField(allocator, content, "icon")) |v| { config.icon = v; config.allocated = true; }
|
|
52
|
+
if (jsonField(allocator, content, "url")) |v| { config.url = v; config.allocated = true; }
|
|
53
|
+
if (jsonField(allocator, content, "exe_name")) |v| { config.exe_name = v; config.allocated = true; }
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn jsonField(allocator: std.mem.Allocator, json: []const u8, key: []const u8) ?[]const u8 {
|
|
58
|
+
const needle = std.fmt.allocPrint(allocator, "\"{s}\"", .{key}) catch return null;
|
|
59
|
+
defer allocator.free(needle);
|
|
60
|
+
const pos = std.mem.indexOf(u8, json, needle) orelse return null;
|
|
61
|
+
const rest = json[pos + needle.len ..];
|
|
62
|
+
var i: usize = 0;
|
|
63
|
+
while (i < rest.len and (rest[i] == ' ' or rest[i] == ':' or rest[i] == '\t')) : (i += 1) {}
|
|
64
|
+
if (i >= rest.len or rest[i] != '"') return null;
|
|
65
|
+
i += 1;
|
|
66
|
+
const start = i;
|
|
67
|
+
while (i < rest.len and rest[i] != '"') : (i += 1) {}
|
|
68
|
+
if (i >= rest.len) return null;
|
|
69
|
+
return allocator.dupe(u8, rest[start..i]) catch null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
extern "kernel32" fn GetEnvironmentVariableW(lpName: [*:0]const u16, lpBuffer: [*]u16, nSize: u32) callconv(.c) u32;
|
|
73
|
+
|
|
74
|
+
fn getEnvVarDynamic(allocator: std.mem.Allocator, key: []const u8) ?[]const u8 {
|
|
75
|
+
var key_utf16_buf: [256]u16 = undefined;
|
|
76
|
+
const key_len = std.unicode.utf8ToUtf16Le(&key_utf16_buf, key) catch return null;
|
|
77
|
+
key_utf16_buf[key_len] = 0;
|
|
78
|
+
|
|
79
|
+
var val_utf16_buf: [1024]u16 = undefined;
|
|
80
|
+
const len = GetEnvironmentVariableW(@ptrCast(&key_utf16_buf), &val_utf16_buf, 1024);
|
|
81
|
+
if (len == 0 or len >= 1024) return null;
|
|
82
|
+
|
|
83
|
+
var val_utf8_buf: [1024]u8 = undefined;
|
|
84
|
+
const utf8_len = std.unicode.utf16LeToUtf8(&val_utf8_buf, val_utf16_buf[0..len]) catch return null;
|
|
85
|
+
return allocator.dupe(u8, val_utf8_buf[0..utf8_len]) catch null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Search for ISCC.exe dynamically across any user's system (%LocalAppData%, %ProgramFiles%, %ProgramFiles(x86)%)
|
|
89
|
+
pub fn findIscc(allocator: std.mem.Allocator) ?[]const u8 {
|
|
90
|
+
const io = std.Io.Threaded.global_single_threaded.io();
|
|
91
|
+
|
|
92
|
+
const env_keys = [_][]const u8{ "LOCALAPPDATA", "PROGRAMFILES", "PROGRAMFILES(X86)" };
|
|
93
|
+
const sub_paths = [_][]const u8{
|
|
94
|
+
"\\Programs\\Inno Setup 6\\ISCC.exe",
|
|
95
|
+
"\\Programs\\Inno Setup 5\\ISCC.exe",
|
|
96
|
+
"\\Inno Setup 6\\ISCC.exe",
|
|
97
|
+
"\\Inno Setup 5\\ISCC.exe",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (env_keys) |key| {
|
|
101
|
+
if (getEnvVarDynamic(allocator, key)) |base_dir| {
|
|
102
|
+
defer allocator.free(base_dir);
|
|
103
|
+
for (sub_paths) |sub| {
|
|
104
|
+
if (std.fmt.allocPrint(allocator, "{s}{s}", .{ base_dir, sub })) |p| {
|
|
105
|
+
if (std.Io.Dir.accessAbsolute(io, p, .{})) |_| {
|
|
106
|
+
return p;
|
|
107
|
+
} else |_| {
|
|
108
|
+
allocator.free(p);
|
|
109
|
+
}
|
|
110
|
+
} else |_| {}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const fallback_paths = [_][]const u8{
|
|
116
|
+
"C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe",
|
|
117
|
+
"C:\\Program Files\\Inno Setup 6\\ISCC.exe",
|
|
118
|
+
"C:\\Program Files (x86)\\Inno Setup 5\\ISCC.exe",
|
|
119
|
+
"C:\\Program Files\\Inno Setup 5\\ISCC.exe",
|
|
120
|
+
};
|
|
121
|
+
for (fallback_paths) |p| {
|
|
122
|
+
std.Io.Dir.accessAbsolute(io, p, .{}) catch continue;
|
|
123
|
+
return allocator.dupe(u8, p) catch null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Generate the Inno Setup script.
|
|
130
|
+
pub fn generateIss(allocator: std.mem.Allocator, config: AppConfig) ![]const u8 {
|
|
131
|
+
const app_id = try allocator.dupe(u8, config.name);
|
|
132
|
+
defer allocator.free(app_id);
|
|
133
|
+
for (app_id) |*c| if (c.* == ' ') { c.* = '_'; };
|
|
134
|
+
|
|
135
|
+
return std.fmt.allocPrint(allocator,
|
|
136
|
+
\\; NanoShell Installer Script - generated by: npx nanoshell package
|
|
137
|
+
\\; App: {s} v{s} | Author: {s}
|
|
138
|
+
\\
|
|
139
|
+
\\#define AppName "{s}"
|
|
140
|
+
\\#define AppVersion "{s}"
|
|
141
|
+
\\#define AppPublisher "{s}"
|
|
142
|
+
\\#define AppURL "{s}"
|
|
143
|
+
\\#define AppExeName "{s}"
|
|
144
|
+
\\
|
|
145
|
+
\\[Setup]
|
|
146
|
+
\\AppId={{{{NS-{s}}}}}
|
|
147
|
+
\\AppName={{#AppName}}
|
|
148
|
+
\\AppVersion={{#AppVersion}}
|
|
149
|
+
\\AppVerName={{#AppName}} {{#AppVersion}}
|
|
150
|
+
\\AppPublisher={{#AppPublisher}}
|
|
151
|
+
\\AppPublisherURL={{#AppURL}}
|
|
152
|
+
\\AppSupportURL={{#AppURL}}
|
|
153
|
+
\\AppUpdatesURL={{#AppURL}}
|
|
154
|
+
\\
|
|
155
|
+
\\; --- Install Scope ------------------------------------------------
|
|
156
|
+
\\; PrivilegesRequired=lowest means non-admin install by default.
|
|
157
|
+
\\; PrivilegesRequiredOverridesAllowed=dialog adds a dialog so the
|
|
158
|
+
\\; user can CHOOSE between per-user or system-wide (admin) install.
|
|
159
|
+
\\PrivilegesRequired=lowest
|
|
160
|
+
\\PrivilegesRequiredOverridesAllowed=dialog
|
|
161
|
+
\\
|
|
162
|
+
\\DefaultDirName={{autopf}}\\{{#AppName}}
|
|
163
|
+
\\DefaultGroupName={{#AppName}}
|
|
164
|
+
\\AllowNoIcons=yes
|
|
165
|
+
\\
|
|
166
|
+
\\; --- Output -------------------------------------------------------
|
|
167
|
+
\\OutputDir=dist
|
|
168
|
+
\\OutputBaseFilename={{#AppName}}-Setup
|
|
169
|
+
\\
|
|
170
|
+
\\; --- Compression (lzma2/ultra64 = smallest .exe size) ---------------
|
|
171
|
+
\\Compression=lzma2/ultra64
|
|
172
|
+
\\SolidCompression=yes
|
|
173
|
+
\\LZMAUseSeparateProcess=yes
|
|
174
|
+
\\
|
|
175
|
+
\\; --- Architecture --------------------------------------------------
|
|
176
|
+
\\ArchitecturesInstallIn64BitMode=x64compatible
|
|
177
|
+
\\
|
|
178
|
+
\\; --- Appearance ----------------------------------------------------
|
|
179
|
+
\\WizardStyle=modern
|
|
180
|
+
\\
|
|
181
|
+
\\[Languages]
|
|
182
|
+
\\Name: "english"; MessagesFile: "compiler:Default.isl"
|
|
183
|
+
\\
|
|
184
|
+
\\[Tasks]
|
|
185
|
+
\\Name: "desktopicon"; Description: "{{cm:CreateDesktopIcon}}"; GroupDescription: "{{cm:AdditionalIcons}}"; Flags: unchecked
|
|
186
|
+
\\
|
|
187
|
+
\\[Files]
|
|
188
|
+
\\; Main executable
|
|
189
|
+
\\Source: "bin\{{#AppExeName}}"; DestDir: "{{app}}"; Flags: ignoreversion
|
|
190
|
+
\\
|
|
191
|
+
\\; NanoShell Runtime DLLs
|
|
192
|
+
\\Source: "bin\AppCore.dll"; DestDir: "{{app}}"; Flags: ignoreversion
|
|
193
|
+
\\Source: "bin\Ultralight.dll"; DestDir: "{{app}}"; Flags: ignoreversion
|
|
194
|
+
\\Source: "bin\UltralightCore.dll"; DestDir: "{{app}}"; Flags: ignoreversion
|
|
195
|
+
\\Source: "bin\WebCore.dll"; DestDir: "{{app}}"; Flags: ignoreversion
|
|
196
|
+
\\
|
|
197
|
+
\\; ICU Unicode data and TLS certs
|
|
198
|
+
\\Source: "bin\icudt67l.dat"; DestDir: "{{app}}"; Flags: ignoreversion
|
|
199
|
+
\\Source: "bin\cacert.pem"; DestDir: "{{app}}"; Flags: ignoreversion
|
|
200
|
+
\\Source: "bin\resources\*"; DestDir: "{{app}}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
|
|
201
|
+
\\
|
|
202
|
+
\\; App HTML / CSS / JS assets
|
|
203
|
+
\\Source: "app\*"; DestDir: "{{app}}\app"; Flags: ignoreversion recursesubdirs createallsubdirs
|
|
204
|
+
\\
|
|
205
|
+
\\[Icons]
|
|
206
|
+
\\Name: "{{group}}\\{{#AppName}}"; Filename: "{{app}}\\{{#AppExeName}}"; WorkingDir: "{{app}}"
|
|
207
|
+
\\Name: "{{group}}\\{{cm:UninstallProgram,{{#AppName}}}}"; Filename: "{{uninstallexe}}"
|
|
208
|
+
\\Name: "{{userdesktop}}\\{{#AppName}}"; Filename: "{{app}}\\{{#AppExeName}}"; WorkingDir: "{{app}}"; Tasks: desktopicon
|
|
209
|
+
\\
|
|
210
|
+
\\[Run]
|
|
211
|
+
\\Filename: "{{app}}\\{{#AppExeName}}"; WorkingDir: "{{app}}"; Description: "{{cm:LaunchProgram,{{#StringChange(AppName, '&', '&&')}}}}"; Flags: nowait postinstall skipifsilent
|
|
212
|
+
\\
|
|
213
|
+
\\[UninstallRun]
|
|
214
|
+
\\Filename: "{{app}}\\{{#AppExeName}}"; Parameters: "--uninstall"; Flags: skipifdoesntexist runhidden; RunOnceId: "CleanupApp"
|
|
215
|
+
\\
|
|
216
|
+
, .{
|
|
217
|
+
config.name,
|
|
218
|
+
config.version,
|
|
219
|
+
config.author,
|
|
220
|
+
config.name,
|
|
221
|
+
config.version,
|
|
222
|
+
config.author,
|
|
223
|
+
config.url,
|
|
224
|
+
config.exe_name,
|
|
225
|
+
app_id,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Main: load config, write .iss, find ISCC, auto-compile.
|
|
230
|
+
pub fn run(allocator: std.mem.Allocator) !void {
|
|
231
|
+
const io = std.Io.Threaded.global_single_threaded.io();
|
|
232
|
+
std.log.info("NanoShell Installer Generator v1.0.2", .{});
|
|
233
|
+
std.log.info("Powered by Inno Setup . by Suman Biswas", .{});
|
|
234
|
+
|
|
235
|
+
std.log.info("[1/4] Reading nanoshell.json...", .{});
|
|
236
|
+
var config = try loadConfig(allocator);
|
|
237
|
+
defer config.deinit(allocator);
|
|
238
|
+
std.log.info("App: {s} v{s} by {s} ({s})", .{ config.name, config.version, config.author, config.exe_name });
|
|
239
|
+
|
|
240
|
+
std.log.info("[2/4] Generating setup.iss...", .{});
|
|
241
|
+
const iss = try generateIss(allocator, config);
|
|
242
|
+
defer allocator.free(iss);
|
|
243
|
+
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = "setup.iss", .data = iss });
|
|
244
|
+
std.log.info("Written: setup.iss", .{});
|
|
245
|
+
|
|
246
|
+
_ = std.Io.Dir.cwd().createDir(io, "dist", .default_dir) catch {};
|
|
247
|
+
|
|
248
|
+
std.log.info("[3/4] Searching for Inno Setup (ISCC.exe)...", .{});
|
|
249
|
+
if (findIscc(allocator)) |iscc_path| {
|
|
250
|
+
defer allocator.free(iscc_path);
|
|
251
|
+
std.log.info("Found Inno Setup at: {s}", .{iscc_path});
|
|
252
|
+
std.log.info("[4/4] Compiling setup.iss into dist\\{s}-Setup.exe...", .{config.name});
|
|
253
|
+
|
|
254
|
+
// Auto-compile using Windows CreateProcessW API
|
|
255
|
+
const cmd_str = try std.fmt.allocPrint(allocator, "\"{s}\" setup.iss", .{iscc_path});
|
|
256
|
+
defer allocator.free(cmd_str);
|
|
257
|
+
|
|
258
|
+
var cmd_utf16: [1024]u16 = undefined;
|
|
259
|
+
const cmd_len = try std.unicode.utf8ToUtf16Le(&cmd_utf16, cmd_str);
|
|
260
|
+
cmd_utf16[cmd_len] = 0;
|
|
261
|
+
|
|
262
|
+
const STARTUPINFOW = extern struct {
|
|
263
|
+
cb: u32 = 104,
|
|
264
|
+
lpReserved: ?[*:0]u16 = null,
|
|
265
|
+
lpDesktop: ?[*:0]u16 = null,
|
|
266
|
+
lpTitle: ?[*:0]u16 = null,
|
|
267
|
+
dwX: u32 = 0,
|
|
268
|
+
dwY: u32 = 0,
|
|
269
|
+
dwXSize: u32 = 0,
|
|
270
|
+
dwYSize: u32 = 0,
|
|
271
|
+
dwXCountChars: u32 = 0,
|
|
272
|
+
dwYCountChars: u32 = 0,
|
|
273
|
+
dwFillAttribute: u32 = 0,
|
|
274
|
+
dwFlags: u32 = 0,
|
|
275
|
+
wShowWindow: u16 = 0,
|
|
276
|
+
cbReserved2: u16 = 0,
|
|
277
|
+
lpReserved2: ?*u8 = null,
|
|
278
|
+
hStdInput: ?std.os.windows.HANDLE = null,
|
|
279
|
+
hStdOutput: ?std.os.windows.HANDLE = null,
|
|
280
|
+
hStdError: ?std.os.windows.HANDLE = null,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const PROCESS_INFORMATION = extern struct {
|
|
284
|
+
hProcess: std.os.windows.HANDLE,
|
|
285
|
+
hThread: std.os.windows.HANDLE,
|
|
286
|
+
dwProcessId: u32,
|
|
287
|
+
dwThreadId: u32,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const WinApi = struct {
|
|
291
|
+
pub extern "kernel32" fn CreateProcessW(
|
|
292
|
+
lpApplicationName: ?[*:0]const u16,
|
|
293
|
+
lpCommandLine: ?[*:0]u16,
|
|
294
|
+
lpProcessAttributes: ?*anyopaque,
|
|
295
|
+
lpThreadAttributes: ?*anyopaque,
|
|
296
|
+
bInheritHandles: i32,
|
|
297
|
+
dwCreationFlags: u32,
|
|
298
|
+
lpEnvironment: ?*anyopaque,
|
|
299
|
+
lpCurrentDirectory: ?[*:0]const u16,
|
|
300
|
+
lpStartupInfo: *const STARTUPINFOW,
|
|
301
|
+
lpProcessInformation: *PROCESS_INFORMATION,
|
|
302
|
+
) callconv(.winapi) i32;
|
|
303
|
+
|
|
304
|
+
pub extern "kernel32" fn WaitForSingleObject(hHandle: std.os.windows.HANDLE, dwMilliseconds: u32) callconv(.winapi) u32;
|
|
305
|
+
pub extern "kernel32" fn CloseHandle(hObject: std.os.windows.HANDLE) callconv(.winapi) i32;
|
|
306
|
+
pub extern "kernel32" fn GetExitCodeProcess(hProcess: std.os.windows.HANDLE, lpExitCode: *u32) callconv(.winapi) i32;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
var si = STARTUPINFOW{};
|
|
310
|
+
var pi: PROCESS_INFORMATION = undefined;
|
|
311
|
+
|
|
312
|
+
const res = WinApi.CreateProcessW(
|
|
313
|
+
null,
|
|
314
|
+
@ptrCast(&cmd_utf16),
|
|
315
|
+
null,
|
|
316
|
+
null,
|
|
317
|
+
0,
|
|
318
|
+
0,
|
|
319
|
+
null,
|
|
320
|
+
null,
|
|
321
|
+
&si,
|
|
322
|
+
&pi,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if (res != 0) {
|
|
326
|
+
defer _ = WinApi.CloseHandle(pi.hProcess);
|
|
327
|
+
defer _ = WinApi.CloseHandle(pi.hThread);
|
|
328
|
+
_ = WinApi.WaitForSingleObject(pi.hProcess, 0xFFFFFFFF);
|
|
329
|
+
|
|
330
|
+
var exit_code: u32 = 0;
|
|
331
|
+
_ = WinApi.GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
332
|
+
|
|
333
|
+
if (exit_code == 0) {
|
|
334
|
+
std.log.info("=================================================================", .{});
|
|
335
|
+
std.log.info(" SUCCESS! Windows Installer Built -> dist\\{s}-Setup.exe", .{config.name});
|
|
336
|
+
std.log.info("=================================================================", .{});
|
|
337
|
+
} else {
|
|
338
|
+
std.log.err("ISCC compilation exited with code {d}.", .{exit_code});
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
std.log.warn("Could not auto-launch ISCC.exe. Open setup.iss in Inno Setup.", .{});
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
std.log.info("[4/4] setup.iss generated successfully!", .{});
|
|
345
|
+
std.log.info(" 1. Install Inno Setup: https://jrsoftware.org/isdl.php", .{});
|
|
346
|
+
std.log.info(" 2. Open setup.iss or run: ISCC setup.iss", .{});
|
|
347
|
+
std.log.info(" 3. Output binary will be saved in: dist\\{s}-Setup.exe", .{config.name});
|
|
348
|
+
}
|
|
349
|
+
}
|
package/cli/main.zig
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const installer = @import("installer.zig");
|
|
3
|
+
|
|
4
|
+
extern "kernel32" fn GetCommandLineW() callconv(.c) [*:0]const u16;
|
|
5
|
+
|
|
6
|
+
pub fn main() !void {
|
|
7
|
+
const allocator = std.heap.page_allocator;
|
|
8
|
+
|
|
9
|
+
const cmd_line_ptr = GetCommandLineW();
|
|
10
|
+
const len = std.mem.indexOfSentinel(u16, 0, cmd_line_ptr);
|
|
11
|
+
|
|
12
|
+
var args_iter = try std.process.Args.Iterator.Windows.init(allocator, cmd_line_ptr[0..len]);
|
|
13
|
+
defer args_iter.deinit();
|
|
14
|
+
|
|
15
|
+
_ = args_iter.skip(); // skip executable name
|
|
16
|
+
|
|
17
|
+
const subcmd = args_iter.next() orelse "";
|
|
18
|
+
|
|
19
|
+
if (std.mem.eql(u8, subcmd, "build")) {
|
|
20
|
+
std.log.info("NanoShell Build", .{});
|
|
21
|
+
std.log.info("Building production release binary...", .{});
|
|
22
|
+
|
|
23
|
+
const io = std.Io.Threaded.global_single_threaded.io();
|
|
24
|
+
_ = std.Io.Dir.cwd().createDir(io, "dist", .default_dir) catch {};
|
|
25
|
+
|
|
26
|
+
var config = installer.loadConfig(allocator) catch installer.AppConfig{};
|
|
27
|
+
defer config.deinit(allocator);
|
|
28
|
+
|
|
29
|
+
var bundler = @import("bundler.zig").SingleBinaryBundler.init(allocator, .{
|
|
30
|
+
.app_name = config.name,
|
|
31
|
+
.entry_point = "app/index.html",
|
|
32
|
+
.output_dir = "dist",
|
|
33
|
+
});
|
|
34
|
+
try bundler.bundle();
|
|
35
|
+
|
|
36
|
+
std.log.info("Build complete.", .{});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (std.mem.eql(u8, subcmd, "package") or std.mem.eql(u8, subcmd, "installer")) {
|
|
41
|
+
std.log.info("NanoShell Build & Package", .{});
|
|
42
|
+
std.log.info("Building production release binary...", .{});
|
|
43
|
+
|
|
44
|
+
const io = std.Io.Threaded.global_single_threaded.io();
|
|
45
|
+
_ = std.Io.Dir.cwd().createDir(io, "dist", .default_dir) catch {};
|
|
46
|
+
|
|
47
|
+
var config = installer.loadConfig(allocator) catch installer.AppConfig{};
|
|
48
|
+
defer config.deinit(allocator);
|
|
49
|
+
|
|
50
|
+
var bundler = @import("bundler.zig").SingleBinaryBundler.init(allocator, .{
|
|
51
|
+
.app_name = config.name,
|
|
52
|
+
.entry_point = "app/index.html",
|
|
53
|
+
.output_dir = "dist",
|
|
54
|
+
});
|
|
55
|
+
try bundler.bundle();
|
|
56
|
+
|
|
57
|
+
try installer.run(allocator);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const dev_server = @import("dev_server.zig");
|
|
62
|
+
|
|
63
|
+
if (std.mem.eql(u8, subcmd, "start")) {
|
|
64
|
+
var server = dev_server.DevServer.init(allocator);
|
|
65
|
+
try server.runDevMode(".");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Scaffolding a new NanoShell project if an app name is passed
|
|
70
|
+
if (subcmd.len > 0 and !std.mem.startsWith(u8, subcmd, "-")) {
|
|
71
|
+
const app_dir_name = subcmd;
|
|
72
|
+
std.log.info("Scaffolding new NanoShell app in .\\{s}...", .{app_dir_name});
|
|
73
|
+
|
|
74
|
+
const io = std.Io.Threaded.global_single_threaded.io();
|
|
75
|
+
_ = std.Io.Dir.cwd().createDir(io, app_dir_name, .default_dir) catch |err| {
|
|
76
|
+
if (err != error.PathAlreadyExists) {
|
|
77
|
+
std.log.err("Failed to create directory '{s}': {s}", .{ app_dir_name, @errorName(err) });
|
|
78
|
+
return err;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
var app_dir = std.Io.Dir.cwd().openDir(io, app_dir_name, .{}) catch |err| {
|
|
83
|
+
std.log.err("Failed to open directory '{s}': {s}", .{ app_dir_name, @errorName(err) });
|
|
84
|
+
return err;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
_ = app_dir.createDir(io, "app", .default_dir) catch {};
|
|
88
|
+
_ = app_dir.createDir(io, "assets", .default_dir) catch {};
|
|
89
|
+
_ = app_dir.createDir(io, "resources", .default_dir) catch {};
|
|
90
|
+
_ = app_dir.createDir(io, "bin", .default_dir) catch {};
|
|
91
|
+
_ = app_dir.createDir(io, "bin/resources", .default_dir) catch {};
|
|
92
|
+
|
|
93
|
+
// Write placeholder assets/icon.ico
|
|
94
|
+
try app_dir.writeFile(io, .{ .sub_path = "assets/icon.ico", .data = "NANO_ICON" });
|
|
95
|
+
|
|
96
|
+
// Write default nanoshell.json with full window management settings
|
|
97
|
+
const json_content = try std.fmt.allocPrint(allocator,
|
|
98
|
+
"{{\n" ++
|
|
99
|
+
" \"name\": \"{s}\",\n" ++
|
|
100
|
+
" \"version\": \"1.0.0\",\n" ++
|
|
101
|
+
" \"author\": \"Your Name\",\n" ++
|
|
102
|
+
" \"description\": \"Blazing fast desktop app created with NanoShell\",\n" ++
|
|
103
|
+
" \"icon\": \"assets\\\\icon.ico\",\n" ++
|
|
104
|
+
" \"exe_name\": \"{s}.exe\",\n" ++
|
|
105
|
+
" \"window\": {{\n" ++
|
|
106
|
+
" \"width\": 1280,\n" ++
|
|
107
|
+
" \"height\": 720,\n" ++
|
|
108
|
+
" \"resizable\": true,\n" ++
|
|
109
|
+
" \"frameless\": false,\n" ++
|
|
110
|
+
" \"transparent\": false,\n" ++
|
|
111
|
+
" \"always_on_top\": false\n" ++
|
|
112
|
+
" }}\n" ++
|
|
113
|
+
"}}\n",
|
|
114
|
+
.{ app_dir_name, app_dir_name }
|
|
115
|
+
);
|
|
116
|
+
defer allocator.free(json_content);
|
|
117
|
+
|
|
118
|
+
try app_dir.writeFile(io, .{ .sub_path = "nanoshell.json", .data = json_content });
|
|
119
|
+
|
|
120
|
+
// Write default HTML/CSS/JS inside app/
|
|
121
|
+
const html_content =
|
|
122
|
+
\\<!DOCTYPE html>
|
|
123
|
+
\\<html lang="en">
|
|
124
|
+
\\<head>
|
|
125
|
+
\\ <meta charset="UTF-8">
|
|
126
|
+
\\ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
127
|
+
\\ <title>NanoShell Application</title>
|
|
128
|
+
\\ <link rel="stylesheet" href="styles.css">
|
|
129
|
+
\\</head>
|
|
130
|
+
\\<body>
|
|
131
|
+
\\ <div class="container">
|
|
132
|
+
\\ <h1>⚡ NanoShell Desktop App</h1>
|
|
133
|
+
\\ <p>Ultra-lightweight WebKit desktop runtime</p>
|
|
134
|
+
\\ <button id="btn">Click Me</button>
|
|
135
|
+
\\ </div>
|
|
136
|
+
\\ <script src="app.js"></script>
|
|
137
|
+
\\</body>
|
|
138
|
+
\\</html>
|
|
139
|
+
;
|
|
140
|
+
try app_dir.writeFile(io, .{ .sub_path = "app/index.html", .data = html_content });
|
|
141
|
+
|
|
142
|
+
const css_content =
|
|
143
|
+
\\body {
|
|
144
|
+
\\ margin: 0;
|
|
145
|
+
\\ font-family: system-ui, -apple-system, sans-serif;
|
|
146
|
+
\\ background: #0f172a;
|
|
147
|
+
\\ color: #f8fafc;
|
|
148
|
+
\\ display: flex;
|
|
149
|
+
\\ justify-content: center;
|
|
150
|
+
\\ align-items: center;
|
|
151
|
+
\\ height: 100vh;
|
|
152
|
+
\\}
|
|
153
|
+
\\.container {
|
|
154
|
+
\\ text-align: center;
|
|
155
|
+
\\ background: #1e293b;
|
|
156
|
+
\\ padding: 2.5rem;
|
|
157
|
+
\\ border-radius: 1rem;
|
|
158
|
+
\\ box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
|
159
|
+
\\}
|
|
160
|
+
\\button {
|
|
161
|
+
\\ background: #38bdf8;
|
|
162
|
+
\\ color: #0f172a;
|
|
163
|
+
\\ border: none;
|
|
164
|
+
\\ padding: 0.75rem 1.5rem;
|
|
165
|
+
\\ font-weight: bold;
|
|
166
|
+
\\ border-radius: 0.5rem;
|
|
167
|
+
\\ cursor: pointer;
|
|
168
|
+
\\}
|
|
169
|
+
\\button:hover { background: #7dd3fc; }
|
|
170
|
+
;
|
|
171
|
+
try app_dir.writeFile(io, .{ .sub_path = "app/styles.css", .data = css_content });
|
|
172
|
+
|
|
173
|
+
const js_content =
|
|
174
|
+
\\document.getElementById('btn').addEventListener('click', () => {
|
|
175
|
+
\\ alert('Hello from NanoShell!');
|
|
176
|
+
\\});
|
|
177
|
+
;
|
|
178
|
+
try app_dir.writeFile(io, .{ .sub_path = "app/app.js", .data = js_content });
|
|
179
|
+
|
|
180
|
+
// Copy prebuilt runtime binaries and DLLs into scaffolded project
|
|
181
|
+
const cwd = std.Io.Dir.cwd();
|
|
182
|
+
const exe_dst_path = try std.fmt.allocPrint(allocator, "bin/{s}.exe", .{app_dir_name});
|
|
183
|
+
defer allocator.free(exe_dst_path);
|
|
184
|
+
|
|
185
|
+
_ = cwd.copyFile("zig-out/bin/example_app.exe", app_dir, exe_dst_path, io, .{}) catch
|
|
186
|
+
cwd.copyFile("example_app.exe", app_dir, exe_dst_path, io, .{}) catch {};
|
|
187
|
+
|
|
188
|
+
_ = cwd.copyFile("zig-out/bin/AppCore.dll", app_dir, "bin/AppCore.dll", io, .{}) catch
|
|
189
|
+
cwd.copyFile("AppCore.dll", app_dir, "bin/AppCore.dll", io, .{}) catch {};
|
|
190
|
+
|
|
191
|
+
_ = cwd.copyFile("zig-out/bin/Ultralight.dll", app_dir, "bin/Ultralight.dll", io, .{}) catch
|
|
192
|
+
cwd.copyFile("Ultralight.dll", app_dir, "bin/Ultralight.dll", io, .{}) catch {};
|
|
193
|
+
|
|
194
|
+
_ = cwd.copyFile("zig-out/bin/UltralightCore.dll", app_dir, "bin/UltralightCore.dll", io, .{}) catch
|
|
195
|
+
cwd.copyFile("UltralightCore.dll", app_dir, "bin/UltralightCore.dll", io, .{}) catch {};
|
|
196
|
+
|
|
197
|
+
_ = cwd.copyFile("zig-out/bin/WebCore.dll", app_dir, "bin/WebCore.dll", io, .{}) catch
|
|
198
|
+
cwd.copyFile("WebCore.dll", app_dir, "bin/WebCore.dll", io, .{}) catch {};
|
|
199
|
+
|
|
200
|
+
_ = cwd.copyFile("vendor/resources/icudt67l.dat", app_dir, "bin/icudt67l.dat", io, .{}) catch
|
|
201
|
+
cwd.copyFile("bin/icudt67l.dat", app_dir, "bin/icudt67l.dat", io, .{}) catch {};
|
|
202
|
+
|
|
203
|
+
_ = cwd.copyFile("vendor/resources/cacert.pem", app_dir, "bin/cacert.pem", io, .{}) catch
|
|
204
|
+
cwd.copyFile("bin/cacert.pem", app_dir, "bin/cacert.pem", io, .{}) catch {};
|
|
205
|
+
|
|
206
|
+
_ = cwd.copyFile("vendor/resources/icudt67l.dat", app_dir, "bin/resources/icudt67l.dat", io, .{}) catch
|
|
207
|
+
cwd.copyFile("bin/resources/icudt67l.dat", app_dir, "bin/resources/icudt67l.dat", io, .{}) catch {};
|
|
208
|
+
|
|
209
|
+
_ = cwd.copyFile("vendor/resources/cacert.pem", app_dir, "bin/resources/cacert.pem", io, .{}) catch
|
|
210
|
+
cwd.copyFile("bin/resources/cacert.pem", app_dir, "bin/resources/cacert.pem", io, .{}) catch {};
|
|
211
|
+
|
|
212
|
+
std.log.info("=================================================================", .{});
|
|
213
|
+
std.log.info(" SUCCESS! Scaffolded new NanoShell app: .\\{s}", .{app_dir_name});
|
|
214
|
+
std.log.info(" To get started:", .{});
|
|
215
|
+
std.log.info(" cd {s}", .{app_dir_name});
|
|
216
|
+
std.log.info(" npx nanoshell package", .{});
|
|
217
|
+
std.log.info("=================================================================", .{});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Help banner if no command or invalid command is provided
|
|
222
|
+
std.log.info("+===================================================+", .{});
|
|
223
|
+
std.log.info("| NanoShell CLI v1.0.4 . by Suman Biswas |", .{});
|
|
224
|
+
std.log.info("| 17MB RAM . 120 FPS . WebKit-powered desktop apps |", .{});
|
|
225
|
+
std.log.info("+===================================================+", .{});
|
|
226
|
+
std.log.info("Usage:", .{});
|
|
227
|
+
std.log.info(" npx nanoshell <my-app> Scaffold a new NanoShell app", .{});
|
|
228
|
+
std.log.info(" npx nanoshell build Build production release binary only", .{});
|
|
229
|
+
std.log.info(" npx nanoshell start Launch the dev engine", .{});
|
|
230
|
+
std.log.info(" npx nanoshell package Build binary and generate Windows installer", .{});
|
|
231
|
+
std.log.info("nanoshell package:", .{});
|
|
232
|
+
std.log.info(" - Reads nanoshell.json for app name, version, author", .{});
|
|
233
|
+
std.log.info(" - Generates setup.iss (Inno Setup script)", .{});
|
|
234
|
+
std.log.info(" - Auto-compiles to dist\\AppName-Setup.exe if ISCC found", .{});
|
|
235
|
+
std.log.info(" - Installer lets user choose: Admin OR per-user install", .{});
|
|
236
|
+
}
|
package/example_app/app.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
console.log("[ZeroUI App] Initializing Showcase Application...");
|
|
4
4
|
|
|
5
|
-
// 1. Query Native System Metrics via ZeroUI Native OS Bridge
|
|
5
|
+
// 1. Query Native System Metrics via NanoShell / ZeroUI Native OS Bridge
|
|
6
6
|
function refreshSystemStats() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
7
|
+
const ns = (typeof NanoShell !== "undefined") ? NanoShell : ((typeof ZeroUI !== "undefined") ? ZeroUI : null);
|
|
8
|
+
if (ns && ns.sys) {
|
|
9
|
+
const mem = ns.sys.getMemoryStats();
|
|
10
|
+
const cpu = ns.sys.getCpuStats();
|
|
10
11
|
|
|
11
|
-
console.log(`[
|
|
12
|
+
console.log(`[NanoShell Sys] Arch: ${cpu.arch}, Cores: ${cpu.logical_cores}, RSS: ${(mem.total_rss_bytes / 1024).toFixed(1)} KB`);
|
|
12
13
|
|
|
13
14
|
const ramEl = document.getElementById("val-ram");
|
|
14
15
|
if (ramEl) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanoshell",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Hyper-lightweight 17MB RAM, 120 FPS native desktop application framework & runtime engine created by Suman Biswas",
|
|
5
5
|
"main": "zig-out/bin/example_app.exe",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"readme": "README.md",
|
|
11
11
|
"files": [
|
|
12
|
-
"cli/
|
|
12
|
+
"cli/",
|
|
13
13
|
"zig-out/bin/*.exe",
|
|
14
14
|
"zig-out/bin/*.dll",
|
|
15
15
|
"vendor/bin/",
|
|
Binary file
|
|
Binary file
|