@belte/belte 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/LICENSE +21 -0
  3. package/README.md +559 -0
  4. package/bin/belte.ts +183 -0
  5. package/package.json +110 -0
  6. package/src/App.svelte +31 -0
  7. package/src/appEntry.ts +151 -0
  8. package/src/assets/app.html +14 -0
  9. package/src/belteResolverPlugin.ts +858 -0
  10. package/src/build.ts +147 -0
  11. package/src/buildCli.ts +129 -0
  12. package/src/buildDisconnected.ts +122 -0
  13. package/src/bundleApp.ts +149 -0
  14. package/src/bundleDisconnectedEntry.ts +17 -0
  15. package/src/cliEntry.ts +25 -0
  16. package/src/clientBuildPlugins.ts +41 -0
  17. package/src/clientEntry.ts +7 -0
  18. package/src/compile.ts +64 -0
  19. package/src/controlServerWorker.ts +422 -0
  20. package/src/dedupeSveltePlugin.ts +66 -0
  21. package/src/devEntry.ts +169 -0
  22. package/src/discoveryEntry.ts +81 -0
  23. package/src/lib/browser/applyStreamedResolution.ts +33 -0
  24. package/src/lib/browser/cacheEntryFromSnapshot.ts +48 -0
  25. package/src/lib/browser/flushUnresolvedPlaceholders.ts +16 -0
  26. package/src/lib/browser/installStreamingPlaceholders.ts +32 -0
  27. package/src/lib/browser/openResolveStream.ts +42 -0
  28. package/src/lib/browser/page.svelte.ts +258 -0
  29. package/src/lib/browser/pageStreamController.ts +17 -0
  30. package/src/lib/browser/refetchPlaceholder.ts +12 -0
  31. package/src/lib/browser/remoteProxy.ts +37 -0
  32. package/src/lib/browser/socketChannel.ts +192 -0
  33. package/src/lib/browser/socketProxy.ts +57 -0
  34. package/src/lib/browser/startClient.ts +153 -0
  35. package/src/lib/browser/subscribe.ts +131 -0
  36. package/src/lib/browser/types/Errors.ts +9 -0
  37. package/src/lib/browser/types/Layouts.ts +7 -0
  38. package/src/lib/browser/types/Pages.ts +7 -0
  39. package/src/lib/browser/types/StreamingDeferred.ts +9 -0
  40. package/src/lib/bundle/BundleMenu.ts +11 -0
  41. package/src/lib/bundle/BundleMenuItem.ts +24 -0
  42. package/src/lib/bundle/BundleWindow.ts +36 -0
  43. package/src/lib/bundle/WEBVIEW_BUILD_REVISION.ts +9 -0
  44. package/src/lib/bundle/WEBVIEW_VERSION.ts +7 -0
  45. package/src/lib/bundle/bindConnectedFlag.ts +29 -0
  46. package/src/lib/bundle/bindRequestNavigate.ts +31 -0
  47. package/src/lib/bundle/buildWebviewLib.ts +111 -0
  48. package/src/lib/bundle/disconnected.css +9 -0
  49. package/src/lib/bundle/disconnected.svelte +386 -0
  50. package/src/lib/bundle/ensureWebviewLib.ts +20 -0
  51. package/src/lib/bundle/exitWithParent.ts +28 -0
  52. package/src/lib/bundle/infoPlist.ts +46 -0
  53. package/src/lib/bundle/installDownloads.ts +24 -0
  54. package/src/lib/bundle/installMacMenu.ts +39 -0
  55. package/src/lib/bundle/listenLocalControlServer.ts +19 -0
  56. package/src/lib/bundle/native/belteMenu.mm +422 -0
  57. package/src/lib/bundle/native/webview.h +4557 -0
  58. package/src/lib/bundle/onMenu.ts +41 -0
  59. package/src/lib/bundle/openWebview.ts +104 -0
  60. package/src/lib/bundle/pngToIcns.ts +47 -0
  61. package/src/lib/bundle/probeBelteServer.ts +34 -0
  62. package/src/lib/bundle/resolveServerBinary.ts +12 -0
  63. package/src/lib/bundle/resolveWebviewLib.ts +53 -0
  64. package/src/lib/bundle/serverBinaryFilename.ts +8 -0
  65. package/src/lib/bundle/signMacApp.ts +35 -0
  66. package/src/lib/bundle/spawnEmbeddedServer.ts +65 -0
  67. package/src/lib/bundle/stableLocalPort.ts +19 -0
  68. package/src/lib/bundle/waitForServer.ts +23 -0
  69. package/src/lib/bundle/webviewCachePath.ts +23 -0
  70. package/src/lib/bundle/webviewLibName.ts +11 -0
  71. package/src/lib/cli/connectToServer.ts +23 -0
  72. package/src/lib/cli/createClient.ts +170 -0
  73. package/src/lib/cli/dispatchCommand.ts +71 -0
  74. package/src/lib/cli/loadEnvFromBinaryDir.ts +16 -0
  75. package/src/lib/cli/parseArgvForRpc.ts +97 -0
  76. package/src/lib/cli/printHelp.ts +119 -0
  77. package/src/lib/cli/printSessionHelp.ts +27 -0
  78. package/src/lib/cli/printSessionStatus.ts +21 -0
  79. package/src/lib/cli/printTrimmed.ts +8 -0
  80. package/src/lib/cli/printValue.ts +10 -0
  81. package/src/lib/cli/resolveCliTarget.ts +48 -0
  82. package/src/lib/cli/runCli.ts +139 -0
  83. package/src/lib/cli/runSession.ts +105 -0
  84. package/src/lib/cli/startLocalInstance.ts +14 -0
  85. package/src/lib/cli/tokenizeLine.ts +51 -0
  86. package/src/lib/cli/types/CliManifest.ts +9 -0
  87. package/src/lib/cli/types/CliManifestEntry.ts +17 -0
  88. package/src/lib/cli/types/CliTarget.ts +13 -0
  89. package/src/lib/mcp/annotationsForMethod.ts +29 -0
  90. package/src/lib/mcp/createMcpResourceServer.ts +101 -0
  91. package/src/lib/mcp/createMcpServer.ts +42 -0
  92. package/src/lib/mcp/dispatchMcpRequest.ts +146 -0
  93. package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
  94. package/src/lib/mcp/mcpSurface.ts +265 -0
  95. package/src/lib/mcp/toolResultFromResponse.ts +66 -0
  96. package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
  97. package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
  98. package/src/lib/mcp/types/McpResourceContents.ts +10 -0
  99. package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
  100. package/src/lib/mcp/types/McpResourceServer.ts +12 -0
  101. package/src/lib/mcp/types/McpServer.ts +9 -0
  102. package/src/lib/mcp/types/McpServerOptions.ts +16 -0
  103. package/src/lib/server/AppModule.ts +33 -0
  104. package/src/lib/server/DELETE.ts +9 -0
  105. package/src/lib/server/GET.ts +9 -0
  106. package/src/lib/server/HEAD.ts +9 -0
  107. package/src/lib/server/PATCH.ts +9 -0
  108. package/src/lib/server/POST.ts +9 -0
  109. package/src/lib/server/PUT.ts +9 -0
  110. package/src/lib/server/agent.ts +76 -0
  111. package/src/lib/server/appDataDir.ts +15 -0
  112. package/src/lib/server/cli/buildEnvContent.ts +19 -0
  113. package/src/lib/server/cli/createTarGz.ts +76 -0
  114. package/src/lib/server/cli/handleCliDownload.ts +153 -0
  115. package/src/lib/server/cli/handleCliInstall.ts +37 -0
  116. package/src/lib/server/cli/installScript.ts +29 -0
  117. package/src/lib/server/cli/maxSourceMtime.ts +26 -0
  118. package/src/lib/server/cookies.ts +29 -0
  119. package/src/lib/server/env.ts +50 -0
  120. package/src/lib/server/error.ts +70 -0
  121. package/src/lib/server/json.ts +28 -0
  122. package/src/lib/server/jsonl.ts +46 -0
  123. package/src/lib/server/prompts/definePrompt.ts +20 -0
  124. package/src/lib/server/prompts/promptRegistry.ts +9 -0
  125. package/src/lib/server/prompts/registerPrompt.ts +6 -0
  126. package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
  127. package/src/lib/server/prompts/types/Prompt.ts +13 -0
  128. package/src/lib/server/prompts/types/PromptOptions.ts +12 -0
  129. package/src/lib/server/prompts/types/PromptRegistryEntry.ts +13 -0
  130. package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
  131. package/src/lib/server/redirect.ts +42 -0
  132. package/src/lib/server/request.ts +18 -0
  133. package/src/lib/server/rpc/defineVerb.ts +133 -0
  134. package/src/lib/server/rpc/dispatchVerbInProcess.ts +46 -0
  135. package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
  136. package/src/lib/server/rpc/parseArgs.ts +95 -0
  137. package/src/lib/server/rpc/registerVerb.ts +6 -0
  138. package/src/lib/server/rpc/types/RemoteHandler.ts +27 -0
  139. package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
  140. package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
  141. package/src/lib/server/rpc/types/VerbHelper.ts +68 -0
  142. package/src/lib/server/rpc/types/VerbRegistryEntry.ts +29 -0
  143. package/src/lib/server/rpc/unprocessed.ts +14 -0
  144. package/src/lib/server/rpc/verbRegistry.ts +11 -0
  145. package/src/lib/server/runtime/DEFAULT_PORT.ts +6 -0
  146. package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
  147. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +29 -0
  148. package/src/lib/server/runtime/acceptsZstd.ts +8 -0
  149. package/src/lib/server/runtime/buildOpenApiSpec.ts +106 -0
  150. package/src/lib/server/runtime/cacheControlForAsset.ts +22 -0
  151. package/src/lib/server/runtime/containsTraversal.ts +37 -0
  152. package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
  153. package/src/lib/server/runtime/createPublicAssetServer.ts +63 -0
  154. package/src/lib/server/runtime/createRouteDispatcher.ts +100 -0
  155. package/src/lib/server/runtime/createServer.ts +692 -0
  156. package/src/lib/server/runtime/devReloadResponse.ts +35 -0
  157. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +27 -0
  158. package/src/lib/server/runtime/envSchemaStore.ts +15 -0
  159. package/src/lib/server/runtime/findOpenPort.ts +35 -0
  160. package/src/lib/server/runtime/getActiveServer.ts +6 -0
  161. package/src/lib/server/runtime/globToPathSet.ts +29 -0
  162. package/src/lib/server/runtime/inProcessServer.ts +20 -0
  163. package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
  164. package/src/lib/server/runtime/isCrossOriginUpgrade.ts +19 -0
  165. package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
  166. package/src/lib/server/runtime/logExposedSurfaces.ts +162 -0
  167. package/src/lib/server/runtime/mimeForExtension.ts +20 -0
  168. package/src/lib/server/runtime/parseIdleTimeout.ts +10 -0
  169. package/src/lib/server/runtime/parsePort.ts +11 -0
  170. package/src/lib/server/runtime/registryManifests.ts +66 -0
  171. package/src/lib/server/runtime/requestContext.ts +5 -0
  172. package/src/lib/server/runtime/resolveStreamResponse.ts +29 -0
  173. package/src/lib/server/runtime/runWithRequestScope.ts +57 -0
  174. package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
  175. package/src/lib/server/runtime/serializeCacheSnapshot.ts +45 -0
  176. package/src/lib/server/runtime/serverSlot.ts +13 -0
  177. package/src/lib/server/runtime/setActiveServer.ts +6 -0
  178. package/src/lib/server/runtime/snapshotEntryFromCache.ts +81 -0
  179. package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
  180. package/src/lib/server/runtime/streamFromIterator.ts +86 -0
  181. package/src/lib/server/runtime/streamStash.ts +64 -0
  182. package/src/lib/server/runtime/types/Assets.ts +1 -0
  183. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  184. package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
  185. package/src/lib/server/server.ts +32 -0
  186. package/src/lib/server/socket.ts +31 -0
  187. package/src/lib/server/sockets/createSocketDispatcher.ts +311 -0
  188. package/src/lib/server/sockets/defineSocket.ts +167 -0
  189. package/src/lib/server/sockets/lookupSocket.ts +6 -0
  190. package/src/lib/server/sockets/recentHistory.ts +11 -0
  191. package/src/lib/server/sockets/registerSocket.ts +6 -0
  192. package/src/lib/server/sockets/socketOperations.ts +35 -0
  193. package/src/lib/server/sockets/socketRegistry.ts +9 -0
  194. package/src/lib/server/sockets/types/Socket.ts +21 -0
  195. package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
  196. package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
  197. package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
  198. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +17 -0
  199. package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
  200. package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
  201. package/src/lib/server/sse.ts +53 -0
  202. package/src/lib/shared/BELTE_PACKAGE_NAME.ts +7 -0
  203. package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
  204. package/src/lib/shared/HttpError.ts +19 -0
  205. package/src/lib/shared/RESOLVE_STREAM_PATH.ts +7 -0
  206. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
  207. package/src/lib/shared/activeCacheStore.ts +20 -0
  208. package/src/lib/shared/appDataDir.ts +34 -0
  209. package/src/lib/shared/belteImportName.ts +44 -0
  210. package/src/lib/shared/browserClientFlags.ts +10 -0
  211. package/src/lib/shared/buildRpcRequest.ts +70 -0
  212. package/src/lib/shared/bundleLayout.ts +36 -0
  213. package/src/lib/shared/bundled.ts +34 -0
  214. package/src/lib/shared/cache.ts +559 -0
  215. package/src/lib/shared/cacheStoreSlot.ts +16 -0
  216. package/src/lib/shared/canonicalJson.ts +63 -0
  217. package/src/lib/shared/carriesBodyArgs.ts +13 -0
  218. package/src/lib/shared/clearLastConnection.ts +7 -0
  219. package/src/lib/shared/commandNameForUrl.ts +17 -0
  220. package/src/lib/shared/createCacheStore.ts +75 -0
  221. package/src/lib/shared/createPushIterator.ts +93 -0
  222. package/src/lib/shared/createRemoteFunction.ts +99 -0
  223. package/src/lib/shared/decodeResponse.ts +47 -0
  224. package/src/lib/shared/detectTarget.ts +27 -0
  225. package/src/lib/shared/exeSuffix.ts +9 -0
  226. package/src/lib/shared/exitOnBuildFailure.ts +17 -0
  227. package/src/lib/shared/extraForwardHeaders.ts +16 -0
  228. package/src/lib/shared/fileStem.ts +9 -0
  229. package/src/lib/shared/findExportCallSite.ts +479 -0
  230. package/src/lib/shared/forwardHeaders.ts +41 -0
  231. package/src/lib/shared/getRemoteMeta.ts +5 -0
  232. package/src/lib/shared/globalCacheStore.ts +15 -0
  233. package/src/lib/shared/globalCacheStoreSlot.ts +14 -0
  234. package/src/lib/shared/importNamesToStrip.ts +13 -0
  235. package/src/lib/shared/invalidateEvent.ts +11 -0
  236. package/src/lib/shared/isCompileTarget.ts +15 -0
  237. package/src/lib/shared/isDebugEnabled.ts +23 -0
  238. package/src/lib/shared/isModuleNotFound.ts +16 -0
  239. package/src/lib/shared/isReadOnlyMethod.ts +14 -0
  240. package/src/lib/shared/isStreamingResponse.ts +11 -0
  241. package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
  242. package/src/lib/shared/jsonSchemaForSchema.ts +32 -0
  243. package/src/lib/shared/jsonlErrorFrame.ts +24 -0
  244. package/src/lib/shared/keyForRemoteCall.ts +29 -0
  245. package/src/lib/shared/lastConnectionPath.ts +7 -0
  246. package/src/lib/shared/loadEnvFile.ts +17 -0
  247. package/src/lib/shared/loadEnvFromDataDir.ts +15 -0
  248. package/src/lib/shared/loadSvelteConfig.ts +18 -0
  249. package/src/lib/shared/log.ts +104 -0
  250. package/src/lib/shared/manifestModule.ts +39 -0
  251. package/src/lib/shared/memoizeByKey.ts +24 -0
  252. package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
  253. package/src/lib/shared/normalizeTarget.ts +10 -0
  254. package/src/lib/shared/pageUrlForFile.ts +14 -0
  255. package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
  256. package/src/lib/shared/parseEnv.ts +30 -0
  257. package/src/lib/shared/parsePromptMarkdown.ts +34 -0
  258. package/src/lib/shared/parseRouteSegments.ts +22 -0
  259. package/src/lib/shared/prepareRpcModule.ts +59 -0
  260. package/src/lib/shared/prepareSocketModule.ts +49 -0
  261. package/src/lib/shared/programNameForPackage.ts +14 -0
  262. package/src/lib/shared/promptNameForFile.ts +10 -0
  263. package/src/lib/shared/queryStringFromArgs.ts +27 -0
  264. package/src/lib/shared/readEnvFile.ts +15 -0
  265. package/src/lib/shared/readLastConnection.ts +18 -0
  266. package/src/lib/shared/readPackageJson.ts +9 -0
  267. package/src/lib/shared/recordRemoteMeta.ts +5 -0
  268. package/src/lib/shared/remoteMetaStore.ts +16 -0
  269. package/src/lib/shared/resolveClientFlags.ts +20 -0
  270. package/src/lib/shared/responseErrorText.ts +9 -0
  271. package/src/lib/shared/rpcUrlForFile.ts +19 -0
  272. package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
  273. package/src/lib/shared/serializeEnv.ts +18 -0
  274. package/src/lib/shared/setCacheStoreResolver.ts +6 -0
  275. package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
  276. package/src/lib/shared/socketNameForFile.ts +11 -0
  277. package/src/lib/shared/sseErrorFrame.ts +29 -0
  278. package/src/lib/shared/streamResponse.ts +169 -0
  279. package/src/lib/shared/stripImport.ts +27 -0
  280. package/src/lib/shared/subscribableFromResponse.ts +51 -0
  281. package/src/lib/shared/toBunRoutePattern.ts +28 -0
  282. package/src/lib/shared/types/CacheEntry.ts +63 -0
  283. package/src/lib/shared/types/CacheInvalidation.ts +9 -0
  284. package/src/lib/shared/types/CacheOptions.ts +33 -0
  285. package/src/lib/shared/types/CacheSnapshot.ts +16 -0
  286. package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
  287. package/src/lib/shared/types/CacheStore.ts +32 -0
  288. package/src/lib/shared/types/ClientFlags.ts +11 -0
  289. package/src/lib/shared/types/CompileTarget.ts +6 -0
  290. package/src/lib/shared/types/HttpVerb.ts +1 -0
  291. package/src/lib/shared/types/LastConnection.ts +9 -0
  292. package/src/lib/shared/types/PromptArgument.ts +12 -0
  293. package/src/lib/shared/types/RawRemoteFunction.ts +13 -0
  294. package/src/lib/shared/types/RemoteFunction.ts +42 -0
  295. package/src/lib/shared/types/StandardSchemaV1.ts +57 -0
  296. package/src/lib/shared/types/StreamedResolution.ts +10 -0
  297. package/src/lib/shared/types/StreamingPlaceholder.ts +13 -0
  298. package/src/lib/shared/types/Subscribable.ts +15 -0
  299. package/src/lib/shared/types/SvelteConfig.ts +5 -0
  300. package/src/lib/shared/withJsonSchema.ts +20 -0
  301. package/src/lib/shared/writeLastConnection.ts +13 -0
  302. package/src/lib/shared/writeRoutesDts.ts +67 -0
  303. package/src/lib/test/clearVerbRegistry.ts +11 -0
  304. package/src/lib/test/createTestClient.ts +78 -0
  305. package/src/preload.ts +20 -0
  306. package/src/scaffold.ts +92 -0
  307. package/src/serverBuildPlugins.ts +25 -0
  308. package/src/serverEntry.ts +94 -0
  309. package/src/sveltePlugin.ts +58 -0
  310. package/src/tailwindStylePreprocessor.ts +62 -0
  311. package/template/bunfig.toml +4 -0
  312. package/template/package.json +19 -0
  313. package/template/src/app.ts +23 -0
  314. package/template/src/browser/app.css +21 -0
  315. package/template/src/browser/app.html +24 -0
  316. package/template/src/browser/pages/about/page.svelte +5 -0
  317. package/template/src/browser/pages/layout.svelte +26 -0
  318. package/template/src/browser/pages/page.svelte +20 -0
  319. package/template/src/bundle/icon.png +0 -0
  320. package/template/src/cli/banner.txt +3 -0
  321. package/template/src/cli/footer.txt +1 -0
  322. package/template/src/server/config.ts +17 -0
  323. package/template/src/server/rpc/getHello.ts +35 -0
  324. package/template/svelte.config.js +12 -0
  325. package/template/tsconfig.json +18 -0
  326. package/tsconfig.app.json +16 -0
@@ -0,0 +1,46 @@
1
+ /*
2
+ Renders the `Info.plist` for a macOS `.app` bundle. CFBundleExecutable
3
+ must match the launcher's filename in `Contents/MacOS/` or the app won't
4
+ launch. `icon` is the filename (without extension) of an `.icns` under
5
+ `Contents/Resources/`; omitted when the project ships no icon. The
6
+ identifier is synthesized from the program name; a real distribution would
7
+ override it with a registered reverse-DNS id.
8
+ */
9
+ export function infoPlist({
10
+ name,
11
+ version,
12
+ icon,
13
+ }: {
14
+ name: string
15
+ version: string
16
+ icon?: string
17
+ }): string {
18
+ const iconEntry = icon
19
+ ? ` <key>CFBundleIconFile</key>
20
+ <string>${icon}</string>
21
+ `
22
+ : ''
23
+ return `<?xml version="1.0" encoding="UTF-8"?>
24
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
25
+ <plist version="1.0">
26
+ <dict>
27
+ <key>CFBundleName</key>
28
+ <string>${name}</string>
29
+ <key>CFBundleDisplayName</key>
30
+ <string>${name}</string>
31
+ <key>CFBundleExecutable</key>
32
+ <string>${name}</string>
33
+ <key>CFBundleIdentifier</key>
34
+ <string>com.belte.${name}</string>
35
+ <key>CFBundleVersion</key>
36
+ <string>${version}</string>
37
+ <key>CFBundleShortVersionString</key>
38
+ <string>${version}</string>
39
+ ${iconEntry} <key>CFBundlePackageType</key>
40
+ <string>APPL</string>
41
+ <key>NSHighResolutionCapable</key>
42
+ <true/>
43
+ </dict>
44
+ </plist>
45
+ `
46
+ }
@@ -0,0 +1,24 @@
1
+ import { dlopen, FFIType, type Pointer } from 'bun:ffi'
2
+
3
+ /*
4
+ Wires belte's native download delegate onto the bundle's WKWebView, so `<a
5
+ download>` clicks, blob:/data: links, and attachment responses save a real file
6
+ into the user's Downloads folder and reveal it in Finder — the bare upstream
7
+ webview sets no navigation delegate and silently drops all of these.
8
+
9
+ A no-op off macOS (the shim symbol isn't compiled into the library there) and on
10
+ macOS before 11.3 (no WKDownload API). Opened as its own short-lived handle,
11
+ mirroring installMacMenu, to keep openWebview's FFI map fully typed — a
12
+ conditional symbol there defeats Bun's argument-type inference. The delegate
13
+ attaches to the live WKWebView, so it persists after this handle closes.
14
+ */
15
+ export function installDownloads(libPath: string, webviewHandle: Pointer | null): void {
16
+ if (process.platform !== 'darwin') {
17
+ return
18
+ }
19
+ const { symbols, close } = dlopen(libPath, {
20
+ belte_install_downloads: { args: [FFIType.ptr], returns: FFIType.void },
21
+ })
22
+ symbols.belte_install_downloads(webviewHandle)
23
+ close()
24
+ }
@@ -0,0 +1,39 @@
1
+ import { dlopen, FFIType, type Pointer } from 'bun:ffi'
2
+ import type { BundleMenu } from './BundleMenu.ts'
3
+
4
+ /*
5
+ Installs the macOS application menu bar via belte's native shim in the webview
6
+ library. The standard App/Edit/Window menus are always present — so Cmd-Q and the
7
+ Edit shortcuts (Cmd-C/V/X/A/Z) work, which the bare upstream webview window lacks
8
+ — plus the launcher's `fileMenu` (inserted as File, before Edit) and the bundle's
9
+ custom `menu` (between Edit and Window). Menu items are serialised as
10
+ `{ separator: true }`, `{ label, shortcut?, navigate, role? }`, or
11
+ `{ label, shortcut?, emit }`: `navigate` items repoint the live window (the
12
+ launcher's File menu uses these, with `role` gating their enabled state against the
13
+ native connected flag set by `belte_set_connected`), `emit` items dispatch
14
+ `belte:menu` events into the page. `appName` labels the App-menu items.
15
+
16
+ The config is serialised to JSON and parsed natively, so the launcher never
17
+ touches FFI. A no-op off macOS, where the shim symbol isn't compiled into the
18
+ library; opened as its own short-lived handle to keep openWebview's FFI map
19
+ fully typed (a conditional symbol there defeats Bun's argument-type inference).
20
+ The native menu attaches to the shared NSApplication, so it persists after this
21
+ handle closes.
22
+ */
23
+ export function installMacMenu(
24
+ libPath: string,
25
+ webviewHandle: Pointer | null,
26
+ appName: string,
27
+ menu: BundleMenu[] | undefined,
28
+ fileMenu: BundleMenu | undefined,
29
+ ): void {
30
+ if (process.platform !== 'darwin') {
31
+ return
32
+ }
33
+ const { symbols, close } = dlopen(libPath, {
34
+ belte_install_app_menu: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
35
+ })
36
+ const config = JSON.stringify({ appName, fileMenu, menu })
37
+ symbols.belte_install_app_menu(webviewHandle, new TextEncoder().encode(`${config}\0`))
38
+ close()
39
+ }
@@ -0,0 +1,19 @@
1
+ /*
2
+ Binds the launcher's in-process control server to the given localhost port,
3
+ falling back to a kernel-assigned free port when that port is already taken
4
+ (another instance of the same app, or an unrelated process). The stable port
5
+ keeps the connect screen's origin — and its localStorage — constant across
6
+ launches; the fallback trades that stability for the app still booting. Callers
7
+ read the actual port from the returned server's `.port` to build the origin.
8
+ */
9
+ export function listenLocalControlServer(
10
+ port: number,
11
+ fetch: (request: Request) => Response | Promise<Response>,
12
+ ): ReturnType<typeof Bun.serve> {
13
+ try {
14
+ return Bun.serve({ port, hostname: '127.0.0.1', fetch })
15
+ } catch {
16
+ // EADDRINUSE (or any bind failure) on the stable port → take any free one.
17
+ return Bun.serve({ port: 0, hostname: '127.0.0.1', fetch })
18
+ }
19
+ }
@@ -0,0 +1,422 @@
1
+ // belte's native macOS shim, compiled into the same dylib as the vendored
2
+ // webview header. The upstream webview library creates a bare NSWindow with no
3
+ // application menu bar, so a non-bundled webview app has no Quit item and the
4
+ // standard Edit shortcuts (Cmd-C/V/X/A/Z) never reach the WKWebView. This
5
+ // installs the conventional menu so the window behaves like a normal Mac app,
6
+ // the built-in File menu (Start / Disconnect), and the bundle's custom menus
7
+ // whose items emit events into the page.
8
+ #import <Cocoa/Cocoa.h>
9
+ #import <WebKit/WebKit.h>
10
+ #import <objc/runtime.h>
11
+ #include <cstdlib>
12
+ #include <cstring>
13
+
14
+ // webview C entry points, also in this dylib — used to drive the live window.
15
+ extern "C" int webview_navigate(void *w, const char *url);
16
+ extern "C" int webview_eval(void *w, const char *js);
17
+ // Marshals a callback onto the UI thread; the only safe way to touch the window
18
+ // from another thread (the launcher runs its control server off the main thread).
19
+ extern "C" int webview_dispatch(void *w, void (*fn)(void *w, void *arg), void *arg);
20
+ // Returns a backend-native handle for the webview; with kind
21
+ // WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER (2) that's the WKWebView.
22
+ extern "C" void *webview_get_native_handle(void *w, int kind);
23
+
24
+ // Exported despite -fvisibility=hidden so belte's FFI layer can resolve it.
25
+ #define BELTE_EXPORT __attribute__((visibility("default")))
26
+
27
+ /*
28
+ Process-wide connection flag the menu validation reads. The launcher owns every
29
+ connection transition and flips this via belte_set_connected, so the Server menu
30
+ items enable/disable correctly: menu validation runs each time a menu opens, so
31
+ just storing the bool is enough — no explicit revalidation needed.
32
+ */
33
+ static int g_belte_connected = 0;
34
+
35
+ // Roles for the built-in Server menu items, parsed from each item's `role` field.
36
+ // `none` is any non-Server navigate item (always enabled).
37
+ typedef enum {
38
+ BelteRoleNone = 0,
39
+ BelteRoleStart,
40
+ BelteRoleDisconnect,
41
+ } BelteRole;
42
+
43
+ // Sets the connection flag the Server menu's validateMenuItem: reads.
44
+ extern "C" BELTE_EXPORT void belte_set_connected(int connected) {
45
+ g_belte_connected = connected;
46
+ }
47
+
48
+ // Runs on the UI thread via webview_dispatch: navigate, then free the copy made
49
+ // below (the original buffer is long gone — the dispatch is asynchronous).
50
+ static void belteDispatchNavigate(void *w, void *arg) {
51
+ char *url = (char *)arg;
52
+ webview_navigate(w, url);
53
+ free(url);
54
+ }
55
+
56
+ /*
57
+ Navigate the live window to `url` from any thread. The launcher's control server
58
+ runs off the main thread, so it can't call webview_navigate (a Cocoa/UI call)
59
+ directly; this hops onto the UI thread via webview_dispatch. The URL is copied
60
+ synchronously here because the dispatch runs later, after the caller's buffer is
61
+ gone — belteDispatchNavigate frees the copy once it has navigated.
62
+ */
63
+ extern "C" BELTE_EXPORT void belte_request_navigate(void *w, const char *url) {
64
+ webview_dispatch(w, belteDispatchNavigate, strdup(url));
65
+ }
66
+
67
+ // Maps a JSON `role` string to its enum; unknown/absent → none.
68
+ static BelteRole roleFromString(NSString *role) {
69
+ if ([role isEqualToString:@"start"]) {
70
+ return BelteRoleStart;
71
+ }
72
+ if ([role isEqualToString:@"disconnect"]) {
73
+ return BelteRoleDisconnect;
74
+ }
75
+ return BelteRoleNone;
76
+ }
77
+
78
+ // Builds a JS string literal for `value`, safely escaped via NSJSONSerialization.
79
+ static NSString *jsString(NSString *value) {
80
+ NSData *json = [NSJSONSerialization dataWithJSONObject:value
81
+ options:NSJSONWritingFragmentsAllowed
82
+ error:nil];
83
+ return [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding];
84
+ }
85
+
86
+ /*
87
+ Action target for a menu item. Owns the webview handle so a click can drive the
88
+ live window: a `navigate` item repoints the window at a URL (the Server menu's
89
+ Start/Disconnect, gated by `role`), a `emit` item dispatches a belte:menu
90
+ event into the page. Lives for the whole process (NSMenuItem does not retain its
91
+ target), matching the never-released menu objects below.
92
+ */
93
+ @interface BelteMenuAction : NSObject {
94
+ @public
95
+ void *webviewHandle;
96
+ NSString *navigateUrl; // target URL for a navigate item; nil otherwise
97
+ NSString *emitName; // event name for an emit item; nil otherwise
98
+ BelteRole role; // gating role for a Server navigate item; none otherwise
99
+ }
100
+ - (void)navigateTo:(id)sender;
101
+ - (void)emit:(id)sender;
102
+ - (BOOL)validateMenuItem:(NSMenuItem *)item;
103
+ @end
104
+
105
+ @implementation BelteMenuAction
106
+ // Points the live webview at this item's URL (already on the UI thread here).
107
+ - (void)navigateTo:(id)sender {
108
+ if (navigateUrl != nil) {
109
+ webview_navigate(webviewHandle, [navigateUrl UTF8String]);
110
+ }
111
+ }
112
+
113
+ // Dispatches a belte:menu CustomEvent (detail `{ name }`) into the page, so the
114
+ // app's own code handles it — including computing args for a parameterised call.
115
+ - (void)emit:(id)sender {
116
+ NSString *js = [NSString
117
+ stringWithFormat:
118
+ @"window.dispatchEvent(new CustomEvent('belte:menu',{detail:{name:%@}}))",
119
+ jsString(emitName)];
120
+ webview_eval(webviewHandle, [js UTF8String]);
121
+ }
122
+
123
+ /*
124
+ Drives the enabled state per connection. Server navigate items follow the truth
125
+ table — Start only when disconnected, Disconnect only when connected — emit items
126
+ only fire into a loaded page (so connected), and plain navigate items (role none)
127
+ are always enabled.
128
+ */
129
+ - (BOOL)validateMenuItem:(NSMenuItem *)item {
130
+ if (navigateUrl != nil) {
131
+ switch (role) {
132
+ case BelteRoleStart:
133
+ return g_belte_connected == 0;
134
+ case BelteRoleDisconnect:
135
+ return g_belte_connected != 0;
136
+ default:
137
+ return YES;
138
+ }
139
+ }
140
+ if (emitName != nil) {
141
+ return g_belte_connected != 0;
142
+ }
143
+ return YES;
144
+ }
145
+ @end
146
+
147
+ // Builds one bundle submenu from its JSON description and items, or nil when
148
+ // the description is malformed. Each item is a separator, a `navigate` item
149
+ // (optionally roled), or an `emit` item.
150
+ static NSMenu *buildBundleMenu(NSDictionary *menuDef, void *webview_handle) {
151
+ if (![menuDef isKindOfClass:[NSDictionary class]]) {
152
+ return nil;
153
+ }
154
+ NSMenu *menu = [[NSMenu alloc] initWithTitle:menuDef[@"label"] ?: @""];
155
+ for (NSDictionary *item in menuDef[@"items"]) {
156
+ if ([item[@"separator"] boolValue]) {
157
+ [menu addItem:[NSMenuItem separatorItem]];
158
+ continue;
159
+ }
160
+ NSString *navigate = item[@"navigate"];
161
+ NSString *emitName = item[@"emit"];
162
+ BelteMenuAction *target = [[BelteMenuAction alloc] init];
163
+ target->webviewHandle = webview_handle;
164
+ SEL action;
165
+ if ([navigate isKindOfClass:[NSString class]]) {
166
+ target->navigateUrl = [navigate copy];
167
+ target->role = roleFromString(item[@"role"]);
168
+ action = @selector(navigateTo:);
169
+ } else if ([emitName isKindOfClass:[NSString class]]) {
170
+ target->emitName = [emitName copy];
171
+ action = @selector(emit:);
172
+ } else {
173
+ continue;
174
+ }
175
+ NSMenuItem *menuItem = [menu addItemWithTitle:(item[@"label"] ?: @"")
176
+ action:action
177
+ keyEquivalent:(item[@"shortcut"] ?: @"")];
178
+ [menuItem setTarget:target];
179
+ }
180
+ return menu;
181
+ }
182
+
183
+ /*
184
+ Builds and installs the macOS main menu on the shared application: App, File (the
185
+ launcher's built-in Start/Disconnect), Edit, the bundle's custom menus, and
186
+ Window.
187
+
188
+ Safe to call after webview_create — which has already created the
189
+ NSApplication — and must run before webview_run so the menu is present when the
190
+ run loop starts. `webview_handle` is the value returned by webview_create,
191
+ captured so the menu can drive it. `config_json` is a JSON object
192
+ `{ "appName": string, "fileMenu"?: { label, items }, "menu"?: [{ label, items }] }`,
193
+ where each item is `{ separator: true }`, `{ label, shortcut?, navigate, role? }`,
194
+ or `{ label, shortcut?, emit }`; pass NULL for the standard menus only.
195
+
196
+ Compiled without ARC (matching the webview header's manual memory model): the
197
+ menu objects and action targets intentionally live for the whole process, so
198
+ the +1 alloc counts are never released.
199
+ */
200
+ extern "C" BELTE_EXPORT void belte_install_app_menu(void *webview_handle,
201
+ const char *config_json) {
202
+ @autoreleasepool {
203
+ NSDictionary *config = nil;
204
+ if (config_json) {
205
+ NSData *data = [[NSString stringWithUTF8String:config_json]
206
+ dataUsingEncoding:NSUTF8StringEncoding];
207
+ id parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
208
+ if ([parsed isKindOfClass:[NSDictionary class]]) {
209
+ config = parsed;
210
+ }
211
+ }
212
+ NSApplication *app = [NSApplication sharedApplication];
213
+ NSString *appName = config[@"appName"] ?: @"App";
214
+
215
+ NSMenu *mainMenu = [[NSMenu alloc] init];
216
+
217
+ // Application menu — the bold first menu. Its title is ignored by macOS
218
+ // (the process/bundle name wins), but its items are what users reach.
219
+ NSMenuItem *appMenuItem = [[NSMenuItem alloc] init];
220
+ [mainMenu addItem:appMenuItem];
221
+ NSMenu *appMenu = [[NSMenu alloc] init];
222
+ [appMenuItem setSubmenu:appMenu];
223
+
224
+ [appMenu addItemWithTitle:[@"About " stringByAppendingString:appName]
225
+ action:@selector(orderFrontStandardAboutPanel:)
226
+ keyEquivalent:@""];
227
+ [appMenu addItem:[NSMenuItem separatorItem]];
228
+ [appMenu addItemWithTitle:[@"Hide " stringByAppendingString:appName]
229
+ action:@selector(hide:)
230
+ keyEquivalent:@"h"];
231
+ NSMenuItem *hideOthers = [appMenu addItemWithTitle:@"Hide Others"
232
+ action:@selector(hideOtherApplications:)
233
+ keyEquivalent:@"h"];
234
+ [hideOthers setKeyEquivalentModifierMask:(NSEventModifierFlagCommand |
235
+ NSEventModifierFlagOption)];
236
+ [appMenu addItemWithTitle:@"Show All"
237
+ action:@selector(unhideAllApplications:)
238
+ keyEquivalent:@""];
239
+ [appMenu addItem:[NSMenuItem separatorItem]];
240
+ [appMenu addItemWithTitle:[@"Quit " stringByAppendingString:appName]
241
+ action:@selector(terminate:)
242
+ keyEquivalent:@"q"];
243
+
244
+ // File menu — the launcher's Start/Disconnect, in the conventional
245
+ // slot right after the App menu and before Edit. Built from the same item
246
+ // shape as the custom menus, so `role` gating applies.
247
+ NSMenu *fileMenu = buildBundleMenu(config[@"fileMenu"], webview_handle);
248
+ if (fileMenu) {
249
+ NSMenuItem *fileMenuItem = [[NSMenuItem alloc] init];
250
+ [mainMenu addItem:fileMenuItem];
251
+ [fileMenuItem setSubmenu:fileMenu];
252
+ }
253
+
254
+ // Edit menu — the actions resolve against the first responder, which is
255
+ // the WKWebView, so Cmd-Z/X/C/V/A operate on the page's text fields.
256
+ NSMenuItem *editMenuItem = [[NSMenuItem alloc] init];
257
+ [mainMenu addItem:editMenuItem];
258
+ NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"];
259
+ [editMenuItem setSubmenu:editMenu];
260
+ [editMenu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"];
261
+ NSMenuItem *redo = [editMenu addItemWithTitle:@"Redo"
262
+ action:@selector(redo:)
263
+ keyEquivalent:@"z"];
264
+ [redo setKeyEquivalentModifierMask:(NSEventModifierFlagCommand |
265
+ NSEventModifierFlagShift)];
266
+ [editMenu addItem:[NSMenuItem separatorItem]];
267
+ [editMenu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"];
268
+ [editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"];
269
+ [editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"];
270
+ [editMenu addItemWithTitle:@"Select All"
271
+ action:@selector(selectAll:)
272
+ keyEquivalent:@"a"];
273
+
274
+ // Bundle's menus, inserted between Edit and Window — the launcher passes
275
+ // its built-in Server menu first, followed by the app's custom menus.
276
+ for (NSDictionary *menuDef in config[@"menu"]) {
277
+ NSMenu *bundleMenu = buildBundleMenu(menuDef, webview_handle);
278
+ if (bundleMenu) {
279
+ NSMenuItem *bundleMenuItem = [[NSMenuItem alloc] init];
280
+ [mainMenu addItem:bundleMenuItem];
281
+ [bundleMenuItem setSubmenu:bundleMenu];
282
+ }
283
+ }
284
+
285
+ // Window menu — Minimize/Zoom/Close, registered so macOS tracks the
286
+ // app's windows in it automatically.
287
+ NSMenuItem *windowMenuItem = [[NSMenuItem alloc] init];
288
+ [mainMenu addItem:windowMenuItem];
289
+ NSMenu *windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
290
+ [windowMenuItem setSubmenu:windowMenu];
291
+ [windowMenu addItemWithTitle:@"Minimize"
292
+ action:@selector(performMiniaturize:)
293
+ keyEquivalent:@"m"];
294
+ [windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
295
+ [windowMenu addItem:[NSMenuItem separatorItem]];
296
+ [windowMenu addItemWithTitle:@"Close"
297
+ action:@selector(performClose:)
298
+ keyEquivalent:@"w"];
299
+ [app setWindowsMenu:windowMenu];
300
+
301
+ [app setMainMenu:mainMenu];
302
+ }
303
+ }
304
+
305
+ // WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER — the WKWebView pointer.
306
+ static const int kBelteBrowserController = 2;
307
+
308
+ // Associated-object key under which each WKDownload stashes its chosen
309
+ // destination URL, so downloadDidFinish: can reveal the saved file in Finder.
310
+ static const char kBelteDownloadDestKey = 0;
311
+
312
+ /*
313
+ Navigation + download delegate for the bundle's WKWebView. The upstream webview
314
+ sets no navigation delegate, so WKWebView silently drops `<a download>` clicks,
315
+ blob:/data: downloads, and attachment responses — leaving every belte bundle app
316
+ unable to save a file. This routes those to a real download saved into the user's
317
+ Downloads folder and reveals it in Finder, while passing every ordinary
318
+ navigation straight through (the app's own page loads must not be hijacked).
319
+ A process-lifetime singleton, never released (MRC), matching the menu objects
320
+ above; WKWebView holds its navigationDelegate weakly, so the strong global is
321
+ what keeps it alive.
322
+ */
323
+ API_AVAILABLE(macos(11.3))
324
+ @interface BelteDownloadDelegate : NSObject <WKNavigationDelegate, WKDownloadDelegate>
325
+ @end
326
+
327
+ @implementation BelteDownloadDelegate
328
+
329
+ // A link with a `download` attribute (e.g. URL.createObjectURL + a.download)
330
+ // sets shouldPerformDownload; turn only those into downloads and allow the rest
331
+ // — notably the app's own navigations, which must load normally.
332
+ - (void)webView:(WKWebView *)webView
333
+ decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
334
+ decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
335
+ if (navigationAction.shouldPerformDownload) {
336
+ decisionHandler(WKNavigationActionPolicyDownload);
337
+ } else {
338
+ decisionHandler(WKNavigationActionPolicyAllow);
339
+ }
340
+ }
341
+
342
+ // A response the webview can't render (or that the server marks as an
343
+ // attachment) becomes a download too, mirroring how a browser behaves.
344
+ - (void)webView:(WKWebView *)webView
345
+ decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse
346
+ decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
347
+ if (navigationResponse.canShowMIMEType) {
348
+ decisionHandler(WKNavigationResponsePolicyAllow);
349
+ } else {
350
+ decisionHandler(WKNavigationResponsePolicyDownload);
351
+ }
352
+ }
353
+
354
+ - (void)webView:(WKWebView *)webView
355
+ navigationAction:(WKNavigationAction *)navigationAction
356
+ didBecomeDownload:(WKDownload *)download {
357
+ download.delegate = self;
358
+ }
359
+
360
+ - (void)webView:(WKWebView *)webView
361
+ navigationResponse:(WKNavigationResponse *)navigationResponse
362
+ didBecomeDownload:(WKDownload *)download {
363
+ download.delegate = self;
364
+ }
365
+
366
+ // Save under ~/Downloads using the browser-suggested name, de-duplicating with a
367
+ // " (n)" suffix so a repeat export never silently clobbers the previous file.
368
+ - (void)download:(WKDownload *)download
369
+ decideDestinationUsingResponse:(NSURLResponse *)response
370
+ suggestedFilename:(NSString *)suggestedFilename
371
+ completionHandler:(void (^)(NSURL *))completionHandler {
372
+ NSFileManager *fm = [NSFileManager defaultManager];
373
+ NSURL *dir =
374
+ [[fm URLsForDirectory:NSDownloadsDirectory inDomains:NSUserDomainMask] firstObject];
375
+ if (dir == nil) {
376
+ dir = [NSURL fileURLWithPath:NSHomeDirectory()];
377
+ }
378
+ NSString *name = suggestedFilename.length ? suggestedFilename : @"download";
379
+ NSURL *dest = [dir URLByAppendingPathComponent:name];
380
+ NSString *base = [name stringByDeletingPathExtension];
381
+ NSString *ext = [name pathExtension];
382
+ for (int i = 1; [fm fileExistsAtPath:dest.path]; i++) {
383
+ NSString *candidate =
384
+ ext.length ? [NSString stringWithFormat:@"%@ (%d).%@", base, i, ext]
385
+ : [NSString stringWithFormat:@"%@ (%d)", base, i];
386
+ dest = [dir URLByAppendingPathComponent:candidate];
387
+ }
388
+ objc_setAssociatedObject(download, &kBelteDownloadDestKey, dest,
389
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
390
+ completionHandler(dest);
391
+ }
392
+
393
+ - (void)downloadDidFinish:(WKDownload *)download {
394
+ NSURL *dest = objc_getAssociatedObject(download, &kBelteDownloadDestKey);
395
+ if (dest != nil) {
396
+ [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[ dest ]];
397
+ }
398
+ }
399
+
400
+ @end
401
+
402
+ /*
403
+ Attaches the download delegate to the bundle's WKWebView. Safe to call after
404
+ webview_create and must run before the first navigation. A no-op on macOS
405
+ versions before 11.3 (no WKDownload API) — there downloads stay unsupported, as
406
+ they were. The delegate is a strong process-lifetime singleton because WKWebView
407
+ holds its navigationDelegate weakly.
408
+ */
409
+ extern "C" BELTE_EXPORT void belte_install_downloads(void *webview_handle) {
410
+ if (@available(macOS 11.3, *)) {
411
+ WKWebView *webView =
412
+ (WKWebView *)webview_get_native_handle(webview_handle, kBelteBrowserController);
413
+ if (webView == nil) {
414
+ return;
415
+ }
416
+ static BelteDownloadDelegate *delegate = nil;
417
+ if (delegate == nil) {
418
+ delegate = [[BelteDownloadDelegate alloc] init];
419
+ }
420
+ webView.navigationDelegate = delegate;
421
+ }
422
+ }