@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.
- package/CHANGELOG.md +313 -0
- package/LICENSE +21 -0
- package/README.md +559 -0
- package/bin/belte.ts +183 -0
- package/package.json +110 -0
- package/src/App.svelte +31 -0
- package/src/appEntry.ts +151 -0
- package/src/assets/app.html +14 -0
- package/src/belteResolverPlugin.ts +858 -0
- package/src/build.ts +147 -0
- package/src/buildCli.ts +129 -0
- package/src/buildDisconnected.ts +122 -0
- package/src/bundleApp.ts +149 -0
- package/src/bundleDisconnectedEntry.ts +17 -0
- package/src/cliEntry.ts +25 -0
- package/src/clientBuildPlugins.ts +41 -0
- package/src/clientEntry.ts +7 -0
- package/src/compile.ts +64 -0
- package/src/controlServerWorker.ts +422 -0
- package/src/dedupeSveltePlugin.ts +66 -0
- package/src/devEntry.ts +169 -0
- package/src/discoveryEntry.ts +81 -0
- package/src/lib/browser/applyStreamedResolution.ts +33 -0
- package/src/lib/browser/cacheEntryFromSnapshot.ts +48 -0
- package/src/lib/browser/flushUnresolvedPlaceholders.ts +16 -0
- package/src/lib/browser/installStreamingPlaceholders.ts +32 -0
- package/src/lib/browser/openResolveStream.ts +42 -0
- package/src/lib/browser/page.svelte.ts +258 -0
- package/src/lib/browser/pageStreamController.ts +17 -0
- package/src/lib/browser/refetchPlaceholder.ts +12 -0
- package/src/lib/browser/remoteProxy.ts +37 -0
- package/src/lib/browser/socketChannel.ts +192 -0
- package/src/lib/browser/socketProxy.ts +57 -0
- package/src/lib/browser/startClient.ts +153 -0
- package/src/lib/browser/subscribe.ts +131 -0
- package/src/lib/browser/types/Errors.ts +9 -0
- package/src/lib/browser/types/Layouts.ts +7 -0
- package/src/lib/browser/types/Pages.ts +7 -0
- package/src/lib/browser/types/StreamingDeferred.ts +9 -0
- package/src/lib/bundle/BundleMenu.ts +11 -0
- package/src/lib/bundle/BundleMenuItem.ts +24 -0
- package/src/lib/bundle/BundleWindow.ts +36 -0
- package/src/lib/bundle/WEBVIEW_BUILD_REVISION.ts +9 -0
- package/src/lib/bundle/WEBVIEW_VERSION.ts +7 -0
- package/src/lib/bundle/bindConnectedFlag.ts +29 -0
- package/src/lib/bundle/bindRequestNavigate.ts +31 -0
- package/src/lib/bundle/buildWebviewLib.ts +111 -0
- package/src/lib/bundle/disconnected.css +9 -0
- package/src/lib/bundle/disconnected.svelte +386 -0
- package/src/lib/bundle/ensureWebviewLib.ts +20 -0
- package/src/lib/bundle/exitWithParent.ts +28 -0
- package/src/lib/bundle/infoPlist.ts +46 -0
- package/src/lib/bundle/installDownloads.ts +24 -0
- package/src/lib/bundle/installMacMenu.ts +39 -0
- package/src/lib/bundle/listenLocalControlServer.ts +19 -0
- package/src/lib/bundle/native/belteMenu.mm +422 -0
- package/src/lib/bundle/native/webview.h +4557 -0
- package/src/lib/bundle/onMenu.ts +41 -0
- package/src/lib/bundle/openWebview.ts +104 -0
- package/src/lib/bundle/pngToIcns.ts +47 -0
- package/src/lib/bundle/probeBelteServer.ts +34 -0
- package/src/lib/bundle/resolveServerBinary.ts +12 -0
- package/src/lib/bundle/resolveWebviewLib.ts +53 -0
- package/src/lib/bundle/serverBinaryFilename.ts +8 -0
- package/src/lib/bundle/signMacApp.ts +35 -0
- package/src/lib/bundle/spawnEmbeddedServer.ts +65 -0
- package/src/lib/bundle/stableLocalPort.ts +19 -0
- package/src/lib/bundle/waitForServer.ts +23 -0
- package/src/lib/bundle/webviewCachePath.ts +23 -0
- package/src/lib/bundle/webviewLibName.ts +11 -0
- package/src/lib/cli/connectToServer.ts +23 -0
- package/src/lib/cli/createClient.ts +170 -0
- package/src/lib/cli/dispatchCommand.ts +71 -0
- package/src/lib/cli/loadEnvFromBinaryDir.ts +16 -0
- package/src/lib/cli/parseArgvForRpc.ts +97 -0
- package/src/lib/cli/printHelp.ts +119 -0
- package/src/lib/cli/printSessionHelp.ts +27 -0
- package/src/lib/cli/printSessionStatus.ts +21 -0
- package/src/lib/cli/printTrimmed.ts +8 -0
- package/src/lib/cli/printValue.ts +10 -0
- package/src/lib/cli/resolveCliTarget.ts +48 -0
- package/src/lib/cli/runCli.ts +139 -0
- package/src/lib/cli/runSession.ts +105 -0
- package/src/lib/cli/startLocalInstance.ts +14 -0
- package/src/lib/cli/tokenizeLine.ts +51 -0
- package/src/lib/cli/types/CliManifest.ts +9 -0
- package/src/lib/cli/types/CliManifestEntry.ts +17 -0
- package/src/lib/cli/types/CliTarget.ts +13 -0
- package/src/lib/mcp/annotationsForMethod.ts +29 -0
- package/src/lib/mcp/createMcpResourceServer.ts +101 -0
- package/src/lib/mcp/createMcpServer.ts +42 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +146 -0
- package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
- package/src/lib/mcp/mcpSurface.ts +265 -0
- package/src/lib/mcp/toolResultFromResponse.ts +66 -0
- package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
- package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
- package/src/lib/mcp/types/McpResourceContents.ts +10 -0
- package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
- package/src/lib/mcp/types/McpResourceServer.ts +12 -0
- package/src/lib/mcp/types/McpServer.ts +9 -0
- package/src/lib/mcp/types/McpServerOptions.ts +16 -0
- package/src/lib/server/AppModule.ts +33 -0
- package/src/lib/server/DELETE.ts +9 -0
- package/src/lib/server/GET.ts +9 -0
- package/src/lib/server/HEAD.ts +9 -0
- package/src/lib/server/PATCH.ts +9 -0
- package/src/lib/server/POST.ts +9 -0
- package/src/lib/server/PUT.ts +9 -0
- package/src/lib/server/agent.ts +76 -0
- package/src/lib/server/appDataDir.ts +15 -0
- package/src/lib/server/cli/buildEnvContent.ts +19 -0
- package/src/lib/server/cli/createTarGz.ts +76 -0
- package/src/lib/server/cli/handleCliDownload.ts +153 -0
- package/src/lib/server/cli/handleCliInstall.ts +37 -0
- package/src/lib/server/cli/installScript.ts +29 -0
- package/src/lib/server/cli/maxSourceMtime.ts +26 -0
- package/src/lib/server/cookies.ts +29 -0
- package/src/lib/server/env.ts +50 -0
- package/src/lib/server/error.ts +70 -0
- package/src/lib/server/json.ts +28 -0
- package/src/lib/server/jsonl.ts +46 -0
- package/src/lib/server/prompts/definePrompt.ts +20 -0
- package/src/lib/server/prompts/promptRegistry.ts +9 -0
- package/src/lib/server/prompts/registerPrompt.ts +6 -0
- package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
- package/src/lib/server/prompts/types/Prompt.ts +13 -0
- package/src/lib/server/prompts/types/PromptOptions.ts +12 -0
- package/src/lib/server/prompts/types/PromptRegistryEntry.ts +13 -0
- package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
- package/src/lib/server/redirect.ts +42 -0
- package/src/lib/server/request.ts +18 -0
- package/src/lib/server/rpc/defineVerb.ts +133 -0
- package/src/lib/server/rpc/dispatchVerbInProcess.ts +46 -0
- package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
- package/src/lib/server/rpc/parseArgs.ts +95 -0
- package/src/lib/server/rpc/registerVerb.ts +6 -0
- package/src/lib/server/rpc/types/RemoteHandler.ts +27 -0
- package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
- package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
- package/src/lib/server/rpc/types/VerbHelper.ts +68 -0
- package/src/lib/server/rpc/types/VerbRegistryEntry.ts +29 -0
- package/src/lib/server/rpc/unprocessed.ts +14 -0
- package/src/lib/server/rpc/verbRegistry.ts +11 -0
- package/src/lib/server/runtime/DEFAULT_PORT.ts +6 -0
- package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
- package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +29 -0
- package/src/lib/server/runtime/acceptsZstd.ts +8 -0
- package/src/lib/server/runtime/buildOpenApiSpec.ts +106 -0
- package/src/lib/server/runtime/cacheControlForAsset.ts +22 -0
- package/src/lib/server/runtime/containsTraversal.ts +37 -0
- package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +63 -0
- package/src/lib/server/runtime/createRouteDispatcher.ts +100 -0
- package/src/lib/server/runtime/createServer.ts +692 -0
- package/src/lib/server/runtime/devReloadResponse.ts +35 -0
- package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +27 -0
- package/src/lib/server/runtime/envSchemaStore.ts +15 -0
- package/src/lib/server/runtime/findOpenPort.ts +35 -0
- package/src/lib/server/runtime/getActiveServer.ts +6 -0
- package/src/lib/server/runtime/globToPathSet.ts +29 -0
- package/src/lib/server/runtime/inProcessServer.ts +20 -0
- package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
- package/src/lib/server/runtime/isCrossOriginUpgrade.ts +19 -0
- package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
- package/src/lib/server/runtime/logExposedSurfaces.ts +162 -0
- package/src/lib/server/runtime/mimeForExtension.ts +20 -0
- package/src/lib/server/runtime/parseIdleTimeout.ts +10 -0
- package/src/lib/server/runtime/parsePort.ts +11 -0
- package/src/lib/server/runtime/registryManifests.ts +66 -0
- package/src/lib/server/runtime/requestContext.ts +5 -0
- package/src/lib/server/runtime/resolveStreamResponse.ts +29 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +57 -0
- package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +45 -0
- package/src/lib/server/runtime/serverSlot.ts +13 -0
- package/src/lib/server/runtime/setActiveServer.ts +6 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +81 -0
- package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
- package/src/lib/server/runtime/streamFromIterator.ts +86 -0
- package/src/lib/server/runtime/streamStash.ts +64 -0
- package/src/lib/server/runtime/types/Assets.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +27 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
- package/src/lib/server/server.ts +32 -0
- package/src/lib/server/socket.ts +31 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +311 -0
- package/src/lib/server/sockets/defineSocket.ts +167 -0
- package/src/lib/server/sockets/lookupSocket.ts +6 -0
- package/src/lib/server/sockets/recentHistory.ts +11 -0
- package/src/lib/server/sockets/registerSocket.ts +6 -0
- package/src/lib/server/sockets/socketOperations.ts +35 -0
- package/src/lib/server/sockets/socketRegistry.ts +9 -0
- package/src/lib/server/sockets/types/Socket.ts +21 -0
- package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
- package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
- package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +17 -0
- package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
- package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
- package/src/lib/server/sse.ts +53 -0
- package/src/lib/shared/BELTE_PACKAGE_NAME.ts +7 -0
- package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
- package/src/lib/shared/HttpError.ts +19 -0
- package/src/lib/shared/RESOLVE_STREAM_PATH.ts +7 -0
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
- package/src/lib/shared/activeCacheStore.ts +20 -0
- package/src/lib/shared/appDataDir.ts +34 -0
- package/src/lib/shared/belteImportName.ts +44 -0
- package/src/lib/shared/browserClientFlags.ts +10 -0
- package/src/lib/shared/buildRpcRequest.ts +70 -0
- package/src/lib/shared/bundleLayout.ts +36 -0
- package/src/lib/shared/bundled.ts +34 -0
- package/src/lib/shared/cache.ts +559 -0
- package/src/lib/shared/cacheStoreSlot.ts +16 -0
- package/src/lib/shared/canonicalJson.ts +63 -0
- package/src/lib/shared/carriesBodyArgs.ts +13 -0
- package/src/lib/shared/clearLastConnection.ts +7 -0
- package/src/lib/shared/commandNameForUrl.ts +17 -0
- package/src/lib/shared/createCacheStore.ts +75 -0
- package/src/lib/shared/createPushIterator.ts +93 -0
- package/src/lib/shared/createRemoteFunction.ts +99 -0
- package/src/lib/shared/decodeResponse.ts +47 -0
- package/src/lib/shared/detectTarget.ts +27 -0
- package/src/lib/shared/exeSuffix.ts +9 -0
- package/src/lib/shared/exitOnBuildFailure.ts +17 -0
- package/src/lib/shared/extraForwardHeaders.ts +16 -0
- package/src/lib/shared/fileStem.ts +9 -0
- package/src/lib/shared/findExportCallSite.ts +479 -0
- package/src/lib/shared/forwardHeaders.ts +41 -0
- package/src/lib/shared/getRemoteMeta.ts +5 -0
- package/src/lib/shared/globalCacheStore.ts +15 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +14 -0
- package/src/lib/shared/importNamesToStrip.ts +13 -0
- package/src/lib/shared/invalidateEvent.ts +11 -0
- package/src/lib/shared/isCompileTarget.ts +15 -0
- package/src/lib/shared/isDebugEnabled.ts +23 -0
- package/src/lib/shared/isModuleNotFound.ts +16 -0
- package/src/lib/shared/isReadOnlyMethod.ts +14 -0
- package/src/lib/shared/isStreamingResponse.ts +11 -0
- package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
- package/src/lib/shared/jsonSchemaForSchema.ts +32 -0
- package/src/lib/shared/jsonlErrorFrame.ts +24 -0
- package/src/lib/shared/keyForRemoteCall.ts +29 -0
- package/src/lib/shared/lastConnectionPath.ts +7 -0
- package/src/lib/shared/loadEnvFile.ts +17 -0
- package/src/lib/shared/loadEnvFromDataDir.ts +15 -0
- package/src/lib/shared/loadSvelteConfig.ts +18 -0
- package/src/lib/shared/log.ts +104 -0
- package/src/lib/shared/manifestModule.ts +39 -0
- package/src/lib/shared/memoizeByKey.ts +24 -0
- package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
- package/src/lib/shared/normalizeTarget.ts +10 -0
- package/src/lib/shared/pageUrlForFile.ts +14 -0
- package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
- package/src/lib/shared/parseEnv.ts +30 -0
- package/src/lib/shared/parsePromptMarkdown.ts +34 -0
- package/src/lib/shared/parseRouteSegments.ts +22 -0
- package/src/lib/shared/prepareRpcModule.ts +59 -0
- package/src/lib/shared/prepareSocketModule.ts +49 -0
- package/src/lib/shared/programNameForPackage.ts +14 -0
- package/src/lib/shared/promptNameForFile.ts +10 -0
- package/src/lib/shared/queryStringFromArgs.ts +27 -0
- package/src/lib/shared/readEnvFile.ts +15 -0
- package/src/lib/shared/readLastConnection.ts +18 -0
- package/src/lib/shared/readPackageJson.ts +9 -0
- package/src/lib/shared/recordRemoteMeta.ts +5 -0
- package/src/lib/shared/remoteMetaStore.ts +16 -0
- package/src/lib/shared/resolveClientFlags.ts +20 -0
- package/src/lib/shared/responseErrorText.ts +9 -0
- package/src/lib/shared/rpcUrlForFile.ts +19 -0
- package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
- package/src/lib/shared/serializeEnv.ts +18 -0
- package/src/lib/shared/setCacheStoreResolver.ts +6 -0
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
- package/src/lib/shared/socketNameForFile.ts +11 -0
- package/src/lib/shared/sseErrorFrame.ts +29 -0
- package/src/lib/shared/streamResponse.ts +169 -0
- package/src/lib/shared/stripImport.ts +27 -0
- package/src/lib/shared/subscribableFromResponse.ts +51 -0
- package/src/lib/shared/toBunRoutePattern.ts +28 -0
- package/src/lib/shared/types/CacheEntry.ts +63 -0
- package/src/lib/shared/types/CacheInvalidation.ts +9 -0
- package/src/lib/shared/types/CacheOptions.ts +33 -0
- package/src/lib/shared/types/CacheSnapshot.ts +16 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
- package/src/lib/shared/types/CacheStore.ts +32 -0
- package/src/lib/shared/types/ClientFlags.ts +11 -0
- package/src/lib/shared/types/CompileTarget.ts +6 -0
- package/src/lib/shared/types/HttpVerb.ts +1 -0
- package/src/lib/shared/types/LastConnection.ts +9 -0
- package/src/lib/shared/types/PromptArgument.ts +12 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +13 -0
- package/src/lib/shared/types/RemoteFunction.ts +42 -0
- package/src/lib/shared/types/StandardSchemaV1.ts +57 -0
- package/src/lib/shared/types/StreamedResolution.ts +10 -0
- package/src/lib/shared/types/StreamingPlaceholder.ts +13 -0
- package/src/lib/shared/types/Subscribable.ts +15 -0
- package/src/lib/shared/types/SvelteConfig.ts +5 -0
- package/src/lib/shared/withJsonSchema.ts +20 -0
- package/src/lib/shared/writeLastConnection.ts +13 -0
- package/src/lib/shared/writeRoutesDts.ts +67 -0
- package/src/lib/test/clearVerbRegistry.ts +11 -0
- package/src/lib/test/createTestClient.ts +78 -0
- package/src/preload.ts +20 -0
- package/src/scaffold.ts +92 -0
- package/src/serverBuildPlugins.ts +25 -0
- package/src/serverEntry.ts +94 -0
- package/src/sveltePlugin.ts +58 -0
- package/src/tailwindStylePreprocessor.ts +62 -0
- package/template/bunfig.toml +4 -0
- package/template/package.json +19 -0
- package/template/src/app.ts +23 -0
- package/template/src/browser/app.css +21 -0
- package/template/src/browser/app.html +24 -0
- package/template/src/browser/pages/about/page.svelte +5 -0
- package/template/src/browser/pages/layout.svelte +26 -0
- package/template/src/browser/pages/page.svelte +20 -0
- package/template/src/bundle/icon.png +0 -0
- package/template/src/cli/banner.txt +3 -0
- package/template/src/cli/footer.txt +1 -0
- package/template/src/server/config.ts +17 -0
- package/template/src/server/rpc/getHello.ts +35 -0
- package/template/svelte.config.js +12 -0
- package/template/tsconfig.json +18 -0
- 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
|
+
}
|