craft-native 0.0.171 → 0.0.173

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.
@@ -0,0 +1,342 @@
1
+ import AppKit
2
+ import AVKit
3
+ import CoreImage
4
+ import UserNotifications
5
+ import UniformTypeIdentifiers
6
+
7
+ final class CraftMacScene: NSObject, NSWindowDelegate {
8
+ let name: String
9
+ let window: NSWindow
10
+ let runtime: CraftMacRuntime
11
+ let renderer: CraftMacRenderer
12
+ init(name: String, route: String, options: [String: Any], application: CraftMacApplication) {
13
+ self.name = name
14
+ runtime = CraftMacRuntime(route: route, params: options["params"] as? [String: Any] ?? [:], network: application.network)
15
+ renderer = CraftMacRenderer(runtime: runtime)
16
+ let width = number(options["width"]) ?? 1120, height = number(options["height"]) ?? 740
17
+ window = NSWindow(contentRect: CGRect(x: 0, y: 0, width: width, height: height), styleMask: [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: false)
18
+ super.init()
19
+ runtime.scene = self; renderer.window = window
20
+ window.title = options["title"] as? String ?? application.appName
21
+ window.minSize = CGSize(width: number(options["minWidth"]) ?? 640, height: number(options["minHeight"]) ?? 400)
22
+ window.isReleasedWhenClosed = false
23
+ renderer.root.autoresizingMask = [.width, .height]
24
+ window.contentView = renderer.root
25
+ window.delegate = self; window.center()
26
+ if name == "main" { window.setFrameAutosaveName("\(application.appName)NativeMain") }
27
+ window.makeKeyAndOrderFront(nil)
28
+ runtime.load(application.bundle)
29
+ }
30
+ func windowShouldClose(_ sender: NSWindow) -> Bool {
31
+ if name == "main" { sender.orderOut(nil); return false }
32
+ return true
33
+ }
34
+ func windowWillClose(_ notification: Notification) {
35
+ runtime.dispose()
36
+ CraftMacApplication.shared.scenes.removeValue(forKey: name)
37
+ }
38
+ func windowDidBecomeKey(_ notification: Notification) { runtime.send("SCREEN_FOCUS", ["focused": true]) }
39
+ func windowDidResignKey(_ notification: Notification) { runtime.send("SCREEN_FOCUS", ["focused": false]) }
40
+ }
41
+
42
+ final class CraftMacMenuTarget: NSObject {
43
+ var chosen: String?
44
+ @objc func choose(_ item: NSMenuItem) { chosen = item.representedObject as? String }
45
+ }
46
+
47
+ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
48
+ static let shared = CraftMacApplication()
49
+ var appName = "Craft"
50
+ var bundle = ""
51
+ var initialRoute = "Main"
52
+ var network: CraftMacNetwork!
53
+ var scenes: [String: CraftMacScene] = [:]
54
+ var panels: [NSWindow] = []
55
+ var chosenFiles = Set<String>()
56
+ var testDirectory: URL?
57
+ var testTimer: Timer?
58
+ var processedActions = Set<String>()
59
+ var appearanceObservation: NSKeyValueObservation?
60
+
61
+ func applicationDidFinishLaunching(_ notification: Notification) {
62
+ installMenus()
63
+ appearanceObservation = NSApp.observe(\.effectiveAppearance, options: [.new]) { [weak self] _, _ in
64
+ DispatchQueue.main.async { self?.appearanceChanged() }
65
+ }
66
+ NSApp.activate(ignoringOtherApps: true)
67
+ // The agent issues an HttpOnly cookie. It stays in URLSession, shared
68
+ // by native windows, and is never handed to the JavaScript context.
69
+ let request = URLRequest(url: network.base.appendingPathComponent("native/session"))
70
+ network.session.dataTask(with: request) { [weak self] _, response, error in
71
+ DispatchQueue.main.async {
72
+ guard let self else { return }
73
+ guard error == nil, (response as? HTTPURLResponse)?.statusCode == 200 else {
74
+ let alert = NSAlert(); alert.messageText = "Could not connect to the app"; alert.informativeText = error?.localizedDescription ?? "The local agent did not issue a native session."; alert.runModal(); NSApp.terminate(nil); return
75
+ }
76
+ self.open(name: "main", route: self.initialRoute, options: ["title": self.appName])
77
+ self.startTestDriver()
78
+ }
79
+ }.resume()
80
+ }
81
+ func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { scenes["main"]?.window.makeKeyAndOrderFront(nil); return true }
82
+ func applicationWillTerminate(_ notification: Notification) { scenes.values.forEach { $0.runtime.dispose() }; testTimer?.invalidate() }
83
+ func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { false }
84
+ func open(name: String, route: String, options: [String: Any]) {
85
+ if let scene = scenes[name] { scene.window.makeKeyAndOrderFront(nil); return }
86
+ scenes[name] = CraftMacScene(name: name, route: route, options: options, application: self)
87
+ }
88
+ @objc func command(_ item: NSMenuItem) {
89
+ guard let id = item.representedObject as? String else { return }
90
+ broadcast(id)
91
+ }
92
+ func broadcast(_ id: String) { for scene in scenes.values { scene.runtime.send("CRAFT_EVENT", ["event": "craftDesktopCommand", "detail": ["id": id]]) } }
93
+ func appearanceChanged() {
94
+ let scheme = CraftMacColor.isDark ? "dark" : "light"
95
+ for scene in scenes.values {
96
+ scene.runtime.context.evaluateScript("__stxNativeBridge.colorScheme = '\(scheme)';")
97
+ scene.window.effectiveAppearance.performAsCurrentDrawingAppearance {
98
+ for box in scene.renderer.boxes.values {
99
+ box.layer?.backgroundColor = CraftMacColor.parse(box.style["backgroundColor"], fallback: .clear).cgColor
100
+ box.layer?.borderColor = CraftMacColor.parse(box.style["borderColor"], fallback: .separatorColor).cgColor
101
+ }
102
+ }
103
+ }
104
+ broadcast("appearance")
105
+ }
106
+
107
+ private func installMenus() {
108
+ let bar = NSMenu()
109
+ let app = NSMenu(title: appName)
110
+ app.addItem(withTitle: "About \(appName)", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
111
+ app.addItem(.separator())
112
+ let settings = app.addItem(withTitle: "Settings…", action: #selector(command(_:)), keyEquivalent: ","); settings.target = self; settings.representedObject = "settings"
113
+ app.addItem(.separator())
114
+ app.addItem(withTitle: "Hide \(appName)", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
115
+ app.addItem(withTitle: "Quit \(appName)", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
116
+ addMenu(app, to: bar)
117
+ let edit = NSMenu(title: "Edit")
118
+ edit.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z")
119
+ edit.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z")
120
+ edit.addItem(.separator())
121
+ edit.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
122
+ edit.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
123
+ edit.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
124
+ edit.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
125
+ addMenu(edit, to: bar)
126
+ let window = NSMenu(title: "Window")
127
+ window.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w")
128
+ window.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m")
129
+ window.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "")
130
+ addMenu(window, to: bar)
131
+ NSApp.mainMenu = bar; NSApp.windowsMenu = window
132
+ }
133
+ private func addMenu(_ menu: NSMenu, to bar: NSMenu) { let item = NSMenuItem(title: menu.title, action: nil, keyEquivalent: ""); item.submenu = menu; bar.addItem(item) }
134
+ private func menuItem(_ raw: [String: Any], target: AnyObject, action: Selector) -> NSMenuItem {
135
+ if raw["separator"] as? Bool == true { return .separator() }
136
+ let parts = (raw["shortcut"] as? String ?? "").lowercased().split(separator: "+").map(String.init)
137
+ let key = ["up": "\u{F700}", "down": "\u{F701}", "left": "\u{F702}", "right": "\u{F703}" ][parts.last ?? ""] ?? parts.last ?? ""
138
+ let item = NSMenuItem(title: raw["label"] as? String ?? raw["title"] as? String ?? "", action: action, keyEquivalent: key)
139
+ item.target = target; item.representedObject = raw["id"]
140
+ item.keyEquivalentModifierMask = []
141
+ if parts.contains("cmd") { item.keyEquivalentModifierMask.insert(.command) }
142
+ if parts.contains("ctrl") { item.keyEquivalentModifierMask.insert(.control) }
143
+ if parts.contains("shift") { item.keyEquivalentModifierMask.insert(.shift) }
144
+ if parts.contains("alt") { item.keyEquivalentModifierMask.insert(.option) }
145
+ item.isEnabled = raw["disabled"] as? Bool != true
146
+ item.state = raw["checked"] as? Bool == true ? .on : .off
147
+ if let symbol = raw["icon"] as? String { item.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil) }
148
+ return item
149
+ }
150
+ func perform(_ method: String, args: [Any], scene: CraftMacScene?, reply: @escaping (Any) -> Void) {
151
+ let options = args.first as? [String: Any] ?? [:]
152
+ switch method {
153
+ case "openWindow":
154
+ let name = options["name"] as? String ?? options["route"] as? String ?? "window"
155
+ let route = options["route"] as? String ?? initialRoute
156
+ open(name: name, route: route, options: options); reply(["name": name])
157
+ case "closeWindow": scene?.window.performClose(nil); reply(NSNull())
158
+ case "focus":
159
+ if let identifier = args.first as? String, let box = scene?.renderer.boxes.values.first(where: { ($0.props["testID"] as? String ?? $0.props["testId"] as? String ?? $0.id) == identifier }) {
160
+ scene?.window.makeFirstResponder((box.control as? NSScrollView)?.documentView ?? box.control)
161
+ }
162
+ reply(NSNull())
163
+ case "setMenus":
164
+ for raw in args.first as? [[String: Any]] ?? [] {
165
+ guard let title = raw["label"] as? String, ![appName, "Edit", "Window"].contains(title), let bar = NSApp.mainMenu else { continue }
166
+ if let old = bar.items.first(where: { $0.title == title }) { bar.removeItem(old) }
167
+ let menu = NSMenu(title: title); menu.autoenablesItems = false
168
+ for item in raw["items"] as? [[String: Any]] ?? [] { menu.addItem(menuItem(item, target: self, action: #selector(command(_:)))) }
169
+ addMenu(menu, to: bar)
170
+ }; reply(true)
171
+ case "contextMenu":
172
+ let target = CraftMacMenuTarget(), menu = NSMenu(); menu.autoenablesItems = false
173
+ for item in args.first as? [[String: Any]] ?? [] { menu.addItem(menuItem(item, target: target, action: #selector(CraftMacMenuTarget.choose(_:)))) }
174
+ menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil); reply(target.chosen ?? NSNull())
175
+ case "pickFiles":
176
+ let panel = NSOpenPanel(); panel.canChooseFiles = true; panel.canChooseDirectories = false; panel.allowsMultipleSelection = true
177
+ guard panel.runModal() == .OK else { reply([]); return }
178
+ let files: [[String: String]] = panel.urls.map { url in
179
+ chosenFiles.insert(url.path)
180
+ return ["path": url.path, "name": url.lastPathComponent, "type": UTType(filenameExtension: url.pathExtension)?.preferredMIMEType ?? "application/octet-stream"]
181
+ }; reply(files)
182
+ case "uploadFiles":
183
+ upload(args.first as? [[String: String]] ?? [], endpoint: args.count > 1 ? args[1] as? String ?? "" : "", reply: reply)
184
+ case "preview": preview(options, scene: scene); reply(true)
185
+ case "exportFile": export(options, reply: reply)
186
+ case "qrCode":
187
+ let text = args.first as? String ?? ""
188
+ let filter = CIFilter(name: "CIQRCodeGenerator")!; filter.setValue(Data(text.utf8), forKey: "inputMessage"); filter.setValue("M", forKey: "inputCorrectionLevel")
189
+ guard let image = filter.outputImage?.transformed(by: CGAffineTransform(scaleX: 8, y: 8)), let cg = CIContext().createCGImage(image, from: image.extent), let data = NSBitmapImageRep(cgImage: cg).representation(using: .png, properties: [:]) else { reply(NSNull()); return }
190
+ reply("data:image/png;base64," + data.base64EncodedString())
191
+ case "setBadge": let count = (args.first as? NSNumber)?.intValue ?? 0; NSApp.dockTile.badgeLabel = count > 0 ? String(count) : nil; reply(true)
192
+ default: reply(["error": "Unsupported desktop operation: \(method)"])
193
+ }
194
+ }
195
+ private func upload(_ files: [[String: String]], endpoint: String, reply: @escaping (Any) -> Void) {
196
+ var results: [Any] = []
197
+ func next(_ index: Int) {
198
+ guard index < files.count else { reply(results); return }
199
+ let file = files[index]
200
+ guard let path = file["path"], chosenFiles.contains(path), let data = try? Data(contentsOf: URL(fileURLWithPath: path)), data.count <= 256 * 1024 * 1024,
201
+ var request = network.request(["url": endpoint, "method": "POST", "headers": ["x-filename": file["name"] ?? "Attachment", "content-type": file["type"] ?? "application/octet-stream"]]) else { reply(["error": "Could not read a picked attachment"]); return }
202
+ request.httpBody = data
203
+ network.session.dataTask(with: request) { data, response, error in DispatchQueue.main.async {
204
+ guard error == nil, (response as? HTTPURLResponse)?.statusCode == 200, let data, let value = try? JSONSerialization.jsonObject(with: data) else { reply(["error": error?.localizedDescription ?? "Attachment upload failed"]); return }
205
+ results.append(value); next(index + 1)
206
+ } }.resume()
207
+ }; next(0)
208
+ }
209
+ private func preview(_ options: [String: Any], scene: CraftMacScene?) {
210
+ guard let request = network.request(["url": options["url"] ?? ""]) else { return }
211
+ let panel = NSPanel(contentRect: CGRect(x: 0, y: 0, width: 760, height: 600), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false)
212
+ panel.title = options["title"] as? String ?? "Preview"; panel.isReleasedWhenClosed = false; panels.append(panel)
213
+ if options["type"] as? String == "video" || options["type"] as? String == "audio" {
214
+ let player = AVPlayerView(); player.controlsStyle = .floating; panel.contentView = player
215
+ let headers = network.session.configuration.httpCookieStorage?.cookies(for: request.url!).map { HTTPCookie.requestHeaderFields(with: $0) } ?? [:]
216
+ player.player = AVPlayer(playerItem: AVPlayerItem(asset: AVURLAsset(url: request.url!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])))
217
+ player.player?.play()
218
+ } else {
219
+ let image = NSImageView(); image.imageScaling = .scaleProportionallyUpOrDown; panel.contentView = image
220
+ network.session.dataTask(with: request) { data, _, _ in DispatchQueue.main.async { image.image = data.flatMap(NSImage.init(data:)) } }.resume()
221
+ }
222
+ panel.center(); panel.makeKeyAndOrderFront(nil)
223
+ }
224
+ private func export(_ options: [String: Any], reply: @escaping (Any) -> Void) {
225
+ let panel = NSSavePanel(); panel.nameFieldStringValue = options["name"] as? String ?? "Attachment"
226
+ guard panel.runModal() == .OK, let destination = panel.url, let request = network.request(["url": options["url"] ?? ""]) else { reply(false); return }
227
+ network.session.downloadTask(with: request) { location, response, error in
228
+ var saved = false
229
+ if error == nil, (response as? HTTPURLResponse)?.statusCode == 200, let location { do { if FileManager.default.fileExists(atPath: destination.path) { try FileManager.default.removeItem(at: destination) }; try FileManager.default.moveItem(at: location, to: destination); saved = true } catch {} }
230
+ DispatchQueue.main.async { reply(saved) }
231
+ }.resume()
232
+ }
233
+ func notification(_ method: String, options: [String: Any], reply: @escaping (Any) -> Void) {
234
+ guard method == "schedule" else { reply(false); return }
235
+ let center = UNUserNotificationCenter.current()
236
+ let content = UNMutableNotificationContent(); content.title = options["title"] as? String ?? appName; content.body = options["body"] as? String ?? ""
237
+ let identifier = options["id"] as? String ?? UUID().uuidString
238
+ center.add(UNNotificationRequest(identifier: identifier, content: content, trigger: nil)) { error in DispatchQueue.main.async { reply(error == nil) } }
239
+ }
240
+
241
+ /// Explicit local test mode. Exercises AppKit controls on the main event
242
+ /// loop and snapshots actual view classes, never a DOM or an HTTP driver.
243
+ private func startTestDriver() {
244
+ guard let directory = testDirectory else { return }
245
+ try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
246
+ testTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { [weak self] _ in
247
+ guard let self else { return }
248
+ let files = (try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil)) ?? []
249
+ for file in files where file.lastPathComponent.hasPrefix("action-") && file.pathExtension == "json" && !self.processedActions.contains(file.lastPathComponent) {
250
+ guard let data = try? Data(contentsOf: file), let action = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { continue }
251
+ self.processedActions.insert(file.lastPathComponent)
252
+ let result = self.testAction(action)
253
+ if let bytes = try? JSONSerialization.data(withJSONObject: result) { try? bytes.write(to: directory.appendingPathComponent(file.lastPathComponent.replacingOccurrences(of: "action-", with: "result-")), options: .atomic) }
254
+ }
255
+ if let data = try? JSONSerialization.data(withJSONObject: self.snapshot()) { try? data.write(to: directory.appendingPathComponent("state.json"), options: .atomic) }
256
+ }
257
+ }
258
+ private func testAction(_ action: [String: Any]) -> [String: Any] {
259
+ let kind = action["action"] as? String ?? ""
260
+ if kind == "command", let id = action["id"] as? String { broadcast(id); return ["ok": true] }
261
+ if kind == "quit" { DispatchQueue.main.async { NSApp.terminate(nil) }; return ["ok": true] }
262
+ if kind == "appearance" {
263
+ NSApp.appearance = NSAppearance(named: action["value"] as? String == "dark" ? .darkAqua : .aqua)
264
+ appearanceChanged(); return ["ok": true]
265
+ }
266
+ let scene = scenes[action["window"] as? String ?? "main"]
267
+ guard let scene else { return ["ok": false, "error": "Window not found"] }
268
+ if kind == "close" { scene.window.performClose(nil); return ["ok": true] }
269
+ if kind == "screenshot" {
270
+ guard let directory = testDirectory, let view = scene.window.contentView?.superview,
271
+ let bitmap = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { return ["ok": false, "error": "Window cannot be rendered"] }
272
+ scene.window.effectiveAppearance.performAsCurrentDrawingAppearance {
273
+ func invalidate(_ current: NSView) { current.needsDisplay = true; for child in current.subviews { invalidate(child) } }
274
+ invalidate(view); view.displayIfNeeded()
275
+ view.cacheDisplay(in: view.bounds, to: bitmap)
276
+ }
277
+ guard let png = bitmap.representation(using: .png, properties: [:]) else { return ["ok": false, "error": "PNG encoding failed"] }
278
+ let destination = directory.appendingPathComponent("screenshot-\(scene.name)-\(CraftMacColor.isDark ? "dark" : "light").png")
279
+ do { try png.write(to: destination, options: .atomic); return ["ok": true, "path": destination.path] }
280
+ catch { return ["ok": false, "error": error.localizedDescription] }
281
+ }
282
+ guard let identifier = action["target"] as? String,
283
+ let box = scene.renderer.boxes.values.first(where: { ($0.props["testID"] as? String ?? $0.props["testId"] as? String ?? $0.id) == identifier }) else { return ["ok": false, "error": "Control not found"] }
284
+ box.scrollToVisible(box.bounds)
285
+ guard box.window?.isVisible == true, !box.isHidden, box.frame.width > 0, box.frame.height > 0 else { return ["ok": false, "error": "Control is not visible"] }
286
+ if let control = box.control as? NSControl, !control.isEnabled { return ["ok": false, "error": "Control is disabled"] }
287
+ switch kind {
288
+ case "click": if let button = box.control as? NSButton { button.performClick(nil) } else { box.pressed() }
289
+ case "fill":
290
+ let value = action["value"] as? String ?? ""
291
+ if let field = box.control as? NSTextField { field.stringValue = value; box.input(value) }
292
+ else if let text = (box.control as? NSScrollView)?.documentView as? NSTextView { text.string = value; box.input(value) }
293
+ else { return ["ok": false, "error": "Not a text input"] }
294
+ case "pick":
295
+ if let picker = box.control as? NSPopUpButton, let item = picker.itemArray.first(where: { ($0.representedObject as? String) == action["value"] as? String }) { picker.select(item); box.changed() }
296
+ else if let sidebar = box.control as? CraftMacSidebar, let id = action["value"] as? String {
297
+ guard let row = (0..<sidebar.outline.numberOfRows).first(where: { (sidebar.outline.item(atRow: $0) as? CraftMacSidebar.Item)?.id == id }) else { return ["ok": false, "error": "Sidebar item not found"] }
298
+ sidebar.outline.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
299
+ } else { return ["ok": false, "error": "Not a picker"] }
300
+ default: return ["ok": false, "error": "Unsupported test action"]
301
+ }
302
+ return ["ok": true]
303
+ }
304
+ private func snapshot() -> [String: Any] {
305
+ let windows: [[String: Any]] = scenes.values.map { scene in
306
+ let nodes: [[String: Any]] = scene.renderer.boxes.values.map { box in
307
+ var node: [String: Any] = ["id": box.props["testID"] as? String ?? box.props["testId"] as? String ?? box.id, "type": box.kind,
308
+ "nativeClass": box.control.map { String(describing: type(of: $0)) } ?? "NSView", "visible": !box.isHidden && box.window?.isVisible == true && box.frame.width > 0 && box.frame.height > 0,
309
+ "frame": [box.frame.minX, box.frame.minY, box.frame.width, box.frame.height]]
310
+ if let field = box.control as? NSTextField { node["value"] = field.stringValue }
311
+ if let text = (box.control as? NSScrollView)?.documentView as? NSTextView { node["value"] = text.string; node["editorClass"] = NSStringFromClass(type(of: text)) }
312
+ if let button = box.control as? NSButton { node["label"] = button.title; node["enabled"] = button.isEnabled }
313
+ if let picker = box.control as? NSPopUpButton { node["value"] = picker.selectedItem?.representedObject ?? "" }
314
+ if let sidebar = box.control as? CraftMacSidebar { node["value"] = (sidebar.outline.item(atRow: sidebar.outline.selectedRow) as? CraftMacSidebar.Item)?.id ?? "" }
315
+ return node
316
+ }
317
+ func classes(_ view: NSView) -> [String] { [String(describing: type(of: view))] + view.subviews.flatMap(classes) }
318
+ return ["name": scene.name, "title": scene.window.title, "route": scene.runtime.route, "contextClass": String(describing: type(of: scene.runtime.context)), "nodes": nodes, "viewClasses": classes(scene.renderer.root), "errors": scene.runtime.errors]
319
+ }
320
+ let webViews = windows.reduce(0) { count, window in count + ((window["viewClasses"] as? [String] ?? []).filter { $0.contains("WKWebView") || $0 == "WebView" }.count) }
321
+ return ["renderer": "appkit", "scriptEngine": "zig-js", "appearance": CraftMacColor.isDark ? "dark" : "light", "windows": windows, "webViews": webViews]
322
+ }
323
+ }
324
+
325
+ @main struct CraftMacMain {
326
+ static func main() throws {
327
+ func argument(_ flag: String, fallback: String = "") -> String { guard let index = CommandLine.arguments.firstIndex(of: flag), CommandLine.arguments.indices.contains(index + 1) else { return fallback }; return CommandLine.arguments[index + 1] }
328
+ let application = CraftMacApplication.shared
329
+ if CommandLine.arguments.contains("--version") { print("craft native macOS renderer 0.0.172 (zig-js)"); return }
330
+ guard let api = URL(string: argument("--api-url")), ["http", "https"].contains(api.scheme ?? ""), api.host != nil else { throw NSError(domain: "Craft", code: 1, userInfo: [NSLocalizedDescriptionKey: "--api-url must name the application's HTTP agent"] ) }
331
+ let bundlePath = argument("--native-bundle")
332
+ guard !bundlePath.isEmpty else { throw NSError(domain: "Craft", code: 2, userInfo: [NSLocalizedDescriptionKey: "--native-bundle must name a compiled STX bundle"] ) }
333
+ application.bundle = try String(contentsOfFile: bundlePath, encoding: .utf8)
334
+ application.appName = argument("--title", fallback: "Craft")
335
+ application.initialRoute = argument("--route", fallback: "Main")
336
+ application.network = CraftMacNetwork(base: api)
337
+ let testDirectory = argument("--test-dir")
338
+ if !testDirectory.isEmpty { application.testDirectory = URL(fileURLWithPath: testDirectory, isDirectory: true) }
339
+ let app = NSApplication.shared
340
+ app.setActivationPolicy(.regular); app.delegate = application; app.run()
341
+ }
342
+ }