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_shortcuts = if (@hasField(@TypeOf(app_manifest), "shortcuts")) app_manifest.shortcuts 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 = .{}, menus: []const native_sdk.Menu = &.{}, 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(); } }; 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 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); // 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, .menus = options.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); // 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, .menus = options.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); // 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, .menus = options.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); // 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, .menus = options.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; }