akanjs 3.0.2 → 3.1.0-rc.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 +26 -0
- package/base/baseEnv.ts +0 -4
- package/base/primitiveRegistry.ts +2 -2
- package/client/cookie.ts +5 -1
- package/client/csrLocation.ts +122 -0
- package/client/csrTypes.ts +32 -4
- package/client/device.ts +2 -1
- package/client/frameConfig.ts +78 -7
- package/client/index.ts +3 -0
- package/client/makePageProto.tsx +3 -2
- package/client/native/external-view.ts +1 -0
- package/client/route/RootLayoutDefinition.ts +7 -1
- package/client/route/RouteHead.ts +108 -0
- package/client/route/index.ts +1 -0
- package/client/router.ts +36 -23
- package/client/rscNavigation.ts +8 -2
- package/client/viewportMeta.ts +60 -0
- package/client/webManifest.ts +18 -0
- package/common/Logger.ts +7 -2
- package/common/PipelineNames.ts +31 -0
- package/common/PipelinePattern.ts +70 -0
- package/common/PipelinePlanSchema.ts +490 -0
- package/common/backoff.ts +14 -0
- package/common/index.ts +39 -0
- package/common/pipelinePlan.ts +259 -0
- package/common/routeConvention.ts +1 -0
- package/common/types.ts +17 -0
- package/constant/constantRegistry.ts +26 -6
- package/dictionary/base.dictionary.ts +8 -0
- package/dictionary/trans.ts +9 -1
- package/document/into.ts +5 -0
- package/fetch/client/fetchClient.ts +135 -31
- package/fetch/client/httpClient.ts +45 -0
- package/fetch/client/wsClient.ts +125 -16
- package/fetch/fetchType/buildFetch.type.ts +1 -0
- package/index.ts +243 -10
- package/package.json +1 -5
- package/server/akanApp.ts +5 -2
- package/server/akanServer.ts +44 -8
- package/server/artifact/builderRpc.ts +8 -3
- package/server/artifact/ipcTypes.ts +5 -2
- package/server/di/diLifecycle.ts +39 -3
- package/server/di/utils.ts +28 -1
- package/server/ed25519Key.ts +48 -0
- package/server/index.ts +1 -0
- package/server/machine/index.ts +2 -0
- package/server/machine/machineAccount.ts +27 -0
- package/server/machine/machineToken.ts +151 -0
- package/server/mcp/McpApps.ts +256 -0
- package/server/mcp/McpDispatcher.ts +31 -7
- package/server/mcp/McpRouter.ts +209 -46
- package/server/ops/opsToken.ts +3 -10
- package/server/resolver/database.resolver.ts +3 -0
- package/server/resolver/signal.resolver.ts +40 -3
- package/server/routeElementComposer.tsx +78 -2
- package/server/routeState.ts +6 -0
- package/server/routeTreeBuilder.ts +26 -9
- package/server/routing/apiRouter.ts +6 -3
- package/server/routing/roomReplay.ts +88 -0
- package/server/rscClient.tsx +53 -8
- package/server/rscWorker.tsx +141 -27
- package/server/rscWorkerHost.ts +18 -5
- package/server/sitemap.ts +14 -0
- package/server/ssrFromRscRenderer.tsx +12 -6
- package/server/ssrTypes.ts +3 -1
- package/server/systemPageDocument.tsx +38 -30
- package/server/types.tsx +2 -0
- package/server/webRouter.ts +74 -5
- package/service/injectInfo.ts +9 -1
- package/service/ipcTypes.ts +2 -1
- package/service/predefinedAdaptor/database.adaptor.ts +1 -146
- package/service/predefinedAdaptor/queue.adaptor.ts +44 -6
- package/service/predefinedAdaptor/queueBackoff.ts +12 -0
- package/service/predefinedAdaptor/solidQueue.adaptor.ts +17 -13
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +44 -2
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +3 -0
- package/service/predefinedAdaptor/sql/dialect/sqlite.ts +5 -2
- package/service/predefinedAdaptor/sql/search/fts5.ts +1 -1
- package/service/predefinedAdaptor/sql/types.ts +8 -6
- package/service/predefinedAdaptor/sql/values.ts +0 -23
- package/service/serve.ts +5 -1
- package/signal/agent.signal.ts +1 -1
- package/signal/base.signal.ts +2 -2
- package/signal/endpointIdempotency.ts +53 -0
- package/signal/internalArg.ts +14 -0
- package/signal/mcp/McpDocument.ts +38 -1
- package/signal/mcp/mcpProtocol.ts +18 -0
- package/signal/signalContext.ts +58 -30
- package/signal/signalRegistry.ts +14 -4
- package/signal/types.ts +18 -3
- package/store/agent/StoreSurfaceSource.ts +1 -1
- package/store/agentic/StToolBuilder.ts +3 -0
- package/test/conformance.ts +9 -33
- package/types/base/baseEnv.d.ts +0 -4
- package/types/base/primitiveRegistry.d.ts +1 -1
- package/types/client/csrLocation.d.ts +32 -0
- package/types/client/csrTypes.d.ts +40 -4
- package/types/client/frameConfig.d.ts +14 -2
- package/types/client/index.d.ts +3 -0
- package/types/client/native/external-view.d.ts +1 -0
- package/types/client/route/RootLayoutDefinition.d.ts +2 -1
- package/types/client/route/RouteHead.d.ts +13 -0
- package/types/client/route/index.d.ts +1 -0
- package/types/client/router.d.ts +6 -0
- package/types/client/rscNavigation.d.ts +4 -0
- package/types/client/viewportMeta.d.ts +18 -0
- package/types/client/webManifest.d.ts +4 -0
- package/types/common/PipelineNames.d.ts +6 -0
- package/types/common/PipelinePattern.d.ts +14 -0
- package/types/common/PipelinePlanSchema.d.ts +9 -0
- package/types/common/backoff.d.ts +10 -0
- package/types/common/index.d.ts +5 -0
- package/types/common/pipelinePlan.d.ts +224 -0
- package/types/common/types.d.ts +16 -0
- package/types/constant/constantRegistry.d.ts +5 -2
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/into.d.ts +9 -0
- package/types/fetch/client/fetchClient.d.ts +31 -4
- package/types/fetch/client/httpClient.d.ts +8 -0
- package/types/fetch/client/wsClient.d.ts +26 -2
- package/types/fetch/fetchType/buildFetch.type.d.ts +1 -0
- package/types/index.d.ts +230 -9
- package/types/server/akanServer.d.ts +16 -1
- package/types/server/artifact/builderRpc.d.ts +7 -2
- package/types/server/artifact/ipcTypes.d.ts +5 -1
- package/types/server/di/utils.d.ts +1 -0
- package/types/server/ed25519Key.d.ts +13 -0
- package/types/server/index.d.ts +1 -0
- package/types/server/machine/index.d.ts +2 -0
- package/types/server/machine/machineAccount.d.ts +5 -0
- package/types/server/machine/machineToken.d.ts +29 -0
- package/types/server/machine.d.ts +1 -0
- package/types/server/mcp/McpApps.d.ts +50 -0
- package/types/server/mcp/McpDispatcher.d.ts +4 -1
- package/types/server/mcp/McpRouter.d.ts +4 -0
- package/types/server/resolver/signal.resolver.d.ts +2 -1
- package/types/server/routeElementComposer.d.ts +24 -4
- package/types/server/routeState.d.ts +4 -1
- package/types/server/routing/roomReplay.d.ts +24 -0
- package/types/server/rscWorkerHost.d.ts +5 -2
- package/types/server/sitemap.d.ts +2 -0
- package/types/server/ssrFromRscRenderer.d.ts +3 -3
- package/types/server/ssrTypes.d.ts +4 -1
- package/types/server/systemPageDocument.d.ts +2 -0
- package/types/server/types.d.ts +4 -1
- package/types/server/webRouter.d.ts +3 -1
- package/types/service/ipcTypes.d.ts +2 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +1 -24
- package/types/service/predefinedAdaptor/queue.adaptor.d.ts +10 -3
- package/types/service/predefinedAdaptor/queueBackoff.d.ts +4 -0
- package/types/service/predefinedAdaptor/solidQueue.adaptor.d.ts +3 -3
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +6 -1
- package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/search/fts5.d.ts +1 -1
- package/types/service/predefinedAdaptor/sql/types.d.ts +9 -5
- package/types/service/predefinedAdaptor/sql/values.d.ts +0 -3
- package/types/service/serve.d.ts +3 -0
- package/types/signal/endpointIdempotency.d.ts +7 -0
- package/types/signal/internalArg.d.ts +10 -0
- package/types/signal/mcp/McpDocument.d.ts +8 -0
- package/types/signal/mcp/mcpProtocol.d.ts +17 -0
- package/types/signal/signalContext.d.ts +5 -3
- package/types/signal/signalRegistry.d.ts +4 -4
- package/types/signal/types.d.ts +21 -1
- package/types/store/agentic/StToolBuilder.d.ts +2 -0
- package/types/test/conformance.d.ts +7 -10
- package/types/ui/Agent/Chat.d.ts +5 -1
- package/types/ui/Agent/EmbedApproval.d.ts +1 -0
- package/types/ui/Agent/index.d.ts +3 -0
- package/types/ui/Agent/useKeyboardFrame.d.ts +2 -0
- package/types/ui/Agent/useScreenActions.d.ts +18 -0
- package/types/ui/BottomSheet.d.ts +1 -0
- package/types/ui/Constant/Doc.d.ts +11 -17
- package/types/ui/Constant/index.d.ts +7 -2
- package/types/ui/Constant/schemaDoc.d.ts +11 -0
- package/types/ui/Dialog/Modal.d.ts +6 -1
- package/types/ui/Dialog/index.d.ts +1 -1
- package/types/ui/Dialog/modalSurface.d.ts +12 -0
- package/types/ui/Layout/BottomTab.d.ts +11 -2
- package/types/ui/Layout/FrameSlotFallback.d.ts +9 -0
- package/types/ui/Layout/Navbar.d.ts +4 -1
- package/types/ui/Layout/TopInset.d.ts +1 -1
- package/types/ui/Layout/index.d.ts +3 -3
- package/types/ui/Link/Action.d.ts +5 -1
- package/types/ui/Link/Anchor.d.ts +5 -3
- package/types/ui/Link/activePath.d.ts +13 -0
- package/types/ui/Link/index.d.ts +1 -1
- package/types/ui/Modal.d.ts +6 -1
- package/types/ui/Model/EditModal.d.ts +6 -1
- package/types/ui/Model/New.d.ts +6 -1
- package/types/ui/Model/ViewEditModal.d.ts +6 -1
- package/types/ui/Model/ViewModal.d.ts +6 -1
- package/types/ui/Model/newFormOrigin.d.ts +7 -0
- package/types/ui/Portal.d.ts +3 -1
- package/types/ui/Reference/origin.d.ts +12 -0
- package/types/ui/Signal/Doc.d.ts +23 -18
- package/types/ui/Signal/Listener.d.ts +2 -6
- package/types/ui/Signal/Object.d.ts +6 -10
- package/types/ui/Signal/Response.d.ts +3 -6
- package/types/ui/Signal/RestApi.d.ts +11 -15
- package/types/ui/Signal/WebSocket.d.ts +11 -21
- package/types/ui/Signal/endpointEntries.d.ts +8 -0
- package/types/ui/Signal/index.d.ts +35 -12
- package/types/ui/Signal/signalText.d.ts +178 -0
- package/types/ui/System/CSR.d.ts +2 -2
- package/types/ui/System/Client.d.ts +4 -3
- package/types/ui/System/Common.d.ts +27 -6
- package/types/ui/System/SSR.d.ts +6 -6
- package/types/ui/Tooltip.d.ts +4 -2
- package/types/ui/index.d.ts +2 -1
- package/types/ui/overlayLayer.d.ts +6 -0
- package/types/ui/overlayPosition.d.ts +17 -0
- package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +14 -3
- package/types/vendor/@akanjs/native/packages/cli/src/commands/update.d.ts +1 -1
- package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +30 -5
- package/types/vendor/@akanjs/native/packages/cli/src/lib/compat.d.ts +8 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.d.ts +5 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/distribution.d.ts +22 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +4 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +16 -10
- package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +51 -7
- package/types/vendor/@akanjs/native/packages/cli/src/lib/vectors.d.ts +1 -1
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +7 -0
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows.d.ts +5 -0
- package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +36 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +2 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +11 -0
- package/types/vendor/@akanjs/native/plugins/external-view/src/desktop.d.ts +17 -0
- package/types/vendor/@akanjs/native/plugins/external-view/src/index.d.ts +132 -0
- package/types/vendor/@akanjs/native/plugins/external-view/src.d.ts +1 -0
- package/types/vendor/@akanjs/native/plugins/updates/src/index.d.ts +8 -0
- package/types/vendor/@akanjs/native/plugins/window/src/desktop.d.ts +4 -1
- package/types/vendor/@akanjs/native/plugins/window/src/index.d.ts +49 -0
- package/types/vendor/use-agentic/types.d.ts +2 -0
- package/types/webkit/CsrStack.d.ts +1 -1
- package/types/webkit/McpAppAgent.d.ts +14 -0
- package/types/webkit/McpAppApprovals.d.ts +8 -0
- package/types/webkit/McpAppBridge.d.ts +101 -0
- package/types/webkit/McpAppView.d.ts +21 -0
- package/types/webkit/index.d.ts +1 -0
- package/types/webkit/useLocation.d.ts +1 -1
- package/types/webkit/useVisualViewportFrame.d.ts +13 -0
- package/ui/Agent/Chat.tsx +29 -10
- package/ui/Agent/Composer.tsx +2 -1
- package/ui/Agent/EmbedApproval.tsx +17 -0
- package/ui/Agent/RichInput.tsx +1 -1
- package/ui/Agent/index.ts +2 -0
- package/ui/Agent/useKeyboardFrame.ts +10 -0
- package/ui/Agent/useScreenActions.ts +45 -0
- package/ui/BottomSheet.tsx +82 -53
- package/ui/Constant/Doc.tsx +113 -60
- package/ui/Constant/index.ts +2 -2
- package/ui/Constant/schemaDoc.ts +86 -26
- package/ui/Dialog/LegacyModal.tsx +53 -52
- package/ui/Dialog/Modal.tsx +93 -49
- package/ui/Dialog/modalSurface.ts +61 -0
- package/ui/DragAction.tsx +1 -1
- package/ui/DraggableList.tsx +1 -1
- package/ui/Dropdown.tsx +4 -6
- package/ui/KeyboardAvoiding.tsx +1 -1
- package/ui/Layout/BottomInset.tsx +20 -14
- package/ui/Layout/BottomTab.tsx +42 -16
- package/ui/Layout/FrameSlotFallback.tsx +59 -0
- package/ui/Layout/Navbar.tsx +37 -11
- package/ui/Layout/Sider.tsx +1 -1
- package/ui/Layout/TopInset.tsx +15 -14
- package/ui/Link/Action.tsx +15 -5
- package/ui/Link/Anchor.tsx +25 -13
- package/ui/Link/activePath.ts +25 -0
- package/ui/Link/index.tsx +1 -1
- package/ui/Load/Pagination.tsx +1 -1
- package/ui/Load/Units.tsx +1 -1
- package/ui/Loading/ProgressBar.tsx +1 -1
- package/ui/Modal.tsx +9 -0
- package/ui/Model/EditModal.tsx +16 -1
- package/ui/Model/New.tsx +25 -2
- package/ui/Model/NewWrapper_Client.tsx +2 -0
- package/ui/Model/ViewEditModal.tsx +9 -0
- package/ui/Model/ViewModal.tsx +16 -1
- package/ui/Model/newFormOrigin.ts +45 -0
- package/ui/Popconfirm.tsx +5 -7
- package/ui/Portal.tsx +4 -7
- package/ui/Reference/origin.ts +31 -0
- package/ui/ScreenNavigator.tsx +1 -1
- package/ui/Select.tsx +2 -7
- package/ui/Signal/Arg.tsx +2 -2
- package/ui/Signal/Doc.tsx +125 -63
- package/ui/Signal/Endpoint.tsx +11 -10
- package/ui/Signal/Listener.tsx +7 -10
- package/ui/Signal/Object.tsx +7 -14
- package/ui/Signal/Response.tsx +24 -24
- package/ui/Signal/RestApi.tsx +63 -52
- package/ui/Signal/WebSocket.tsx +65 -72
- package/ui/Signal/endpointEntries.ts +25 -0
- package/ui/Signal/index.ts +23 -6
- package/ui/Signal/signalText.ts +59 -0
- package/ui/System/CSR.tsx +34 -8
- package/ui/System/Client.tsx +36 -5
- package/ui/System/Common.tsx +76 -32
- package/ui/System/SSR.tsx +73 -31
- package/ui/Toast.tsx +1 -1
- package/ui/Tooltip.tsx +84 -30
- package/ui/index.ts +11 -1
- package/ui/overlayLayer.ts +25 -2
- package/ui/overlayPosition.ts +90 -0
- package/ui/recipe/inputRecipe.ts +8 -1
- package/ui/styles.css +152 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +22 -2
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeKernel.kt +37 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeUpdateReports.kt +169 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeUpdates.kt +110 -38
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeVectors.kt +20 -0
- package/vendor/@akanjs/native/native/desktop/Cargo.lock +16 -16
- package/vendor/@akanjs/native/native/desktop/src/external.rs +651 -0
- package/vendor/@akanjs/native/native/desktop/src/json.rs +23 -0
- package/vendor/@akanjs/native/native/desktop/src/lib.rs +228 -5
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeApp.swift +2 -1
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeKernel.swift +47 -1
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeUpdateReports.swift +145 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeUpdates.swift +112 -43
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeVectors.swift +18 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeViewController.swift +24 -2
- package/vendor/@akanjs/native/packages/cli/src/api.ts +22 -6
- package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +24 -8
- package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +11 -9
- package/vendor/@akanjs/native/packages/cli/src/config.ts +35 -7
- package/vendor/@akanjs/native/packages/cli/src/lib/compat.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +6 -4
- package/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.ts +11 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/distribution.ts +73 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +15 -3
- package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +70 -23
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +159 -23
- package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +9 -4
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +1 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +4 -2
- package/vendor/@akanjs/native/packages/cli/src/platforms/ios.ts +9 -4
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +25 -11
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +14 -3
- package/vendor/@akanjs/native/packages/core/src/kernel.ts +49 -0
- package/vendor/@akanjs/native/packages/core/vectors/updates.json +31 -0
- package/vendor/@akanjs/native/packages/desktop/src/main.ts +3 -0
- package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +2 -0
- package/vendor/@akanjs/native/packages/desktop/src/server.ts +70 -6
- package/vendor/@akanjs/native/plugins/external-view/native-plugin.json +23 -0
- package/vendor/@akanjs/native/plugins/external-view/src/desktop.ts +321 -0
- package/vendor/@akanjs/native/plugins/external-view/src/index.ts +106 -0
- package/vendor/@akanjs/native/plugins/updates/android/UpdatesPlugin.kt +133 -41
- package/vendor/@akanjs/native/plugins/updates/ios/UpdatesPlugin.swift +101 -36
- package/vendor/@akanjs/native/plugins/updates/native-plugin.json +4 -2
- package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +15 -1
- package/vendor/@akanjs/native/plugins/updates/src/index.ts +7 -1
- package/vendor/@akanjs/native/plugins/window/native-plugin.json +4 -0
- package/vendor/@akanjs/native/plugins/window/src/desktop.ts +68 -1
- package/vendor/@akanjs/native/plugins/window/src/index.ts +47 -0
- package/vendor/use-agentic/ToolRunner.ts +8 -1
- package/vendor/use-agentic/types.ts +2 -0
- package/webkit/CsrRouteTable.ts +1 -0
- package/webkit/CsrStack.ts +9 -2
- package/webkit/McpAppAgent.ts +138 -0
- package/webkit/McpAppApprovals.ts +56 -0
- package/webkit/McpAppBridge.ts +291 -0
- package/webkit/McpAppView.ts +258 -0
- package/webkit/bootCsr.tsx +54 -5
- package/webkit/index.ts +1 -0
- package/webkit/useCsrValues.ts +16 -10
- package/webkit/useFrameRuntime.ts +10 -1
- package/webkit/useLocation.ts +4 -3
- package/webkit/useVisualViewportFrame.tsx +52 -0
- package/service/predefinedAdaptor/sql/driver/libsql.ts +0 -45
- package/types/service/predefinedAdaptor/sql/driver/libsql.d.ts +0 -20
- package/types/ui/Agent/useKeyboardInset.d.ts +0 -1
- package/types/ui/ServerPortal.d.ts +0 -15
- package/ui/Agent/useKeyboardInset.ts +0 -19
- package/ui/ServerPortal.tsx +0 -49
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
//! The external-view plugin (plugins/external-view): a third-party page shown first-party inside an app
|
|
2
|
+
//! window, as signage needs it. Nothing of the app reaches it: no init script, no IPC, no app scheme. It runs
|
|
3
|
+
//! in a browser profile of its own (a data folder on Windows, a data store on macOS 14+), so its storage is
|
|
4
|
+
//! neither the app's nor cleared with it, and it navigates only to the origins it was opened for; popups,
|
|
5
|
+
//! downloads and device permissions are refused. Its web process ending reloads it (UserEvent::ExternalGone).
|
|
6
|
+
|
|
7
|
+
use std::{
|
|
8
|
+
collections::HashMap,
|
|
9
|
+
path::{Path, PathBuf},
|
|
10
|
+
sync::Arc,
|
|
11
|
+
time::{Duration, Instant},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
use tao::dpi::{LogicalPosition, LogicalSize};
|
|
15
|
+
use wry::{NewWindowResponse, PageLoadEvent, PermissionResponse, Rect, WebView, WebViewBuilder, RGBA};
|
|
16
|
+
|
|
17
|
+
use crate::{json, push_event, send, UserEvent};
|
|
18
|
+
|
|
19
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
20
|
+
pub(crate) enum Bounds {
|
|
21
|
+
/// The window's whole content area, kept so as the window resizes.
|
|
22
|
+
Fill,
|
|
23
|
+
At { x: f64, y: f64, width: f64, height: f64 },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub(crate) struct View {
|
|
27
|
+
/// Before the context it was built in: dropped first.
|
|
28
|
+
webview: WebView,
|
|
29
|
+
_context: wry::WebContext,
|
|
30
|
+
pub(crate) window: u32,
|
|
31
|
+
bounds: Bounds,
|
|
32
|
+
allow: Arc<Vec<String>>,
|
|
33
|
+
url: String,
|
|
34
|
+
visible: bool,
|
|
35
|
+
/// Times its web process ended and the view loaded its page again.
|
|
36
|
+
restarts: u32,
|
|
37
|
+
/// Ends in a row, each within a minute of the one before.
|
|
38
|
+
gone_streak: u32,
|
|
39
|
+
gone_at: Option<Instant>,
|
|
40
|
+
/// Its browser process ended, which takes the webview with it: it is built again, not reloaded.
|
|
41
|
+
dead: bool,
|
|
42
|
+
init_script: Option<String>,
|
|
43
|
+
muted: bool,
|
|
44
|
+
autoplay: bool,
|
|
45
|
+
console: bool,
|
|
46
|
+
/// Its window hides the pointer (window.setCursorVisible): the view does too, in every document it loads.
|
|
47
|
+
cursor_hidden: bool,
|
|
48
|
+
cursor: platform::Cursor,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[derive(Default)]
|
|
52
|
+
pub(crate) struct Views {
|
|
53
|
+
map: HashMap<String, View>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pub(crate) struct OpenOptions {
|
|
57
|
+
pub view: String,
|
|
58
|
+
pub window: u32,
|
|
59
|
+
pub url: String,
|
|
60
|
+
pub allow: Vec<String>,
|
|
61
|
+
pub bounds: Bounds,
|
|
62
|
+
pub init_script: Option<String>,
|
|
63
|
+
pub muted: bool,
|
|
64
|
+
pub autoplay: bool,
|
|
65
|
+
pub console: bool,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// `scheme://host[:port]` of an http(s) URL, lower-cased and without the scheme's default port.
|
|
69
|
+
pub(crate) fn origin_of(url: &str) -> Option<String> {
|
|
70
|
+
let (scheme, rest) = url.split_once("://")?;
|
|
71
|
+
let scheme = scheme.to_ascii_lowercase();
|
|
72
|
+
if scheme != "https" && scheme != "http" {
|
|
73
|
+
return None;
|
|
74
|
+
}
|
|
75
|
+
let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
|
|
76
|
+
let mut host = rest[..end].to_ascii_lowercase();
|
|
77
|
+
if host.is_empty() || host.contains('@') {
|
|
78
|
+
return None;
|
|
79
|
+
}
|
|
80
|
+
let default = if scheme == "https" { ":443" } else { ":80" };
|
|
81
|
+
if host.ends_with(default) {
|
|
82
|
+
host.truncate(host.len() - default.len());
|
|
83
|
+
}
|
|
84
|
+
Some(format!("{scheme}://{host}"))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub(crate) fn allowed(allow: &[String], url: &str) -> bool {
|
|
88
|
+
url == "about:blank" || origin_of(url).is_some_and(|origin| allow.contains(&origin))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// A view id as a folder name: letters, digits, `-` and `_` only (the plugin checks the id before it gets here).
|
|
92
|
+
fn folder(view: &str) -> String {
|
|
93
|
+
view.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Stable across builds (std's hasher is not): the same view id finds the same macOS data store every launch.
|
|
97
|
+
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
|
98
|
+
fn store_id(view: &str) -> [u8; 16] {
|
|
99
|
+
let fnv = |basis: u64| view.bytes().fold(basis, |hash, byte| (hash ^ byte as u64).wrapping_mul(0x0000_0100_0000_01b3));
|
|
100
|
+
let mut id = [0u8; 16];
|
|
101
|
+
id[..8].copy_from_slice(&fnv(0xcbf2_9ce4_8422_2325).to_le_bytes());
|
|
102
|
+
id[8..].copy_from_slice(&fnv(0x84222325_cbf29ce4).to_le_bytes());
|
|
103
|
+
id
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn rect(bounds: Bounds, inner: LogicalSize<f64>) -> Rect {
|
|
107
|
+
let (x, y, width, height) = match bounds {
|
|
108
|
+
Bounds::Fill => (0.0, 0.0, inner.width, inner.height),
|
|
109
|
+
Bounds::At { x, y, width, height } => (x, y, width, height),
|
|
110
|
+
};
|
|
111
|
+
Rect { position: LogicalPosition::new(x, y).into(), size: LogicalSize::new(width, height).into() }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn event(view: &str, window: u32, rest: &str) -> String {
|
|
115
|
+
format!(r#"{{"type":"externalView","view":{},"window":{window},{rest}}}"#, json::quote(view))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Where its page is now, links followed included; the last URL it was sent to while the engine cannot say.
|
|
119
|
+
fn current_url(view: &View) -> String {
|
|
120
|
+
//? A view answers about:blank until its first navigation commits.
|
|
121
|
+
view.webview.url().ok().filter(|u| !u.is_empty() && u != "about:blank").unwrap_or_else(|| view.url.clone())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn state(id: &str, view: &View) -> String {
|
|
125
|
+
format!(
|
|
126
|
+
r#"{{"view":{},"window":{},"url":{},"visible":{},"restarts":{}}}"#,
|
|
127
|
+
json::quote(id),
|
|
128
|
+
view.window,
|
|
129
|
+
json::quote(¤t_url(view)),
|
|
130
|
+
view.visible,
|
|
131
|
+
view.restarts
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
impl Views {
|
|
136
|
+
pub(crate) fn get(&self, view: &str) -> Result<&View, String> {
|
|
137
|
+
self.map.get(view).ok_or_else(|| format!("NOT_FOUND: no external view {view}"))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn get_mut(&mut self, view: &str) -> Result<&mut View, String> {
|
|
141
|
+
self.map.get_mut(view).ok_or_else(|| format!("NOT_FOUND: no external view {view}"))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
pub(crate) fn list(&self) -> String {
|
|
145
|
+
let mut ids: Vec<&String> = self.map.keys().collect();
|
|
146
|
+
ids.sort();
|
|
147
|
+
format!("[{}]", ids.iter().map(|id| state(id, &self.map[*id])).collect::<Vec<_>>().join(","))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Opens the view, or brings an open one to these options: a page that mounts again finds its view as it left it.
|
|
151
|
+
pub(crate) fn open(
|
|
152
|
+
&mut self,
|
|
153
|
+
window: &tao::window::Window,
|
|
154
|
+
inner: LogicalSize<f64>,
|
|
155
|
+
data_root: &Path,
|
|
156
|
+
devtools: bool,
|
|
157
|
+
background: RGBA,
|
|
158
|
+
opts: OpenOptions,
|
|
159
|
+
) -> Result<String, String> {
|
|
160
|
+
if let Some(view) = self.map.get_mut(&opts.view) {
|
|
161
|
+
if view.window != opts.window {
|
|
162
|
+
return Err(format!("view {} is open in window {}", opts.view, view.window));
|
|
163
|
+
}
|
|
164
|
+
view.allow = Arc::new(opts.allow);
|
|
165
|
+
view.bounds = opts.bounds;
|
|
166
|
+
if view.muted != opts.muted {
|
|
167
|
+
platform::set_muted(&view.webview, opts.muted)?;
|
|
168
|
+
view.muted = opts.muted;
|
|
169
|
+
}
|
|
170
|
+
view.webview.set_bounds(rect(view.bounds, inner)).map_err(|e| e.to_string())?;
|
|
171
|
+
if view.url != opts.url {
|
|
172
|
+
view.webview.load_url(&opts.url).map_err(|e| e.to_string())?;
|
|
173
|
+
view.url = opts.url;
|
|
174
|
+
}
|
|
175
|
+
return Ok(state(&opts.view, view));
|
|
176
|
+
}
|
|
177
|
+
let view = build(window, inner, data_root, devtools, background, &opts)?;
|
|
178
|
+
let answer = state(&opts.view, &view);
|
|
179
|
+
self.map.insert(opts.view, view);
|
|
180
|
+
Ok(answer)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
pub(crate) fn close(&mut self, view: &str) -> Result<String, String> {
|
|
184
|
+
let closed = self.map.remove(view).ok_or_else(|| format!("NOT_FOUND: no external view {view}"))?;
|
|
185
|
+
let _ = closed.webview.set_visible(false);
|
|
186
|
+
let _ = closed.webview.load_url("about:blank");
|
|
187
|
+
push_event(&event(view, closed.window, r#""event":"closed""#));
|
|
188
|
+
Ok("null".into())
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// The views of a window that is going away.
|
|
192
|
+
pub(crate) fn close_window(&mut self, window: u32) {
|
|
193
|
+
let ids: Vec<String> = self.map.iter().filter(|(_, v)| v.window == window).map(|(id, _)| id.clone()).collect();
|
|
194
|
+
for id in ids {
|
|
195
|
+
let _ = self.close(&id);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// A window resized: its filling views follow.
|
|
200
|
+
pub(crate) fn fit(&self, window: u32, inner: LogicalSize<f64>) {
|
|
201
|
+
for view in self.map.values().filter(|v| v.window == window && v.bounds == Bounds::Fill) {
|
|
202
|
+
let _ = view.webview.set_bounds(rect(Bounds::Fill, inner));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pub(crate) fn navigate(&mut self, view: &str, url: &str) -> Result<String, String> {
|
|
207
|
+
let v = self.get_mut(view)?;
|
|
208
|
+
if !allowed(&v.allow, url) {
|
|
209
|
+
return Err(format!("NOT_ALLOWED: {url} is not on an origin view {view} was opened for"));
|
|
210
|
+
}
|
|
211
|
+
v.webview.load_url(url).map_err(|e| e.to_string())?;
|
|
212
|
+
v.url = url.to_string();
|
|
213
|
+
Ok("null".into())
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pub(crate) fn reload(&self, view: &str) -> Result<String, String> {
|
|
217
|
+
self.get(view)?.webview.reload().map_err(|e| e.to_string())?;
|
|
218
|
+
Ok("null".into())
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
pub(crate) fn set_visible(&mut self, view: &str, visible: bool) -> Result<String, String> {
|
|
222
|
+
let v = self.get_mut(view)?;
|
|
223
|
+
v.webview.set_visible(visible).map_err(|e| e.to_string())?;
|
|
224
|
+
v.visible = visible;
|
|
225
|
+
Ok("null".into())
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
pub(crate) fn set_bounds(&mut self, view: &str, bounds: Bounds, inner: LogicalSize<f64>) -> Result<String, String> {
|
|
229
|
+
let v = self.get_mut(view)?;
|
|
230
|
+
v.webview.set_bounds(rect(bounds, inner)).map_err(|e| e.to_string())?;
|
|
231
|
+
v.bounds = bounds;
|
|
232
|
+
Ok("null".into())
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
pub(crate) fn clear_data(&self, view: &str) -> Result<String, String> {
|
|
236
|
+
self.get(view)?.webview.clear_all_browsing_data().map_err(|e| e.to_string())?;
|
|
237
|
+
Ok("null".into())
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
pub(crate) fn set_muted(&mut self, view: &str, muted: bool) -> Result<String, String> {
|
|
241
|
+
let v = self.get_mut(view)?;
|
|
242
|
+
platform::set_muted(&v.webview, muted)?;
|
|
243
|
+
v.muted = muted;
|
|
244
|
+
Ok("null".into())
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// The script's completion value as JSON (a promise is not awaited), answered from the engine's callback.
|
|
248
|
+
pub(crate) fn evaluate(&self, view: &str, script: &str, done: impl FnOnce(Result<String, String>) + Send + 'static) -> Result<(), String> {
|
|
249
|
+
//? wry takes an `Fn`; the engine calls it once.
|
|
250
|
+
let done = std::sync::Mutex::new(Some(done));
|
|
251
|
+
let callback = move |value: String| {
|
|
252
|
+
if let Some(done) = done.lock().unwrap_or_else(|e| e.into_inner()).take() {
|
|
253
|
+
done(Ok(value));
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
self.get(view)?.webview.evaluate_script_with_callback(script, callback).map_err(|e| e.to_string())
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// A Chrome DevTools Protocol method on the view's page: trusted input, a screenshot (Windows only).
|
|
260
|
+
pub(crate) fn cdp(&self, view: &str, method: &str, params: &str, done: impl FnOnce(Result<String, String>) + 'static) -> Result<(), String> {
|
|
261
|
+
platform::cdp(&self.get(view)?.webview, method, params, done)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// Its web process ended: report it with the count so far, and load the page again after recovery_wait.
|
|
265
|
+
pub(crate) fn gone(&mut self, view: &str, reason: &str) {
|
|
266
|
+
let Some(v) = self.map.get_mut(view) else { return };
|
|
267
|
+
let now = Instant::now();
|
|
268
|
+
let again = v.gone_at.is_some_and(|t| now.duration_since(t) < Duration::from_secs(60));
|
|
269
|
+
v.gone_streak = if again { v.gone_streak + 1 } else { 0 };
|
|
270
|
+
v.gone_at = Some(now);
|
|
271
|
+
v.restarts += 1;
|
|
272
|
+
v.dead |= reason == "browserExited";
|
|
273
|
+
push_event(&event(view, v.window, &format!(r#""event":"gone","reason":"{reason}","restarts":{}"#, v.restarts)));
|
|
274
|
+
let id = view.to_string();
|
|
275
|
+
match crate::recovery_wait(v.gone_streak) {
|
|
276
|
+
None => send(UserEvent::ExternalReload(id)),
|
|
277
|
+
Some(wait) => {
|
|
278
|
+
log!("external view {view}: its page stopped again ({reason}); loading it again in {} s", wait.as_secs());
|
|
279
|
+
std::thread::spawn(move || {
|
|
280
|
+
std::thread::sleep(wait);
|
|
281
|
+
send(UserEvent::ExternalReload(id));
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// After `gone`: a reload, or a new webview where the old one's browser process ended.
|
|
288
|
+
pub(crate) fn recover(
|
|
289
|
+
&mut self,
|
|
290
|
+
view: &str,
|
|
291
|
+
window: &tao::window::Window,
|
|
292
|
+
inner: LogicalSize<f64>,
|
|
293
|
+
data_root: &Path,
|
|
294
|
+
devtools: bool,
|
|
295
|
+
background: RGBA,
|
|
296
|
+
) -> Result<(), String> {
|
|
297
|
+
let Some(v) = self.map.get(view) else { return Ok(()) };
|
|
298
|
+
if !v.dead {
|
|
299
|
+
return v.webview.reload().map_err(|e| e.to_string());
|
|
300
|
+
}
|
|
301
|
+
let old = self.map.remove(view).expect("looked up above");
|
|
302
|
+
let opts = OpenOptions {
|
|
303
|
+
view: view.to_string(),
|
|
304
|
+
window: old.window,
|
|
305
|
+
url: current_url(&old),
|
|
306
|
+
allow: (*old.allow).clone(),
|
|
307
|
+
bounds: old.bounds,
|
|
308
|
+
init_script: old.init_script.clone(),
|
|
309
|
+
muted: old.muted,
|
|
310
|
+
autoplay: old.autoplay,
|
|
311
|
+
console: old.console,
|
|
312
|
+
};
|
|
313
|
+
let kept = (old.restarts, old.gone_streak, old.gone_at, old.visible);
|
|
314
|
+
let cursor_hidden = old.cursor_hidden;
|
|
315
|
+
drop(old);
|
|
316
|
+
let mut fresh = build(window, inner, data_root, devtools, background, &opts)?;
|
|
317
|
+
(fresh.restarts, fresh.gone_streak, fresh.gone_at, fresh.visible) = kept;
|
|
318
|
+
if !fresh.visible {
|
|
319
|
+
let _ = fresh.webview.set_visible(false);
|
|
320
|
+
}
|
|
321
|
+
if cursor_hidden {
|
|
322
|
+
fresh.cursor_hidden = true;
|
|
323
|
+
platform::set_cursor_hidden(&fresh.webview, true, &fresh.cursor);
|
|
324
|
+
}
|
|
325
|
+
self.map.insert(view.to_string(), fresh);
|
|
326
|
+
Ok(())
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// The window hid or showed its pointer: its views follow, and keep doing so across their navigations (Windows).
|
|
330
|
+
pub(crate) fn set_cursor_hidden(&mut self, window: u32, hidden: bool) {
|
|
331
|
+
for view in self.map.values_mut().filter(|v| v.window == window && v.cursor_hidden != hidden) {
|
|
332
|
+
view.cursor_hidden = hidden;
|
|
333
|
+
platform::set_cursor_hidden(&view.webview, hidden, &view.cursor);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/// The id of the window a view is in.
|
|
338
|
+
pub(crate) fn window_of(&self, view: &str) -> Option<u32> {
|
|
339
|
+
self.map.get(view).map(|v| v.window)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
fn build(
|
|
344
|
+
window: &tao::window::Window,
|
|
345
|
+
inner: LogicalSize<f64>,
|
|
346
|
+
data_root: &Path,
|
|
347
|
+
devtools: bool,
|
|
348
|
+
background: RGBA,
|
|
349
|
+
opts: &OpenOptions,
|
|
350
|
+
) -> Result<View, String> {
|
|
351
|
+
#[cfg(target_os = "linux")]
|
|
352
|
+
{
|
|
353
|
+
let _ = (window, inner, data_root, devtools, background, opts);
|
|
354
|
+
return Err("UNSUPPORTED: external views are not on Linux yet".into());
|
|
355
|
+
}
|
|
356
|
+
#[cfg(not(target_os = "linux"))]
|
|
357
|
+
{
|
|
358
|
+
let data: PathBuf = data_root.join(folder(&opts.view));
|
|
359
|
+
let mut context = wry::WebContext::new(Some(data));
|
|
360
|
+
let allow = Arc::new(opts.allow.clone());
|
|
361
|
+
let nav_allow = allow.clone();
|
|
362
|
+
let (view_id, window_id) = (opts.view.clone(), opts.window);
|
|
363
|
+
let nav_view = view_id.clone();
|
|
364
|
+
let mut builder = WebViewBuilder::new_with_web_context(&mut context)
|
|
365
|
+
.with_url(&opts.url)
|
|
366
|
+
.with_bounds(rect(opts.bounds, inner))
|
|
367
|
+
// WebView2 paints a new view white until the remote page's first frame; the app's backgroundColor covers that gap.
|
|
368
|
+
.with_background_color(background)
|
|
369
|
+
.with_autoplay(opts.autoplay)
|
|
370
|
+
.with_devtools(devtools)
|
|
371
|
+
.with_navigation_handler(move |url| {
|
|
372
|
+
let ok = allowed(&nav_allow, &url);
|
|
373
|
+
if !ok {
|
|
374
|
+
log!("external view {nav_view}: refused a navigation to {url}, which is on no origin it was opened for");
|
|
375
|
+
}
|
|
376
|
+
ok
|
|
377
|
+
})
|
|
378
|
+
.with_new_window_req_handler(|_, _| NewWindowResponse::Deny)
|
|
379
|
+
.with_download_started_handler(|_, _| false)
|
|
380
|
+
.with_permission_handler(|_| PermissionResponse::Deny)
|
|
381
|
+
.with_on_page_load_handler(move |ev, url| {
|
|
382
|
+
let state = match ev {
|
|
383
|
+
PageLoadEvent::Started => "started",
|
|
384
|
+
PageLoadEvent::Finished => "finished",
|
|
385
|
+
};
|
|
386
|
+
push_event(&event(&view_id, window_id, &format!(r#""event":"load","state":"{state}","url":{}"#, json::quote(&url))));
|
|
387
|
+
});
|
|
388
|
+
if let Some(script) = &opts.init_script {
|
|
389
|
+
builder = builder.with_initialization_script_for_main_only(script.clone(), true);
|
|
390
|
+
}
|
|
391
|
+
#[cfg(target_os = "macos")]
|
|
392
|
+
{
|
|
393
|
+
use wry::WebViewBuilderExtDarwin;
|
|
394
|
+
builder = builder.with_data_store_identifier(store_id(&opts.view));
|
|
395
|
+
}
|
|
396
|
+
let webview = builder.build_as_child(window).map_err(|e| format!("external view: {e}"))?;
|
|
397
|
+
platform::watch(&webview, &opts.view, opts.window, opts.console);
|
|
398
|
+
if opts.muted {
|
|
399
|
+
platform::set_muted(&webview, true)?;
|
|
400
|
+
}
|
|
401
|
+
Ok(View {
|
|
402
|
+
webview,
|
|
403
|
+
_context: context,
|
|
404
|
+
window: opts.window,
|
|
405
|
+
bounds: opts.bounds,
|
|
406
|
+
allow,
|
|
407
|
+
url: opts.url.clone(),
|
|
408
|
+
visible: true,
|
|
409
|
+
restarts: 0,
|
|
410
|
+
gone_streak: 0,
|
|
411
|
+
gone_at: None,
|
|
412
|
+
dead: false,
|
|
413
|
+
init_script: opts.init_script.clone(),
|
|
414
|
+
muted: opts.muted,
|
|
415
|
+
autoplay: opts.autoplay,
|
|
416
|
+
console: opts.console,
|
|
417
|
+
cursor_hidden: false,
|
|
418
|
+
cursor: platform::Cursor::default(),
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#[cfg(target_os = "windows")]
|
|
424
|
+
mod platform {
|
|
425
|
+
use std::{cell::RefCell, rc::Rc};
|
|
426
|
+
|
|
427
|
+
use webview2_com::{
|
|
428
|
+
take_pwstr, AddScriptToExecuteOnDocumentCreatedCompletedHandler, CallDevToolsProtocolMethodCompletedHandler,
|
|
429
|
+
DevToolsProtocolEventReceivedEventHandler, Microsoft::Web::WebView2::Win32::*, NavigationCompletedEventHandler,
|
|
430
|
+
ProcessFailedEventHandler,
|
|
431
|
+
};
|
|
432
|
+
use windows::core::{Interface, BOOL, HSTRING, PWSTR};
|
|
433
|
+
use wry::{WebView, WebViewExtWindows};
|
|
434
|
+
|
|
435
|
+
use super::event;
|
|
436
|
+
use crate::{json, push_event, send, UserEvent};
|
|
437
|
+
|
|
438
|
+
const HIDE: &str = r#"(()=>{const id="__akan_cursor_none";const add=()=>{if(document.getElementById(id))return;const s=document.createElement("style");s.id=id;s.textContent="*,*::before,*::after{cursor:none!important}";(document.head||document.documentElement).appendChild(s);};if(document.documentElement)add();else document.addEventListener("DOMContentLoaded",add,{once:true});})();"#;
|
|
439
|
+
const SHOW: &str = r#"document.getElementById("__akan_cursor_none")?.remove();"#;
|
|
440
|
+
|
|
441
|
+
/// The script WebView2 runs in each new document while the pointer is hidden, by the id it was registered under.
|
|
442
|
+
#[derive(Default)]
|
|
443
|
+
pub(crate) struct CursorScript {
|
|
444
|
+
id: Option<String>,
|
|
445
|
+
wanted: bool,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
pub(crate) type Cursor = Rc<RefCell<CursorScript>>;
|
|
449
|
+
|
|
450
|
+
//? The view's own browser process draws its pointer, so the window's hidden one returns over it; a style in each of
|
|
451
|
+
//? its documents (iframes included) hides it there, registered while hidden and removed when shown again.
|
|
452
|
+
pub(super) fn set_cursor_hidden(webview: &WebView, hidden: bool, cursor: &Cursor) {
|
|
453
|
+
let core = webview.webview();
|
|
454
|
+
let _ = webview.evaluate_script(if hidden { HIDE } else { SHOW });
|
|
455
|
+
let mut state = cursor.borrow_mut();
|
|
456
|
+
state.wanted = hidden;
|
|
457
|
+
if !hidden {
|
|
458
|
+
if let Some(id) = state.id.take() {
|
|
459
|
+
let _ = unsafe { core.RemoveScriptToExecuteOnDocumentCreated(&HSTRING::from(id)) };
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if state.id.is_some() {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
let (shared, owner) = (cursor.clone(), core.clone());
|
|
467
|
+
let handler = AddScriptToExecuteOnDocumentCreatedCompletedHandler::create(Box::new(move |result, id| {
|
|
468
|
+
if result.is_err() {
|
|
469
|
+
return Ok(());
|
|
470
|
+
}
|
|
471
|
+
let mut state = shared.borrow_mut();
|
|
472
|
+
//? Shown again before the registration landed: undo it rather than keep a stale one.
|
|
473
|
+
if state.wanted && state.id.is_none() {
|
|
474
|
+
state.id = Some(id);
|
|
475
|
+
} else {
|
|
476
|
+
let _ = unsafe { owner.RemoveScriptToExecuteOnDocumentCreated(&HSTRING::from(id)) };
|
|
477
|
+
}
|
|
478
|
+
Ok(())
|
|
479
|
+
}));
|
|
480
|
+
let _ = unsafe { core.AddScriptToExecuteOnDocumentCreated(&HSTRING::from(HIDE), &handler) };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
pub(super) fn set_muted(webview: &WebView, muted: bool) -> Result<(), String> {
|
|
484
|
+
let wv: ICoreWebView2_8 = webview.webview().cast().map_err(|e| format!("UNSUPPORTED: this WebView2 Runtime has no per-view mute ({e})"))?;
|
|
485
|
+
unsafe { wv.SetIsMuted(muted) }.map_err(|e| e.to_string())
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
pub(super) fn cdp(webview: &WebView, method: &str, params: &str, done: impl FnOnce(Result<String, String>) + 'static) -> Result<(), String> {
|
|
489
|
+
let name = method.to_string();
|
|
490
|
+
let handler = CallDevToolsProtocolMethodCompletedHandler::create(Box::new(move |result, value| {
|
|
491
|
+
done(result.map(|_| value).map_err(|e| format!("{name}: {e}")));
|
|
492
|
+
Ok(())
|
|
493
|
+
}));
|
|
494
|
+
unsafe { webview.webview().CallDevToolsProtocolMethod(&HSTRING::from(method), &HSTRING::from(params), &handler) }
|
|
495
|
+
.map_err(|e| e.to_string())
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
fn error_name(status: COREWEBVIEW2_WEB_ERROR_STATUS) -> String {
|
|
499
|
+
match status {
|
|
500
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_CANNOT_CONNECT => "cannotConnect".into(),
|
|
501
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_HOST_NAME_NOT_RESOLVED => "hostNameNotResolved".into(),
|
|
502
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_DISCONNECTED => "disconnected".into(),
|
|
503
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_TIMEOUT => "timeout".into(),
|
|
504
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_SERVER_UNREACHABLE => "serverUnreachable".into(),
|
|
505
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_CONNECTION_ABORTED => "connectionAborted".into(),
|
|
506
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_CONNECTION_RESET => "connectionReset".into(),
|
|
507
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_REDIRECT_FAILED => "redirectFailed".into(),
|
|
508
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_EXPIRED
|
|
509
|
+
| COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_IS_INVALID
|
|
510
|
+
| COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_REVOKED
|
|
511
|
+
| COREWEBVIEW2_WEB_ERROR_STATUS_CERTIFICATE_COMMON_NAME_IS_INCORRECT => "certificate".into(),
|
|
512
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_UNEXPECTED_ERROR => "unexpected".into(),
|
|
513
|
+
//? What WebView2 reports for a refused connection, too (verified on 127.0.0.1).
|
|
514
|
+
COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN => "unknown".into(),
|
|
515
|
+
other => format!("status{}", other.0),
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/// A navigation that failed (the network, DNS, a certificate) and the web process ending; with `console`, the
|
|
520
|
+
/// page's console calls and uncaught exceptions as the protocol's own events.
|
|
521
|
+
pub(super) fn watch(webview: &WebView, view: &str, window: u32, console: bool) {
|
|
522
|
+
let core = webview.webview();
|
|
523
|
+
let failed_view = view.to_string();
|
|
524
|
+
let navigation = NavigationCompletedEventHandler::create(Box::new(move |sender, args| {
|
|
525
|
+
let (Some(sender), Some(args)) = (sender, args) else { return Ok(()) };
|
|
526
|
+
let mut ok = BOOL::default();
|
|
527
|
+
unsafe { args.IsSuccess(&mut ok)? };
|
|
528
|
+
if ok.as_bool() {
|
|
529
|
+
return Ok(());
|
|
530
|
+
}
|
|
531
|
+
let mut status = COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN;
|
|
532
|
+
unsafe { args.WebErrorStatus(&mut status)? };
|
|
533
|
+
//? A refused navigation (navigation handler) and one a newer navigation replaced end as cancelled: not a failure.
|
|
534
|
+
if status == COREWEBVIEW2_WEB_ERROR_STATUS_OPERATION_CANCELED {
|
|
535
|
+
return Ok(());
|
|
536
|
+
}
|
|
537
|
+
let mut uri = PWSTR::null();
|
|
538
|
+
unsafe { sender.Source(&mut uri)? };
|
|
539
|
+
let url = take_pwstr(uri);
|
|
540
|
+
//? A server's error page is a failure too, and its status says more than WebErrorStatus does (Runtime 1.0.1245+).
|
|
541
|
+
let mut http = 0;
|
|
542
|
+
if let Ok(args2) = args.cast::<ICoreWebView2NavigationCompletedEventArgs2>() {
|
|
543
|
+
let _ = unsafe { args2.HttpStatusCode(&mut http) };
|
|
544
|
+
}
|
|
545
|
+
let code = if http >= 400 { format!("http{http}") } else { error_name(status) };
|
|
546
|
+
push_event(&event(
|
|
547
|
+
&failed_view,
|
|
548
|
+
window,
|
|
549
|
+
&format!(r#""event":"loadFailed","url":{},"errorCode":"{code}""#, json::quote(&url)),
|
|
550
|
+
));
|
|
551
|
+
Ok(())
|
|
552
|
+
}));
|
|
553
|
+
let mut token = 0;
|
|
554
|
+
if let Err(e) = unsafe { core.add_NavigationCompleted(&navigation, &mut token) } {
|
|
555
|
+
log!("external view {view}: failed navigations are not watched: {e}");
|
|
556
|
+
}
|
|
557
|
+
let gone_view = view.to_string();
|
|
558
|
+
let process = ProcessFailedEventHandler::create(Box::new(move |_, args| {
|
|
559
|
+
let Some(args) = args else { return Ok(()) };
|
|
560
|
+
let mut kind = COREWEBVIEW2_PROCESS_FAILED_KIND_RENDER_PROCESS_EXITED;
|
|
561
|
+
unsafe { args.ProcessFailedKind(&mut kind)? };
|
|
562
|
+
let reason = match kind {
|
|
563
|
+
COREWEBVIEW2_PROCESS_FAILED_KIND_BROWSER_PROCESS_EXITED => "browserExited",
|
|
564
|
+
COREWEBVIEW2_PROCESS_FAILED_KIND_RENDER_PROCESS_EXITED => "crashed",
|
|
565
|
+
COREWEBVIEW2_PROCESS_FAILED_KIND_RENDER_PROCESS_UNRESPONSIVE => "unresponsive",
|
|
566
|
+
_ => return Ok(()),
|
|
567
|
+
};
|
|
568
|
+
send(UserEvent::ExternalGone { view: gone_view.clone(), reason });
|
|
569
|
+
Ok(())
|
|
570
|
+
}));
|
|
571
|
+
let mut token = 0;
|
|
572
|
+
if let Err(e) = unsafe { core.add_ProcessFailed(&process, &mut token) } {
|
|
573
|
+
log!("external view {view}: web process failures are not watched: {e}");
|
|
574
|
+
}
|
|
575
|
+
if !console {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
for method in ["Runtime.consoleAPICalled", "Runtime.exceptionThrown"] {
|
|
579
|
+
let (cdp_view, name) = (view.to_string(), method.to_string());
|
|
580
|
+
let handler = DevToolsProtocolEventReceivedEventHandler::create(Box::new(move |_, args| {
|
|
581
|
+
let Some(args) = args else { return Ok(()) };
|
|
582
|
+
let mut params = PWSTR::null();
|
|
583
|
+
unsafe { args.ParameterObjectAsJson(&mut params)? };
|
|
584
|
+
let params = take_pwstr(params);
|
|
585
|
+
push_event(&event(&cdp_view, window, &format!(r#""event":"cdp","method":"{name}","params":{params}"#)));
|
|
586
|
+
Ok(())
|
|
587
|
+
}));
|
|
588
|
+
let receiver = unsafe { core.GetDevToolsProtocolEventReceiver(&HSTRING::from(method)) };
|
|
589
|
+
let mut token = 0;
|
|
590
|
+
if let Err(e) = receiver.and_then(|r| unsafe { r.add_DevToolsProtocolEventReceived(&handler, &mut token) }) {
|
|
591
|
+
log!("external view {view}: {method} is not watched: {e}");
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if let Err(e) = cdp(webview, "Runtime.enable", "{}", |_| {}) {
|
|
595
|
+
log!("external view {view}: Runtime.enable: {e}");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#[cfg(not(target_os = "windows"))]
|
|
601
|
+
mod platform {
|
|
602
|
+
use wry::WebView;
|
|
603
|
+
|
|
604
|
+
pub(crate) type Cursor = ();
|
|
605
|
+
|
|
606
|
+
pub(super) fn set_cursor_hidden(_webview: &WebView, _hidden: bool, _cursor: &Cursor) {}
|
|
607
|
+
|
|
608
|
+
pub(super) fn set_muted(_webview: &WebView, _muted: bool) -> Result<(), String> {
|
|
609
|
+
Err("UNSUPPORTED: a view is muted on Windows only".into())
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
pub(super) fn cdp(_webview: &WebView, method: &str, _params: &str, _done: impl FnOnce(Result<String, String>) + 'static) -> Result<(), String> {
|
|
613
|
+
Err(format!("UNSUPPORTED: {method} reaches a view on Windows only"))
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
|
617
|
+
pub(super) fn watch(_webview: &WebView, _view: &str, _window: u32, _console: bool) {}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#[cfg(test)]
|
|
621
|
+
mod tests {
|
|
622
|
+
use super::*;
|
|
623
|
+
|
|
624
|
+
#[test]
|
|
625
|
+
fn an_origin_is_scheme_and_host_without_the_default_port() {
|
|
626
|
+
assert_eq!(origin_of("https://Design.Example.com/screen/1?x=2").as_deref(), Some("https://design.example.com"));
|
|
627
|
+
assert_eq!(origin_of("https://design.example.com:443/").as_deref(), Some("https://design.example.com"));
|
|
628
|
+
assert_eq!(origin_of("http://10.0.0.5:8080/a").as_deref(), Some("http://10.0.0.5:8080"));
|
|
629
|
+
assert_eq!(origin_of("https://user@evil.example/"), None);
|
|
630
|
+
assert_eq!(origin_of("file:///C:/a"), None);
|
|
631
|
+
assert_eq!(origin_of("javascript:alert(1)"), None);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
#[test]
|
|
635
|
+
fn navigation_stays_on_the_origins_a_view_was_opened_for() {
|
|
636
|
+
let allow = vec!["https://design.example.com".to_string()];
|
|
637
|
+
assert!(allowed(&allow, "https://design.example.com/screen/1/2"));
|
|
638
|
+
assert!(allowed(&allow, "about:blank"));
|
|
639
|
+
assert!(!allowed(&allow, "https://design.example.com.evil.example/"));
|
|
640
|
+
assert!(!allowed(&allow, "https://other.example/"));
|
|
641
|
+
assert!(!allowed(&allow, "app://localhost/"));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
#[test]
|
|
645
|
+
fn a_view_id_makes_a_safe_folder_and_a_stable_store_id() {
|
|
646
|
+
assert_eq!(folder("cus-board_1"), "cus-board_1");
|
|
647
|
+
assert_eq!(folder("../x"), "___x");
|
|
648
|
+
assert_eq!(store_id("cus"), store_id("cus"));
|
|
649
|
+
assert_ne!(store_id("cus"), store_id("cus2"));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
@@ -40,6 +40,29 @@ impl V {
|
|
|
40
40
|
None
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
pub fn as_array(&self) -> Option<&[V]> {
|
|
44
|
+
if let V::Arr(a) = self {
|
|
45
|
+
Some(a)
|
|
46
|
+
} else {
|
|
47
|
+
None
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// `v` written back as JSON: a page's argument passed on to an engine that takes JSON text.
|
|
53
|
+
pub fn stringify(v: &V) -> String {
|
|
54
|
+
match v {
|
|
55
|
+
V::Null => "null".into(),
|
|
56
|
+
V::Bool(b) => b.to_string(),
|
|
57
|
+
V::Num(n) if n.is_finite() => n.to_string(),
|
|
58
|
+
V::Num(_) => "null".into(),
|
|
59
|
+
V::Str(s) => quote(s),
|
|
60
|
+
V::Arr(items) => format!("[{}]", items.iter().map(stringify).collect::<Vec<_>>().join(",")),
|
|
61
|
+
V::Obj(entries) => format!(
|
|
62
|
+
"{{{}}}",
|
|
63
|
+
entries.iter().map(|(k, v)| format!("{}:{}", quote(k), stringify(v))).collect::<Vec<_>>().join(",")
|
|
64
|
+
),
|
|
65
|
+
}
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
/// Deeper nesting is refused: each level is a recursive call, and a page could send an array nested
|