chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# Native SDK command-app example
|
||||
|
||||
This example shows one `app.sync` command handled from each user-facing entry point:
|
||||
|
||||
- Native toolbar button.
|
||||
- Native menu item.
|
||||
- Native tray item.
|
||||
- App shortcut.
|
||||
- WebView bridge call.
|
||||
- Manifest command catalog listing from the WebView.
|
||||
|
||||
Run with the system backend:
|
||||
|
||||
```sh
|
||||
zig build run -Dplatform=macos -Dweb-engine=system
|
||||
```
|
||||
|
||||
Run the headless test path:
|
||||
|
||||
```sh
|
||||
zig build test -Dplatform=null
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
.{
|
||||
.id = "dev.native_sdk.command_app",
|
||||
.name = "command-app",
|
||||
.display_name = "Command App",
|
||||
.version = "0.1.0",
|
||||
.platforms = .{ "macos", "linux" },
|
||||
.permissions = .{ "command" },
|
||||
.capabilities = .{ "webview", "js_bridge", "native_views", "menus", "shortcuts", "tray" },
|
||||
.commands = .{
|
||||
.{ .id = "app.sync", .title = "Sync" },
|
||||
},
|
||||
.shortcuts = .{
|
||||
.{ .id = "app.sync", .key = "s", .modifiers = .{ "primary" } },
|
||||
},
|
||||
.menus = .{
|
||||
.{
|
||||
.title = "View",
|
||||
.items = .{
|
||||
.{ .label = "Sync", .command = "app.sync", .key = "s", .modifiers = .{ "primary" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
.shell = .{
|
||||
.windows = .{
|
||||
.{
|
||||
.label = "main",
|
||||
.title = "Native SDK Command App",
|
||||
.width = 860,
|
||||
.height = 560,
|
||||
.restore_policy = "center_on_primary",
|
||||
.views = .{
|
||||
.{ .label = "toolbar", .kind = "toolbar", .edge = "top", .height = 48, .role = "Toolbar" },
|
||||
.{ .label = "sync-button", .kind = "button", .parent = "toolbar", .x = 12, .y = 9, .width = 92, .height = 30, .accessibility_label = "Sync now", .text = "Sync", .command = "app.sync" },
|
||||
.{ .label = "main", .kind = "webview", .url = "zero://inline", .fill = true },
|
||||
.{ .label = "statusbar", .kind = "statusbar", .edge = "bottom", .height = 34, .role = "Status" },
|
||||
.{ .label = "status-label", .kind = "label", .parent = "statusbar", .x = 14, .y = 8, .width = 520, .height = 18, .text = "Ready. Use the toolbar, menu, tray, shortcut, or WebView button." },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
.security = .{
|
||||
.navigation = .{
|
||||
.allowed_origins = .{ "zero://app", "zero://inline" },
|
||||
.external_links = .{ .action = "deny" },
|
||||
},
|
||||
},
|
||||
.web_engine = "system",
|
||||
.cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
// This example owns its build: it hand-wires the framework modules, src/runner.zig, and the -Dtrace/-Djs-bridge/-Dweb-engine options that the generated app graph does not expose.
|
||||
const std = @import("std");
|
||||
|
||||
const PlatformOption = enum {
|
||||
auto,
|
||||
null,
|
||||
macos,
|
||||
linux,
|
||||
windows,
|
||||
};
|
||||
|
||||
const TraceOption = enum {
|
||||
off,
|
||||
events,
|
||||
runtime,
|
||||
all,
|
||||
};
|
||||
|
||||
const WebEngineOption = enum {
|
||||
system,
|
||||
chromium,
|
||||
};
|
||||
|
||||
const default_native_sdk_path = "../../";
|
||||
const app_exe_name = "command-app";
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = nativeSdkTarget(b);
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const platform_option = b.option(PlatformOption, "platform", "Desktop backend: auto, null, macos, linux, windows") orelse .auto;
|
||||
const trace_option = b.option(TraceOption, "trace", "Trace output: off, events, runtime, all") orelse .events;
|
||||
const debug_overlay = b.option(bool, "debug-overlay", "Enable debug overlay output") orelse false;
|
||||
const automation_enabled = b.option(bool, "automation", "Enable Native SDK automation artifacts") orelse false;
|
||||
const js_bridge_enabled = b.option(bool, "js-bridge", "Enable optional JavaScript bridge stubs") orelse false;
|
||||
const web_engine_override = b.option(WebEngineOption, "web-engine", "Override app.zon web engine: system, chromium");
|
||||
const cef_dir_override = b.option([]const u8, "cef-dir", "Override CEF root directory for Chromium builds");
|
||||
const cef_auto_install_override = b.option(bool, "cef-auto-install", "Override app.zon CEF auto-install setting");
|
||||
const native_sdk_path = b.option([]const u8, "native-sdk-path", "Path to the Native SDK framework checkout") orelse default_native_sdk_path;
|
||||
const selected_platform: PlatformOption = switch (platform_option) {
|
||||
.auto => if (target.result.os.tag == .macos) .macos else if (target.result.os.tag == .linux) .linux else if (target.result.os.tag == .windows) .windows else .null,
|
||||
else => platform_option,
|
||||
};
|
||||
if (selected_platform == .macos and target.result.os.tag != .macos) {
|
||||
@panic("-Dplatform=macos requires a macOS target");
|
||||
}
|
||||
if (selected_platform == .linux and target.result.os.tag != .linux) {
|
||||
@panic("-Dplatform=linux requires a Linux target");
|
||||
}
|
||||
if (selected_platform == .windows and target.result.os.tag != .windows) {
|
||||
@panic("-Dplatform=windows requires a Windows target");
|
||||
}
|
||||
const app_web_engine = appWebEngineConfig();
|
||||
const web_engine = web_engine_override orelse app_web_engine.web_engine;
|
||||
const cef_dir = cef_dir_override orelse defaultCefDir(selected_platform, app_web_engine.cef_dir);
|
||||
const cef_auto_install = cef_auto_install_override orelse app_web_engine.cef_auto_install;
|
||||
if (web_engine == .chromium and selected_platform != .macos) {
|
||||
@panic("-Dweb-engine=chromium currently requires -Dplatform=macos");
|
||||
}
|
||||
|
||||
const native_sdk_mod = nativeSdkModule(b, target, optimize, native_sdk_path);
|
||||
const options = b.addOptions();
|
||||
options.addOption([]const u8, "platform", switch (selected_platform) {
|
||||
.auto => unreachable,
|
||||
.null => "null",
|
||||
.macos => "macos",
|
||||
.linux => "linux",
|
||||
.windows => "windows",
|
||||
});
|
||||
options.addOption([]const u8, "trace", @tagName(trace_option));
|
||||
options.addOption([]const u8, "web_engine", @tagName(web_engine));
|
||||
options.addOption(bool, "debug_overlay", debug_overlay);
|
||||
options.addOption(bool, "automation", automation_enabled);
|
||||
options.addOption(bool, "js_bridge", js_bridge_enabled);
|
||||
const options_mod = options.createModule();
|
||||
|
||||
const runner_mod = localModule(b, target, optimize, "src/runner.zig");
|
||||
runner_mod.addImport("native_sdk", native_sdk_mod);
|
||||
runner_mod.addImport("build_options", options_mod);
|
||||
runner_mod.addImport("app_manifest_zon", b.createModule(.{ .root_source_file = b.path("app.zon") }));
|
||||
|
||||
const app_mod = localModule(b, target, optimize, "src/main.zig");
|
||||
app_mod.addImport("native_sdk", native_sdk_mod);
|
||||
app_mod.addImport("runner", runner_mod);
|
||||
const exe = b.addExecutable(.{
|
||||
.name = app_exe_name,
|
||||
.root_module = app_mod,
|
||||
});
|
||||
linkPlatform(b, target, app_mod, exe, selected_platform, web_engine, native_sdk_path, cef_dir, cef_auto_install);
|
||||
b.installArtifact(exe);
|
||||
|
||||
const run = b.addRunArtifact(exe);
|
||||
addCefRuntimeRunFiles(b, target, run, exe, web_engine, cef_dir);
|
||||
addWebView2RuntimeRunFiles(b, target, run, web_engine, native_sdk_path);
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run.step);
|
||||
|
||||
const tests = b.addTest(.{ .root_module = app_mod });
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&b.addRunArtifact(tests).step);
|
||||
}
|
||||
|
||||
fn nativeSdkTarget(b: *std.Build) std.Build.ResolvedTarget {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
if (target.result.os.tag != .macos) return target;
|
||||
|
||||
if (b.sysroot == null) {
|
||||
b.sysroot = macosSdkPath(b) orelse b.sysroot;
|
||||
}
|
||||
|
||||
var query = target.query;
|
||||
query.os_tag = .macos;
|
||||
query.os_version_min = .{ .semver = .{ .major = 11, .minor = 0, .patch = 0 } };
|
||||
return b.resolveTargetQuery(query);
|
||||
}
|
||||
|
||||
fn macosSdkPath(b: *std.Build) ?[]const u8 {
|
||||
if (b.graph.environ_map.get("SDKROOT")) |sdkroot| {
|
||||
if (sdkroot.len > 0) return sdkroot;
|
||||
}
|
||||
|
||||
const result = std.process.run(b.allocator, b.graph.io, .{
|
||||
.argv = &.{ "xcrun", "--sdk", "macosx", "--show-sdk-path" },
|
||||
.stdout_limit = .limited(4096),
|
||||
.stderr_limit = .limited(4096),
|
||||
}) catch return null;
|
||||
defer b.allocator.free(result.stderr);
|
||||
if (result.term != .exited or result.term.exited != 0) {
|
||||
b.allocator.free(result.stdout);
|
||||
return null;
|
||||
}
|
||||
return std.mem.trimEnd(u8, result.stdout, "\r\n");
|
||||
}
|
||||
|
||||
fn localModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module {
|
||||
return b.createModule(.{
|
||||
.root_source_file = b.path(path),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
}
|
||||
|
||||
fn nativeSdkPath(b: *std.Build, native_sdk_path: []const u8, sub_path: []const u8) std.Build.LazyPath {
|
||||
return .{ .cwd_relative = b.pathJoin(&.{ native_sdk_path, sub_path }) };
|
||||
}
|
||||
|
||||
fn nativeSdkModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, native_sdk_path: []const u8) *std.Build.Module {
|
||||
const geometry_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/geometry/root.zig");
|
||||
const assets_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/assets/root.zig");
|
||||
const app_dirs_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/app_dirs/root.zig");
|
||||
const trace_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/trace/root.zig");
|
||||
const app_manifest_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/app_manifest/root.zig");
|
||||
const diagnostics_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/diagnostics/root.zig");
|
||||
const platform_info_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/platform_info/root.zig");
|
||||
const json_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/json/root.zig");
|
||||
const canvas_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/canvas/root.zig");
|
||||
canvas_mod.addImport("geometry", geometry_mod);
|
||||
canvas_mod.addImport("json", json_mod);
|
||||
const debug_mod = externalModule(b, target, optimize, native_sdk_path, "src/debug/root.zig");
|
||||
debug_mod.addImport("app_dirs", app_dirs_mod);
|
||||
debug_mod.addImport("trace", trace_mod);
|
||||
|
||||
const native_sdk_mod = externalModule(b, target, optimize, native_sdk_path, "src/root.zig");
|
||||
native_sdk_mod.addImport("geometry", geometry_mod);
|
||||
native_sdk_mod.addImport("assets", assets_mod);
|
||||
native_sdk_mod.addImport("app_dirs", app_dirs_mod);
|
||||
native_sdk_mod.addImport("trace", trace_mod);
|
||||
native_sdk_mod.addImport("app_manifest", app_manifest_mod);
|
||||
native_sdk_mod.addImport("diagnostics", diagnostics_mod);
|
||||
native_sdk_mod.addImport("platform_info", platform_info_mod);
|
||||
native_sdk_mod.addImport("json", json_mod);
|
||||
native_sdk_mod.addImport("canvas", canvas_mod);
|
||||
return native_sdk_mod;
|
||||
}
|
||||
|
||||
fn externalModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, native_sdk_path: []const u8, path: []const u8) *std.Build.Module {
|
||||
return b.createModule(.{
|
||||
.root_source_file = nativeSdkPath(b, native_sdk_path, path),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
}
|
||||
|
||||
fn linkPlatform(b: *std.Build, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, web_engine: WebEngineOption, native_sdk_path: []const u8, cef_dir: []const u8, cef_auto_install: bool) void {
|
||||
if (platform == .macos) {
|
||||
switch (web_engine) {
|
||||
.system => {
|
||||
const sdk_include = if (b.sysroot) |sysroot| b.fmt("-I{s}/usr/include", .{sysroot}) else "";
|
||||
const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0" };
|
||||
app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/macos/appkit_host.m"), .flags = flags });
|
||||
app_mod.linkFramework("WebKit", .{});
|
||||
},
|
||||
.chromium => {
|
||||
const cef_check = addCefCheck(b, target, cef_dir);
|
||||
if (cef_auto_install) {
|
||||
const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir });
|
||||
cef_check.step.dependOn(&cef_auto.step);
|
||||
}
|
||||
exe.step.dependOn(&cef_check.step);
|
||||
const include_arg = b.fmt("-I{s}", .{cef_dir});
|
||||
const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir});
|
||||
// The SDK's usr/include must stay a system include dir (searched after zig's
|
||||
// bundled libc++/libc headers). A plain -I shadows libc++'s <string.h>/<math.h>
|
||||
// wrappers in ObjC++ and surfaces SDK nullability gaps as a diagnostic flood.
|
||||
const sdk_include = if (b.sysroot) |sysroot| b.fmt("-isystem{s}/usr/include", .{sysroot}) else "";
|
||||
const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include, include_arg, define_arg } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", include_arg, define_arg };
|
||||
app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/macos/cef_host.mm"), .flags = flags });
|
||||
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir})));
|
||||
app_mod.addFrameworkPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
||||
app_mod.linkFramework("Chromium Embedded Framework", .{});
|
||||
app_mod.addRPath(.{ .cwd_relative = "@executable_path/Frameworks" });
|
||||
},
|
||||
}
|
||||
if (b.sysroot) |sysroot| {
|
||||
app_mod.addFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sysroot, "System/Library/Frameworks" }) });
|
||||
}
|
||||
app_mod.linkFramework("AppKit", .{});
|
||||
// The audio playback service (the AppKit host's single AVPlayer).
|
||||
app_mod.linkFramework("AVFoundation", .{});
|
||||
// Spectrum analysis of the app's own playback: the MediaToolbox
|
||||
// audio tap hands the player's PCM to the host, and Accelerate
|
||||
// (vDSP) turns it into band magnitudes.
|
||||
app_mod.linkFramework("MediaToolbox", .{});
|
||||
app_mod.linkFramework("Accelerate", .{});
|
||||
app_mod.linkFramework("Foundation", .{});
|
||||
app_mod.linkFramework("CoreText", .{});
|
||||
app_mod.linkFramework("UniformTypeIdentifiers", .{});
|
||||
app_mod.linkFramework("Security", .{});
|
||||
app_mod.linkFramework("Metal", .{});
|
||||
app_mod.linkFramework("QuartzCore", .{});
|
||||
app_mod.linkSystemLibrary("c", .{});
|
||||
if (web_engine == .chromium) app_mod.linkSystemLibrary("c++", .{});
|
||||
} else if (platform == .linux) {
|
||||
switch (web_engine) {
|
||||
.system => {
|
||||
app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/linux/gtk_host.c"), .flags = &.{} });
|
||||
app_mod.linkSystemLibrary("gtk4", .{});
|
||||
app_mod.linkSystemLibrary("webkitgtk-6.0", .{});
|
||||
app_mod.linkSystemLibrary("dl", .{});
|
||||
},
|
||||
.chromium => {
|
||||
const cef_check = addCefCheck(b, target, cef_dir);
|
||||
if (cef_auto_install) {
|
||||
const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir });
|
||||
cef_check.step.dependOn(&cef_auto.step);
|
||||
}
|
||||
exe.step.dependOn(&cef_check.step);
|
||||
const include_arg = b.fmt("-I{s}", .{cef_dir});
|
||||
const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir});
|
||||
app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/linux/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } });
|
||||
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir})));
|
||||
app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
||||
app_mod.linkSystemLibrary("cef", .{});
|
||||
app_mod.addRPath(.{ .cwd_relative = "$ORIGIN" });
|
||||
},
|
||||
}
|
||||
app_mod.linkSystemLibrary("c", .{});
|
||||
if (web_engine == .chromium) app_mod.linkSystemLibrary("stdc++", .{});
|
||||
} else if (platform == .windows) {
|
||||
switch (web_engine) {
|
||||
.system => {
|
||||
// The vendored WebView2 SDK header (third_party/webview2)
|
||||
// turns on the host's embedded-WebView layer; the host
|
||||
// fails the compile by design if it cannot be found.
|
||||
app_mod.addIncludePath(nativeSdkPath(b, native_sdk_path, "third_party/webview2/include"));
|
||||
app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} });
|
||||
// WebView2Loader.dll rides next to the installed app
|
||||
// executable: the host loads it at runtime to discover
|
||||
// the machine's WebView2 runtime. Canvas apps never
|
||||
// touch it.
|
||||
const loader = b.addInstallBinFile(nativeSdkPath(b, native_sdk_path, webView2LoaderSubPath(target)), "WebView2Loader.dll");
|
||||
b.getInstallStep().dependOn(&loader.step);
|
||||
},
|
||||
.chromium => {
|
||||
const cef_check = addCefCheck(b, target, cef_dir);
|
||||
if (cef_auto_install) {
|
||||
const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir });
|
||||
cef_check.step.dependOn(&cef_auto.step);
|
||||
}
|
||||
exe.step.dependOn(&cef_check.step);
|
||||
const include_arg = b.fmt("-I{s}", .{cef_dir});
|
||||
const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir});
|
||||
app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/windows/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } });
|
||||
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib", .{cef_dir})));
|
||||
app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
||||
},
|
||||
}
|
||||
app_mod.linkSystemLibrary("c", .{});
|
||||
app_mod.linkSystemLibrary("c++", .{});
|
||||
app_mod.linkSystemLibrary("user32", .{});
|
||||
app_mod.linkSystemLibrary("gdi32", .{});
|
||||
app_mod.linkSystemLibrary("imm32", .{});
|
||||
app_mod.linkSystemLibrary("comctl32", .{});
|
||||
app_mod.linkSystemLibrary("ole32", .{});
|
||||
app_mod.linkSystemLibrary("oleacc", .{});
|
||||
app_mod.linkSystemLibrary("shell32", .{});
|
||||
// The audio backend: Media Foundation (session + source resolver
|
||||
// + streaming audio renderer) and WinHTTP (the cache fill).
|
||||
app_mod.linkSystemLibrary("mf", .{});
|
||||
app_mod.linkSystemLibrary("mfplat", .{});
|
||||
app_mod.linkSystemLibrary("winhttp", .{});
|
||||
if (web_engine == .chromium) app_mod.linkSystemLibrary("libcef", .{});
|
||||
}
|
||||
}
|
||||
|
||||
/// The vendored WebView2Loader.dll for the target architecture, relative
|
||||
/// to the framework root.
|
||||
fn webView2LoaderSubPath(target: std.Build.ResolvedTarget) []const u8 {
|
||||
return if (target.result.cpu.arch == .aarch64)
|
||||
"third_party/webview2/arm64/WebView2Loader.dll"
|
||||
else
|
||||
"third_party/webview2/x64/WebView2Loader.dll";
|
||||
}
|
||||
|
||||
/// `zig build run` executes the cached artifact, which has no installed
|
||||
/// WebView2Loader.dll beside it; the vendored loader's directory goes on
|
||||
/// the run step's PATH so the host's LoadLibrary resolves it in dev runs.
|
||||
fn addWebView2RuntimeRunFiles(b: *std.Build, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, web_engine: WebEngineOption, native_sdk_path: []const u8) void {
|
||||
if (web_engine != .system) return;
|
||||
if (target.result.os.tag != .windows) return;
|
||||
const loader_dir = std.fs.path.dirname(webView2LoaderSubPath(target)).?;
|
||||
run.addPathDir(b.pathFromRoot(b.pathJoin(&.{ native_sdk_path, loader_dir })));
|
||||
}
|
||||
|
||||
fn addCefRuntimeRunFiles(b: *std.Build, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, exe: *std.Build.Step.Compile, web_engine: WebEngineOption, cef_dir: []const u8) void {
|
||||
if (web_engine != .chromium) return;
|
||||
if (target.result.os.tag != .macos) return;
|
||||
const copy = b.addSystemCommand(&.{
|
||||
"sh", "-c",
|
||||
b.fmt(
|
||||
\\set -e
|
||||
\\exe="$0"
|
||||
\\exe_dir="$(dirname "$exe")"
|
||||
\\rm -rf "zig-out/Frameworks/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/Chromium Embedded Framework.framework" &&
|
||||
\\mkdir -p "zig-out/Frameworks" "zig-out/bin/Frameworks" ".zig-cache/o/Frameworks" "$exe_dir" &&
|
||||
\\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/Frameworks/" &&
|
||||
\\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/" &&
|
||||
\\cp -R "{s}/Release/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libEGL.dylib" "$exe_dir/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib" "$exe_dir/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib" "$exe_dir/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/vk_swiftshader_icd.json" "$exe_dir/"
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
});
|
||||
copy.addFileArg(exe.getEmittedBin());
|
||||
run.step.dependOn(©.step);
|
||||
}
|
||||
|
||||
fn addCefCheck(b: *std.Build, target: std.Build.ResolvedTarget, cef_dir: []const u8) *std.Build.Step.Run {
|
||||
const script = switch (target.result.os.tag) {
|
||||
.macos => b.fmt(
|
||||
\\test -f "{s}/include/cef_app.h" &&
|
||||
\\test -d "{s}/Release/Chromium Embedded Framework.framework" &&
|
||||
\\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{
|
||||
\\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
|
||||
\\ echo "Fix with: native cef install --dir {s}" >&2
|
||||
\\ exit 1
|
||||
\\}}
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
.linux => b.fmt(
|
||||
\\test -f "{s}/include/cef_app.h" &&
|
||||
\\test -f "{s}/Release/libcef.so" &&
|
||||
\\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{
|
||||
\\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
|
||||
\\ echo "Fix with: native cef install --dir {s}" >&2
|
||||
\\ exit 1
|
||||
\\}}
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
.windows => b.fmt(
|
||||
\\test -f "{s}/include/cef_app.h" &&
|
||||
\\test -f "{s}/Release/libcef.dll" &&
|
||||
\\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib" || {{
|
||||
\\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
|
||||
\\ echo "Fix with: native cef install --dir {s}" >&2
|
||||
\\ exit 1
|
||||
\\}}
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
else => "echo unsupported CEF target >&2; exit 1",
|
||||
};
|
||||
return b.addSystemCommand(&.{ "sh", "-c", script });
|
||||
}
|
||||
|
||||
const AppWebEngineConfig = struct {
|
||||
web_engine: WebEngineOption = .system,
|
||||
cef_dir: []const u8 = "third_party/cef/macos",
|
||||
cef_auto_install: bool = false,
|
||||
};
|
||||
|
||||
fn defaultCefDir(platform: PlatformOption, configured: []const u8) []const u8 {
|
||||
if (!std.mem.eql(u8, configured, "third_party/cef/macos")) return configured;
|
||||
return switch (platform) {
|
||||
.linux => "third_party/cef/linux",
|
||||
.windows => "third_party/cef/windows",
|
||||
else => configured,
|
||||
};
|
||||
}
|
||||
|
||||
fn appWebEngineConfig() AppWebEngineConfig {
|
||||
const source = @embedFile("app.zon");
|
||||
var config: AppWebEngineConfig = .{};
|
||||
if (stringField(source, ".web_engine")) |value| {
|
||||
config.web_engine = parseWebEngine(value) orelse .system;
|
||||
}
|
||||
if (objectSection(source, ".cef")) |cef| {
|
||||
if (stringField(cef, ".dir")) |value| config.cef_dir = value;
|
||||
if (boolField(cef, ".auto_install")) |value| config.cef_auto_install = value;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
fn parseWebEngine(value: []const u8) ?WebEngineOption {
|
||||
if (std.mem.eql(u8, value, "system")) return .system;
|
||||
if (std.mem.eql(u8, value, "chromium")) return .chromium;
|
||||
return null;
|
||||
}
|
||||
|
||||
fn stringField(source: []const u8, field: []const u8) ?[]const u8 {
|
||||
const field_index = std.mem.indexOf(u8, source, field) orelse return null;
|
||||
const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null;
|
||||
const start_quote = std.mem.indexOfScalarPos(u8, source, equals, '"') orelse return null;
|
||||
const end_quote = std.mem.indexOfScalarPos(u8, source, start_quote + 1, '"') orelse return null;
|
||||
return source[start_quote + 1 .. end_quote];
|
||||
}
|
||||
|
||||
fn objectSection(source: []const u8, field: []const u8) ?[]const u8 {
|
||||
const field_index = std.mem.indexOf(u8, source, field) orelse return null;
|
||||
const open = std.mem.indexOfScalarPos(u8, source, field_index, '{') orelse return null;
|
||||
var depth: usize = 0;
|
||||
var index = open;
|
||||
while (index < source.len) : (index += 1) {
|
||||
switch (source[index]) {
|
||||
'{' => depth += 1,
|
||||
'}' => {
|
||||
depth -= 1;
|
||||
if (depth == 0) return source[open + 1 .. index];
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn boolField(source: []const u8, field: []const u8) ?bool {
|
||||
const field_index = std.mem.indexOf(u8, source, field) orelse return null;
|
||||
const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null;
|
||||
var index = equals + 1;
|
||||
while (index < source.len and std.ascii.isWhitespace(source[index])) : (index += 1) {}
|
||||
if (std.mem.startsWith(u8, source[index..], "true")) return true;
|
||||
if (std.mem.startsWith(u8, source[index..], "false")) return false;
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.{
|
||||
.name = .command_app,
|
||||
.fingerprint = 0xb253836e291d354d,
|
||||
.version = "0.1.0",
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.dependencies = .{},
|
||||
.paths = .{ "build.zig", "build.zig.zon", "src", "assets", "app.zon", "README.md" },
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
const std = @import("std");
|
||||
const runner = @import("runner");
|
||||
const native_sdk = @import("native_sdk");
|
||||
|
||||
pub const panic = std.debug.FullPanic(native_sdk.debug.capturePanic);
|
||||
|
||||
const window_width: f32 = 860;
|
||||
const window_height: f32 = 560;
|
||||
const toolbar_height: f32 = 48;
|
||||
const statusbar_height: f32 = 34;
|
||||
const command_id = "app.sync";
|
||||
|
||||
const html =
|
||||
\\<!doctype html>
|
||||
\\<html>
|
||||
\\<head>
|
||||
\\ <meta charset="utf-8">
|
||||
\\ <meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
\\ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
|
||||
\\ <style>
|
||||
\\ :root { color-scheme: light dark; }
|
||||
\\ * { box-sizing: border-box; }
|
||||
\\ body {
|
||||
\\ margin: 0;
|
||||
\\ min-height: 100vh;
|
||||
\\ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", Segoe UI, system-ui, sans-serif;
|
||||
\\ background: #f7f8fa;
|
||||
\\ color: #171717;
|
||||
\\ }
|
||||
\\ main {
|
||||
\\ width: min(680px, calc(100vw - 48px));
|
||||
\\ padding: 42px 0;
|
||||
\\ margin: 0 auto;
|
||||
\\ display: grid;
|
||||
\\ gap: 18px;
|
||||
\\ }
|
||||
\\ h1 { margin: 0; font-size: 30px; line-height: 1.1; font-weight: 650; letter-spacing: 0; }
|
||||
\\ p { margin: 0; color: #606975; line-height: 1.55; }
|
||||
\\ .panel {
|
||||
\\ display: grid;
|
||||
\\ grid-template-columns: 1fr auto;
|
||||
\\ gap: 18px;
|
||||
\\ align-items: center;
|
||||
\\ padding: 18px 0;
|
||||
\\ border-top: 1px solid #e3e6ea;
|
||||
\\ border-bottom: 1px solid #e3e6ea;
|
||||
\\ }
|
||||
\\ button {
|
||||
\\ min-width: 116px;
|
||||
\\ border: 1px solid #171717;
|
||||
\\ border-radius: 7px;
|
||||
\\ padding: 9px 13px;
|
||||
\\ font: inherit;
|
||||
\\ font-weight: 590;
|
||||
\\ color: white;
|
||||
\\ background: #171717;
|
||||
\\ cursor: pointer;
|
||||
\\ }
|
||||
\\ pre {
|
||||
\\ min-height: 92px;
|
||||
\\ margin: 0;
|
||||
\\ padding: 14px 16px;
|
||||
\\ overflow: auto;
|
||||
\\ border: 1px solid #dde1e6;
|
||||
\\ border-radius: 7px;
|
||||
\\ background: white;
|
||||
\\ color: #374151;
|
||||
\\ font-size: 13px;
|
||||
\\ line-height: 1.45;
|
||||
\\ }
|
||||
\\ @media (prefers-color-scheme: dark) {
|
||||
\\ body { background: #111316; color: #f4f4f5; }
|
||||
\\ p { color: #a1a1aa; }
|
||||
\\ .panel { border-color: #2b2f37; }
|
||||
\\ button { color: #111316; background: #f4f4f5; border-color: #f4f4f5; }
|
||||
\\ pre { color: #d4d4d8; background: #171a20; border-color: #2b2f37; }
|
||||
\\ }
|
||||
\\ </style>
|
||||
\\</head>
|
||||
\\<body>
|
||||
\\ <main>
|
||||
\\ <h1>One command, five entry points</h1>
|
||||
\\ <p>The toolbar button, View menu, tray item, primary shortcut, and WebView button all dispatch app.sync into the same Zig command handler.</p>
|
||||
\\ <div class="panel">
|
||||
\\ <p>Dispatch from the WebView through the built-in command bridge.</p>
|
||||
\\ <button id="sync" type="button">Sync</button>
|
||||
\\ <button id="commands" type="button">List Commands</button>
|
||||
\\ </div>
|
||||
\\ <pre id="output">Ready.</pre>
|
||||
\\ </main>
|
||||
\\ <script>
|
||||
\\ const output = document.querySelector("#output");
|
||||
\\ const show = (value) => { output.textContent = JSON.stringify(value, null, 2); };
|
||||
\\ const fail = (error) => { output.textContent = `${error.code || "error"}: ${error.message}`; };
|
||||
\\ const invokeCommand = (name) => {
|
||||
\\ if (window.zero && window.zero.commands && window.zero.commands.invoke) {
|
||||
\\ return window.zero.commands.invoke(name);
|
||||
\\ }
|
||||
\\ return window.zero.invoke("native-sdk.command.invoke", { name });
|
||||
\\ };
|
||||
\\ const listCommands = () => {
|
||||
\\ if (window.zero && window.zero.commands && window.zero.commands.list) {
|
||||
\\ return window.zero.commands.list();
|
||||
\\ }
|
||||
\\ return window.zero.invoke("native-sdk.command.list", {});
|
||||
\\ };
|
||||
\\ document.querySelector("#sync").addEventListener("click", async () => {
|
||||
\\ try { show(await invokeCommand("app.sync")); } catch (error) { fail(error); }
|
||||
\\ });
|
||||
\\ document.querySelector("#commands").addEventListener("click", async () => {
|
||||
\\ try { show(await listCommands()); } catch (error) { fail(error); }
|
||||
\\ });
|
||||
\\ </script>
|
||||
\\</body>
|
||||
\\</html>
|
||||
;
|
||||
|
||||
const app_permissions = [_][]const u8{native_sdk.security.permission_command};
|
||||
const bridge_origins = [_][]const u8{ "zero://inline", "zero://app" };
|
||||
const command_permission = [_][]const u8{native_sdk.security.permission_command};
|
||||
const command_catalog = [_]native_sdk.Command{.{ .id = command_id, .title = "Sync" }};
|
||||
const builtin_policies = [_]native_sdk.BridgeCommandPolicy{
|
||||
.{ .name = "native-sdk.command.invoke", .permissions = &command_permission, .origins = &bridge_origins },
|
||||
.{ .name = "native-sdk.command.list", .permissions = &command_permission, .origins = &bridge_origins },
|
||||
};
|
||||
const tray_items = [_]native_sdk.TrayMenuItem{
|
||||
.{ .id = 1, .label = "Sync", .command = command_id },
|
||||
};
|
||||
const shell_views = [_]native_sdk.ShellView{
|
||||
.{ .label = "toolbar", .kind = .toolbar, .edge = .top, .height = toolbar_height, .layer = 20, .role = "Toolbar" },
|
||||
.{ .label = "sync-button", .kind = .button, .parent = "toolbar", .x = 12, .y = 9, .width = 92, .height = 30, .layer = 21, .accessibility_label = "Sync now", .text = "Sync", .command = command_id },
|
||||
.{ .label = "main", .kind = .webview, .url = "zero://inline", .fill = true },
|
||||
.{ .label = "statusbar", .kind = .statusbar, .edge = .bottom, .height = statusbar_height, .layer = 20, .role = "Status" },
|
||||
.{ .label = "status-label", .kind = .label, .parent = "statusbar", .x = 14, .y = 8, .width = 620, .height = 18, .layer = 21, .text = "Ready. Use the toolbar, menu, tray, shortcut, or WebView button." },
|
||||
};
|
||||
const shell_windows = [_]native_sdk.ShellWindow{.{
|
||||
.label = "main",
|
||||
.title = "Native SDK Command App",
|
||||
.width = window_width,
|
||||
.height = window_height,
|
||||
.views = &shell_views,
|
||||
}};
|
||||
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
|
||||
|
||||
const CommandApp = struct {
|
||||
command_count: u32 = 0,
|
||||
sources: [8]native_sdk.CommandSource = [_]native_sdk.CommandSource{.runtime} ** 8,
|
||||
last_command_name: []const u8 = "",
|
||||
|
||||
fn app(self: *@This()) native_sdk.App {
|
||||
return .{
|
||||
.context = self,
|
||||
.name = "command-app",
|
||||
.source = native_sdk.WebViewSource.html(html),
|
||||
.scene_fn = scene,
|
||||
.start_fn = start,
|
||||
.event_fn = event,
|
||||
};
|
||||
}
|
||||
|
||||
fn scene(context: *anyopaque) anyerror!native_sdk.ShellConfig {
|
||||
_ = context;
|
||||
return shell_scene;
|
||||
}
|
||||
|
||||
fn start(context: *anyopaque, runtime: *native_sdk.Runtime) anyerror!void {
|
||||
_ = context;
|
||||
try runtime.createTray(.{
|
||||
.tooltip = "Native SDK Command App",
|
||||
.items = &tray_items,
|
||||
});
|
||||
}
|
||||
|
||||
fn event(context: *anyopaque, runtime: *native_sdk.Runtime, event_value: native_sdk.Event) anyerror!void {
|
||||
const self: *@This() = @ptrCast(@alignCast(context));
|
||||
switch (event_value) {
|
||||
.command => |command| {
|
||||
if (std.mem.eql(u8, command.name, command_id)) {
|
||||
try self.handleCommand(runtime, command);
|
||||
}
|
||||
},
|
||||
.appearance_changed, .shortcut, .timer, .effects_wake, .audio, .files_dropped, .gpu_surface_frame, .gpu_surface_resized, .gpu_surface_input, .canvas_widget_pointer, .canvas_widget_keyboard, .canvas_widget_scroll, .canvas_widget_file_drop, .canvas_widget_drag, .canvas_widget_context_menu, .canvas_widget_context_menu_request, .canvas_widget_dismiss, .canvas_widget_context_press, .canvas_widget_resize, .canvas_widget_change, .window_closed, .automation_provenance, .lifecycle => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn handleCommand(self: *@This(), runtime: *native_sdk.Runtime, command: native_sdk.CommandEvent) anyerror!void {
|
||||
if (self.command_count < self.sources.len) {
|
||||
self.sources[self.command_count] = command.source;
|
||||
}
|
||||
self.command_count += 1;
|
||||
self.last_command_name = command.name;
|
||||
|
||||
var status_buffer: [128]u8 = undefined;
|
||||
const status = try std.fmt.bufPrint(
|
||||
&status_buffer,
|
||||
"Handled {s} from {s}. Count {d}.",
|
||||
.{ command.name, @tagName(command.source), self.command_count },
|
||||
);
|
||||
const status_window_id = if (command.window_id == 0) 1 else command.window_id;
|
||||
_ = try runtime.updateView(status_window_id, "status-label", .{ .text = status });
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main(init: std.process.Init) !void {
|
||||
var app = CommandApp{};
|
||||
try runner.runWithOptions(app.app(), .{
|
||||
.app_name = "command-app",
|
||||
.window_title = "Native SDK Command App",
|
||||
.bundle_id = "dev.native_sdk.command_app",
|
||||
.default_frame = native_sdk.geometry.RectF.init(0, 0, window_width, window_height),
|
||||
.builtin_bridge = .{ .enabled = true, .commands = &builtin_policies },
|
||||
.js_window_api = true,
|
||||
.security = .{
|
||||
.permissions = &app_permissions,
|
||||
.navigation = .{ .allowed_origins = &bridge_origins },
|
||||
},
|
||||
}, init);
|
||||
}
|
||||
|
||||
test "command app routes toolbar menu tray shortcut and bridge commands" {
|
||||
const harness = try native_sdk.TestHarness().create(std.testing.allocator, .{ .size = native_sdk.geometry.SizeF.init(window_width, window_height) });
|
||||
defer harness.destroy(std.testing.allocator);
|
||||
harness.runtime.options.builtin_bridge = .{ .enabled = true, .commands = &builtin_policies };
|
||||
harness.runtime.options.js_window_api = true;
|
||||
harness.runtime.options.commands = &command_catalog;
|
||||
harness.runtime.options.security = .{
|
||||
.permissions = &app_permissions,
|
||||
.navigation = .{ .allowed_origins = &bridge_origins },
|
||||
};
|
||||
|
||||
var app = CommandApp{};
|
||||
try harness.start(app.app());
|
||||
|
||||
try harness.runtime.dispatchPlatformEvent(app.app(), .{ .native_command = .{
|
||||
.name = command_id,
|
||||
.window_id = 1,
|
||||
.view_label = "sync-button",
|
||||
} });
|
||||
try harness.runtime.dispatchPlatformEvent(app.app(), .{ .menu_command = .{
|
||||
.name = command_id,
|
||||
.window_id = 1,
|
||||
} });
|
||||
try std.testing.expectEqual(@as(usize, 1), harness.null_platform.trayCreateCount());
|
||||
try harness.runtime.dispatchPlatformEvent(app.app(), .{ .tray_action = 1 });
|
||||
try harness.runtime.dispatchPlatformEvent(app.app(), .{ .shortcut = .{
|
||||
.id = command_id,
|
||||
.key = "s",
|
||||
.window_id = 1,
|
||||
.modifiers = .{ .primary = true },
|
||||
} });
|
||||
try harness.runtime.dispatchPlatformEvent(app.app(), .{ .bridge_message = .{
|
||||
.bytes = "{\"id\":\"1\",\"command\":\"native-sdk.command.invoke\",\"payload\":{\"name\":\"app.sync\"}}",
|
||||
.origin = "zero://inline",
|
||||
.window_id = 1,
|
||||
.webview_label = "main",
|
||||
} });
|
||||
try harness.runtime.dispatchPlatformEvent(app.app(), .{ .bridge_message = .{
|
||||
.bytes = "{\"id\":\"2\",\"command\":\"native-sdk.command.list\",\"payload\":{}}",
|
||||
.origin = "zero://inline",
|
||||
.window_id = 1,
|
||||
.webview_label = "main",
|
||||
} });
|
||||
|
||||
try std.testing.expectEqual(@as(u32, 5), app.command_count);
|
||||
try std.testing.expectEqualStrings(command_id, app.last_command_name);
|
||||
try std.testing.expectEqual(native_sdk.CommandSource.toolbar, app.sources[0]);
|
||||
try std.testing.expectEqual(native_sdk.CommandSource.menu, app.sources[1]);
|
||||
try std.testing.expectEqual(native_sdk.CommandSource.tray, app.sources[2]);
|
||||
try std.testing.expectEqual(native_sdk.CommandSource.shortcut, app.sources[3]);
|
||||
try std.testing.expectEqual(native_sdk.CommandSource.bridge, app.sources[4]);
|
||||
try std.testing.expect(std.mem.indexOf(u8, harness.null_platform.lastBridgeResponse(), "\"ok\":true") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, harness.null_platform.lastBridgeResponse(), "\"id\":\"app.sync\"") != null);
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const native_sdk = @import("native_sdk");
|
||||
const app_manifest = @import("app_manifest_zon");
|
||||
const manifest_commands = if (@hasField(@TypeOf(app_manifest), "commands")) app_manifest.commands else .{};
|
||||
const manifest_shortcuts = if (@hasField(@TypeOf(app_manifest), "shortcuts")) app_manifest.shortcuts else .{};
|
||||
const manifest_menus = if (@hasField(@TypeOf(app_manifest), "menus")) app_manifest.menus else .{};
|
||||
const manifest_windows = if (@hasField(@TypeOf(app_manifest), "windows")) app_manifest.windows else .{};
|
||||
|
||||
pub const StdoutTraceSink = struct {
|
||||
pub fn sink(self: *StdoutTraceSink) native_sdk.trace.Sink {
|
||||
return .{ .context = self, .write_fn = write };
|
||||
}
|
||||
|
||||
fn write(context: *anyopaque, record: native_sdk.trace.Record) native_sdk.trace.WriteError!void {
|
||||
_ = context;
|
||||
if (!shouldTrace(record)) return;
|
||||
// Never fail on an oversized record: logging failures must
|
||||
// degrade (truncated output), not fail dispatch upstream.
|
||||
var buffer: [4096]u8 = undefined;
|
||||
std.debug.print("{s}\n", .{native_sdk.trace.formatTextBounded(record, &buffer)});
|
||||
}
|
||||
};
|
||||
|
||||
pub const RunOptions = struct {
|
||||
app_name: []const u8,
|
||||
window_title: []const u8 = "",
|
||||
bundle_id: []const u8,
|
||||
icon_path: []const u8 = "assets/icon.png",
|
||||
default_frame: native_sdk.geometry.RectF = native_sdk.geometry.RectF.init(0, 0, 1100, 760),
|
||||
bridge: ?native_sdk.BridgeDispatcher = null,
|
||||
builtin_bridge: native_sdk.BridgePolicy = .{},
|
||||
js_window_api: bool = false,
|
||||
security: native_sdk.SecurityPolicy = .{},
|
||||
commands: ?[]const native_sdk.Command = null,
|
||||
menus: ?[]const native_sdk.Menu = null,
|
||||
shortcuts: ?[]const native_sdk.Shortcut = null,
|
||||
|
||||
fn appInfo(self: RunOptions, buffers: *StateBuffers) native_sdk.AppInfo {
|
||||
var info: native_sdk.AppInfo = .{
|
||||
.app_name = self.app_name,
|
||||
// The identity the OS shows (application menu, Dock, About
|
||||
// panel) reads straight from app.zon at comptime, so dev
|
||||
// runs carry the same display name and version a packaged
|
||||
// bundle gets from its Info.plist.
|
||||
.display_name = manifestStringField("display_name"),
|
||||
.version = manifestStringField("version"),
|
||||
.description = manifestStringField("description"),
|
||||
.has_web_content = manifestHasWebContent(),
|
||||
.window_title = self.window_title,
|
||||
.bundle_id = self.bundle_id,
|
||||
.icon_path = self.icon_path,
|
||||
.main_window = .{
|
||||
.id = 1,
|
||||
.label = "main",
|
||||
.title = self.window_title,
|
||||
.default_frame = self.default_frame,
|
||||
},
|
||||
};
|
||||
const windows = manifestWindowOptions(buffers);
|
||||
if (windows.len > 0) {
|
||||
info.main_window = windows[0];
|
||||
info.windows = windows;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
fn resolvedShortcuts(self: RunOptions, storage: *ShortcutStorage) []const native_sdk.Shortcut {
|
||||
return self.shortcuts orelse storage.fromManifest();
|
||||
}
|
||||
|
||||
fn resolvedCommands(self: RunOptions, storage: *CommandStorage) []const native_sdk.Command {
|
||||
return self.commands orelse storage.fromManifest();
|
||||
}
|
||||
|
||||
fn resolvedMenus(self: RunOptions, storage: *MenuStorage) []const native_sdk.Menu {
|
||||
return self.menus orelse storage.fromManifest();
|
||||
}
|
||||
};
|
||||
|
||||
const CommandStorage = struct {
|
||||
commands: [native_sdk.app_manifest.max_commands]native_sdk.Command = undefined,
|
||||
|
||||
fn fromManifest(self: *CommandStorage) []const native_sdk.Command {
|
||||
comptime {
|
||||
if (manifest_commands.len > native_sdk.app_manifest.max_commands) {
|
||||
@compileError("app.zon defines too many commands");
|
||||
}
|
||||
}
|
||||
|
||||
inline for (manifest_commands, 0..) |command, index| {
|
||||
self.commands[index] = .{
|
||||
.id = command.id,
|
||||
.title = if (@hasField(@TypeOf(command), "title")) command.title else "",
|
||||
.enabled = if (@hasField(@TypeOf(command), "enabled")) command.enabled else true,
|
||||
.checked = if (@hasField(@TypeOf(command), "checked")) command.checked else false,
|
||||
};
|
||||
}
|
||||
return self.commands[0..manifest_commands.len];
|
||||
}
|
||||
};
|
||||
|
||||
const MenuStorage = struct {
|
||||
menus: [native_sdk.platform.max_menus]native_sdk.Menu = undefined,
|
||||
items: [native_sdk.platform.max_menu_items]native_sdk.MenuItem = undefined,
|
||||
|
||||
fn fromManifest(self: *MenuStorage) []const native_sdk.Menu {
|
||||
comptime {
|
||||
if (manifest_menus.len > native_sdk.platform.max_menus) {
|
||||
@compileError("app.zon defines too many menus");
|
||||
}
|
||||
var item_count: usize = 0;
|
||||
for (manifest_menus) |menu| {
|
||||
const items = if (@hasField(@TypeOf(menu), "items")) menu.items else .{};
|
||||
item_count += items.len;
|
||||
}
|
||||
if (item_count > native_sdk.platform.max_menu_items) {
|
||||
@compileError("app.zon defines too many menu items");
|
||||
}
|
||||
}
|
||||
|
||||
var item_index: usize = 0;
|
||||
inline for (manifest_menus, 0..) |menu, menu_index| {
|
||||
const items = if (@hasField(@TypeOf(menu), "items")) menu.items else .{};
|
||||
const first_item = item_index;
|
||||
inline for (items) |item| {
|
||||
self.items[item_index] = menuItem(item);
|
||||
item_index += 1;
|
||||
}
|
||||
self.menus[menu_index] = .{
|
||||
.title = menu.title,
|
||||
.items = self.items[first_item..item_index],
|
||||
};
|
||||
}
|
||||
return self.menus[0..manifest_menus.len];
|
||||
}
|
||||
};
|
||||
|
||||
const ShortcutStorage = struct {
|
||||
shortcuts: [native_sdk.platform.max_shortcuts]native_sdk.Shortcut = undefined,
|
||||
|
||||
fn fromManifest(self: *ShortcutStorage) []const native_sdk.Shortcut {
|
||||
comptime {
|
||||
if (manifest_shortcuts.len > native_sdk.platform.max_shortcuts) {
|
||||
@compileError("app.zon defines too many shortcuts");
|
||||
}
|
||||
}
|
||||
|
||||
inline for (manifest_shortcuts, 0..) |shortcut, index| {
|
||||
self.shortcuts[index] = .{
|
||||
.id = shortcut.id,
|
||||
.key = shortcut.key,
|
||||
.modifiers = shortcutModifiers(shortcut),
|
||||
};
|
||||
}
|
||||
return self.shortcuts[0..manifest_shortcuts.len];
|
||||
}
|
||||
};
|
||||
|
||||
/// A top-level app.zon string field (`display_name`, `version`,
|
||||
/// `description`), or "" when the manifest omits it — optional identity
|
||||
/// stays optional all the way into `AppInfo`.
|
||||
fn manifestStringField(comptime field: []const u8) []const u8 {
|
||||
if (comptime !@hasField(@TypeOf(app_manifest), field)) return "";
|
||||
const value = @field(app_manifest, field);
|
||||
if (comptime @TypeOf(value) == @TypeOf(null)) return "";
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Whether app.zon declares web content: the `webview` capability or a
|
||||
/// `frontend` block. Hosts build honest default menus from this — web
|
||||
/// items like Reload only exist when a webview can answer them, so
|
||||
/// canvas-only apps never ship dead menu items.
|
||||
fn manifestHasWebContent() bool {
|
||||
if (comptime @hasField(@TypeOf(app_manifest), "frontend")) return true;
|
||||
if (comptime !@hasField(@TypeOf(app_manifest), "capabilities")) return false;
|
||||
inline for (app_manifest.capabilities) |capability| {
|
||||
if (comptime std.mem.eql(u8, capability, "webview")) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn manifestWindowOptions(buffers: *StateBuffers) []const native_sdk.WindowOptions {
|
||||
comptime {
|
||||
if (manifest_windows.len > native_sdk.platform.max_windows) {
|
||||
@compileError("app.zon defines too many windows");
|
||||
}
|
||||
}
|
||||
|
||||
inline for (manifest_windows, 0..) |window, index| {
|
||||
buffers.restored_windows[index] = manifestWindow(window, index);
|
||||
}
|
||||
return buffers.restored_windows[0..manifest_windows.len];
|
||||
}
|
||||
|
||||
fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.WindowOptions {
|
||||
return .{
|
||||
.id = index + 1,
|
||||
.label = windowLabel(window, index),
|
||||
.title = windowTitle(window),
|
||||
.default_frame = native_sdk.geometry.RectF.init(
|
||||
windowFloat(window, "x", 0),
|
||||
windowFloat(window, "y", 0),
|
||||
windowFloat(window, "width", 720),
|
||||
windowFloat(window, "height", 480),
|
||||
),
|
||||
.resizable = windowBool(window, "resizable", true),
|
||||
.restore_state = windowBool(window, "restore_state", true),
|
||||
.restore_policy = windowRestorePolicy(window),
|
||||
};
|
||||
}
|
||||
|
||||
fn windowLabel(comptime window: anytype, comptime index: usize) []const u8 {
|
||||
if (comptime @hasField(@TypeOf(window), "label")) return window.label;
|
||||
return if (index == 0) "main" else "window";
|
||||
}
|
||||
|
||||
fn windowTitle(comptime window: anytype) []const u8 {
|
||||
if (comptime !@hasField(@TypeOf(window), "title")) return "";
|
||||
const title = window.title;
|
||||
if (comptime @TypeOf(title) == @TypeOf(null)) return "";
|
||||
return title;
|
||||
}
|
||||
|
||||
fn windowFloat(comptime window: anytype, comptime field: []const u8, comptime default_value: f32) f32 {
|
||||
if (comptime @hasField(@TypeOf(window), field)) return @field(window, field);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
fn windowBool(comptime window: anytype, comptime field: []const u8, comptime default_value: bool) bool {
|
||||
if (comptime @hasField(@TypeOf(window), field)) return @field(window, field);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
fn windowRestorePolicy(comptime window: anytype) native_sdk.WindowRestorePolicy {
|
||||
if (comptime !@hasField(@TypeOf(window), "restore_policy")) return .clamp_to_visible_screen;
|
||||
const value = window.restore_policy;
|
||||
if (comptime std.mem.eql(u8, value, "clamp_to_visible_screen")) return .clamp_to_visible_screen;
|
||||
if (comptime std.mem.eql(u8, value, "center_on_primary")) return .center_on_primary;
|
||||
@compileError("unknown app.zon window restore_policy");
|
||||
}
|
||||
|
||||
fn menuItem(comptime item: anytype) native_sdk.MenuItem {
|
||||
return .{
|
||||
.label = if (@hasField(@TypeOf(item), "label")) item.label else "",
|
||||
.command = if (@hasField(@TypeOf(item), "command")) item.command else "",
|
||||
.key = if (@hasField(@TypeOf(item), "key")) item.key else "",
|
||||
.modifiers = shortcutModifiers(item),
|
||||
.separator = if (@hasField(@TypeOf(item), "separator")) item.separator else false,
|
||||
.enabled = if (@hasField(@TypeOf(item), "enabled")) item.enabled else true,
|
||||
.checked = if (@hasField(@TypeOf(item), "checked")) item.checked else false,
|
||||
};
|
||||
}
|
||||
|
||||
fn shortcutModifiers(comptime shortcut: anytype) native_sdk.ShortcutModifiers {
|
||||
const values = if (@hasField(@TypeOf(shortcut), "modifiers")) shortcut.modifiers else .{};
|
||||
var modifiers: native_sdk.ShortcutModifiers = .{};
|
||||
inline for (values) |value| {
|
||||
const modifier: []const u8 = value;
|
||||
if (comptime std.mem.eql(u8, modifier, "primary")) {
|
||||
modifiers.primary = true;
|
||||
} else if (comptime std.mem.eql(u8, modifier, "command")) {
|
||||
modifiers.command = true;
|
||||
} else if (comptime std.mem.eql(u8, modifier, "control")) {
|
||||
modifiers.control = true;
|
||||
} else if (comptime std.mem.eql(u8, modifier, "option") or std.mem.eql(u8, modifier, "alt")) {
|
||||
modifiers.option = true;
|
||||
} else if (comptime std.mem.eql(u8, modifier, "shift")) {
|
||||
modifiers.shift = true;
|
||||
} else {
|
||||
@compileError("unknown app.zon shortcut modifier");
|
||||
}
|
||||
}
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
pub fn runWithOptions(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
|
||||
if (build_options.debug_overlay) {
|
||||
std.debug.print("debug-overlay=true backend={s} web-engine={s} trace={s}\n", .{ build_options.platform, build_options.web_engine, build_options.trace });
|
||||
}
|
||||
if (comptime std.mem.eql(u8, build_options.platform, "macos")) {
|
||||
try runMacos(app, options, init);
|
||||
} else if (comptime std.mem.eql(u8, build_options.platform, "linux")) {
|
||||
try runLinux(app, options, init);
|
||||
} else if (comptime std.mem.eql(u8, build_options.platform, "windows")) {
|
||||
try runWindows(app, options, init);
|
||||
} else {
|
||||
try runNull(app, options, init);
|
||||
}
|
||||
}
|
||||
|
||||
fn runNull(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
|
||||
var buffers: StateBuffers = undefined;
|
||||
var app_info = options.appInfo(&buffers);
|
||||
const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
|
||||
var null_platform = native_sdk.NullPlatform.initWithOptions(.{}, webEngine(), app_info);
|
||||
var trace_sink = StdoutTraceSink{};
|
||||
var log_buffers: native_sdk.debug.LogPathBuffers = .{};
|
||||
const log_setup = native_sdk.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null;
|
||||
if (log_setup) |setup| native_sdk.debug.installPanicCapture(init.io, setup.paths);
|
||||
var file_trace_sink: native_sdk.debug.FileTraceSink = undefined;
|
||||
var fanout_sinks: [2]native_sdk.trace.Sink = undefined;
|
||||
var fanout_sink: native_sdk.debug.FanoutTraceSink = undefined;
|
||||
var runtime_trace_sink = trace_sink.sink();
|
||||
if (log_setup) |setup| {
|
||||
file_trace_sink = native_sdk.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format);
|
||||
fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() };
|
||||
fanout_sink = .{ .sinks = &fanout_sinks };
|
||||
runtime_trace_sink = fanout_sink.sink();
|
||||
}
|
||||
var shortcut_storage: ShortcutStorage = .{};
|
||||
const shortcuts = options.resolvedShortcuts(&shortcut_storage);
|
||||
var menu_storage: MenuStorage = .{};
|
||||
const menus = options.resolvedMenus(&menu_storage);
|
||||
var command_storage: CommandStorage = .{};
|
||||
const commands = options.resolvedCommands(&command_storage);
|
||||
// The Runtime is tens of megabytes; construct on the heap (default
|
||||
// main-thread stacks overflow on a stack instance).
|
||||
const runtime = try std.heap.page_allocator.create(native_sdk.Runtime);
|
||||
defer std.heap.page_allocator.destroy(runtime);
|
||||
native_sdk.Runtime.initAt(runtime, .{
|
||||
.platform = null_platform.platform(),
|
||||
.trace_sink = runtime_trace_sink,
|
||||
.log_path = if (log_setup) |setup| setup.paths.log_file else null,
|
||||
.bridge = options.bridge,
|
||||
.builtin_bridge = options.builtin_bridge,
|
||||
.js_window_api = options.js_window_api,
|
||||
.security = options.security,
|
||||
.commands = commands,
|
||||
.menus = menus,
|
||||
.shortcuts = shortcuts,
|
||||
.automation = if (build_options.automation) native_sdk.automation.Server.init(init.io, ".zig-cache/native-sdk-automation", app_info.resolvedWindowTitle()) else null,
|
||||
.window_state_store = store,
|
||||
});
|
||||
|
||||
try runtime.run(app);
|
||||
}
|
||||
|
||||
fn runMacos(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
|
||||
var buffers: StateBuffers = undefined;
|
||||
var app_info = options.appInfo(&buffers);
|
||||
const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
|
||||
var mac_platform = try native_sdk.platform.macos.MacPlatform.initWithOptions(native_sdk.geometry.SizeF.init(720, 480), webEngine(), app_info);
|
||||
defer mac_platform.deinit();
|
||||
var trace_sink = StdoutTraceSink{};
|
||||
var log_buffers: native_sdk.debug.LogPathBuffers = .{};
|
||||
const log_setup = native_sdk.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null;
|
||||
if (log_setup) |setup| native_sdk.debug.installPanicCapture(init.io, setup.paths);
|
||||
var file_trace_sink: native_sdk.debug.FileTraceSink = undefined;
|
||||
var fanout_sinks: [2]native_sdk.trace.Sink = undefined;
|
||||
var fanout_sink: native_sdk.debug.FanoutTraceSink = undefined;
|
||||
var runtime_trace_sink = trace_sink.sink();
|
||||
if (log_setup) |setup| {
|
||||
file_trace_sink = native_sdk.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format);
|
||||
fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() };
|
||||
fanout_sink = .{ .sinks = &fanout_sinks };
|
||||
runtime_trace_sink = fanout_sink.sink();
|
||||
}
|
||||
var shortcut_storage: ShortcutStorage = .{};
|
||||
const shortcuts = options.resolvedShortcuts(&shortcut_storage);
|
||||
var menu_storage: MenuStorage = .{};
|
||||
const menus = options.resolvedMenus(&menu_storage);
|
||||
var command_storage: CommandStorage = .{};
|
||||
const commands = options.resolvedCommands(&command_storage);
|
||||
// The Runtime is tens of megabytes; construct on the heap (default
|
||||
// main-thread stacks overflow on a stack instance).
|
||||
const runtime = try std.heap.page_allocator.create(native_sdk.Runtime);
|
||||
defer std.heap.page_allocator.destroy(runtime);
|
||||
native_sdk.Runtime.initAt(runtime, .{
|
||||
.platform = mac_platform.platform(),
|
||||
.trace_sink = runtime_trace_sink,
|
||||
.log_path = if (log_setup) |setup| setup.paths.log_file else null,
|
||||
.bridge = options.bridge,
|
||||
.builtin_bridge = options.builtin_bridge,
|
||||
.js_window_api = options.js_window_api,
|
||||
.security = options.security,
|
||||
.commands = commands,
|
||||
.menus = menus,
|
||||
.shortcuts = shortcuts,
|
||||
.automation = if (build_options.automation) native_sdk.automation.Server.init(init.io, ".zig-cache/native-sdk-automation", app_info.resolvedWindowTitle()) else null,
|
||||
.window_state_store = store,
|
||||
});
|
||||
|
||||
try runtime.run(app);
|
||||
}
|
||||
|
||||
fn runLinux(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
|
||||
var buffers: StateBuffers = undefined;
|
||||
var app_info = options.appInfo(&buffers);
|
||||
const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
|
||||
var linux_platform = try native_sdk.platform.linux.LinuxPlatform.initWithOptions(native_sdk.geometry.SizeF.init(720, 480), webEngine(), app_info);
|
||||
defer linux_platform.deinit();
|
||||
var trace_sink = StdoutTraceSink{};
|
||||
var log_buffers: native_sdk.debug.LogPathBuffers = .{};
|
||||
const log_setup = native_sdk.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null;
|
||||
if (log_setup) |setup| native_sdk.debug.installPanicCapture(init.io, setup.paths);
|
||||
var file_trace_sink: native_sdk.debug.FileTraceSink = undefined;
|
||||
var fanout_sinks: [2]native_sdk.trace.Sink = undefined;
|
||||
var fanout_sink: native_sdk.debug.FanoutTraceSink = undefined;
|
||||
var runtime_trace_sink = trace_sink.sink();
|
||||
if (log_setup) |setup| {
|
||||
file_trace_sink = native_sdk.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format);
|
||||
fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() };
|
||||
fanout_sink = .{ .sinks = &fanout_sinks };
|
||||
runtime_trace_sink = fanout_sink.sink();
|
||||
}
|
||||
var shortcut_storage: ShortcutStorage = .{};
|
||||
const shortcuts = options.resolvedShortcuts(&shortcut_storage);
|
||||
var menu_storage: MenuStorage = .{};
|
||||
const menus = options.resolvedMenus(&menu_storage);
|
||||
var command_storage: CommandStorage = .{};
|
||||
const commands = options.resolvedCommands(&command_storage);
|
||||
// The Runtime is tens of megabytes; construct on the heap (default
|
||||
// main-thread stacks overflow on a stack instance).
|
||||
const runtime = try std.heap.page_allocator.create(native_sdk.Runtime);
|
||||
defer std.heap.page_allocator.destroy(runtime);
|
||||
native_sdk.Runtime.initAt(runtime, .{
|
||||
.platform = linux_platform.platform(),
|
||||
.trace_sink = runtime_trace_sink,
|
||||
.log_path = if (log_setup) |setup| setup.paths.log_file else null,
|
||||
.bridge = options.bridge,
|
||||
.builtin_bridge = options.builtin_bridge,
|
||||
.js_window_api = options.js_window_api,
|
||||
.security = options.security,
|
||||
.commands = commands,
|
||||
.menus = menus,
|
||||
.shortcuts = shortcuts,
|
||||
.automation = if (build_options.automation) native_sdk.automation.Server.init(init.io, ".zig-cache/native-sdk-automation", app_info.resolvedWindowTitle()) else null,
|
||||
.window_state_store = store,
|
||||
});
|
||||
|
||||
try runtime.run(app);
|
||||
}
|
||||
|
||||
fn runWindows(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
|
||||
var buffers: StateBuffers = undefined;
|
||||
var app_info = options.appInfo(&buffers);
|
||||
const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
|
||||
var windows_platform = try native_sdk.platform.windows.WindowsPlatform.initWithOptions(native_sdk.geometry.SizeF.init(720, 480), webEngine(), app_info);
|
||||
defer windows_platform.deinit();
|
||||
var trace_sink = StdoutTraceSink{};
|
||||
var log_buffers: native_sdk.debug.LogPathBuffers = .{};
|
||||
const log_setup = native_sdk.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null;
|
||||
if (log_setup) |setup| native_sdk.debug.installPanicCapture(init.io, setup.paths);
|
||||
var file_trace_sink: native_sdk.debug.FileTraceSink = undefined;
|
||||
var fanout_sinks: [2]native_sdk.trace.Sink = undefined;
|
||||
var fanout_sink: native_sdk.debug.FanoutTraceSink = undefined;
|
||||
var runtime_trace_sink = trace_sink.sink();
|
||||
if (log_setup) |setup| {
|
||||
file_trace_sink = native_sdk.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format);
|
||||
fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() };
|
||||
fanout_sink = .{ .sinks = &fanout_sinks };
|
||||
runtime_trace_sink = fanout_sink.sink();
|
||||
}
|
||||
var shortcut_storage: ShortcutStorage = .{};
|
||||
const shortcuts = options.resolvedShortcuts(&shortcut_storage);
|
||||
var menu_storage: MenuStorage = .{};
|
||||
const menus = options.resolvedMenus(&menu_storage);
|
||||
var command_storage: CommandStorage = .{};
|
||||
const commands = options.resolvedCommands(&command_storage);
|
||||
// The Runtime is tens of megabytes; construct on the heap (default
|
||||
// main-thread stacks overflow on a stack instance).
|
||||
const runtime = try std.heap.page_allocator.create(native_sdk.Runtime);
|
||||
defer std.heap.page_allocator.destroy(runtime);
|
||||
native_sdk.Runtime.initAt(runtime, .{
|
||||
.platform = windows_platform.platform(),
|
||||
.trace_sink = runtime_trace_sink,
|
||||
.log_path = if (log_setup) |setup| setup.paths.log_file else null,
|
||||
.bridge = options.bridge,
|
||||
.builtin_bridge = options.builtin_bridge,
|
||||
.js_window_api = options.js_window_api,
|
||||
.security = options.security,
|
||||
.commands = commands,
|
||||
.menus = menus,
|
||||
.shortcuts = shortcuts,
|
||||
.automation = if (build_options.automation) native_sdk.automation.Server.init(init.io, ".zig-cache/native-sdk-automation", app_info.resolvedWindowTitle()) else null,
|
||||
.window_state_store = store,
|
||||
});
|
||||
|
||||
try runtime.run(app);
|
||||
}
|
||||
|
||||
fn shouldTrace(record: native_sdk.trace.Record) bool {
|
||||
if (comptime std.mem.eql(u8, build_options.trace, "off")) return false;
|
||||
if (comptime std.mem.eql(u8, build_options.trace, "all")) return true;
|
||||
if (comptime std.mem.eql(u8, build_options.trace, "events")) return true;
|
||||
return std.mem.indexOf(u8, record.name, build_options.trace) != null;
|
||||
}
|
||||
|
||||
fn webEngine() native_sdk.WebEngine {
|
||||
if (comptime std.mem.eql(u8, build_options.web_engine, "chromium")) return .chromium;
|
||||
return .system;
|
||||
}
|
||||
|
||||
const StateBuffers = struct {
|
||||
state_dir: [1024]u8 = undefined,
|
||||
file_path: [1200]u8 = undefined,
|
||||
read: [8192]u8 = undefined,
|
||||
restored_windows: [native_sdk.platform.max_windows]native_sdk.WindowOptions = undefined,
|
||||
};
|
||||
|
||||
fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *native_sdk.AppInfo, buffers: *StateBuffers) ?native_sdk.window_state.Store {
|
||||
const paths = native_sdk.window_state.defaultPaths(&buffers.state_dir, &buffers.file_path, app_info.bundle_id, native_sdk.debug.envFromMap(env_map)) catch return null;
|
||||
const store = native_sdk.window_state.Store.init(io, paths.state_dir, paths.file_path);
|
||||
if (app_info.windows.len > 0) {
|
||||
const restored_windows = buffers.restored_windows[0..app_info.windows.len];
|
||||
for (restored_windows, 0..) |*window, index| {
|
||||
if (!window.restore_state) continue;
|
||||
if (store.loadWindow(window.label, &buffers.read) catch null) |saved| {
|
||||
window.default_frame = saved.frame;
|
||||
if (index == 0) app_info.main_window.default_frame = saved.frame;
|
||||
}
|
||||
}
|
||||
} else if (app_info.main_window.restore_state) {
|
||||
if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
|
||||
app_info.main_window.default_frame = saved.frame;
|
||||
}
|
||||
}
|
||||
return store;
|
||||
}
|
||||
Reference in New Issue
Block a user