diff --git a/v2/internal/frontend/desktop/darwin/CustomProtocol.h b/v2/internal/frontend/desktop/darwin/CustomProtocol.h new file mode 100644 index 000000000..da0e7079f --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/CustomProtocol.h @@ -0,0 +1,14 @@ +#ifndef CustomProtocol_h +#define CustomProtocol_h + +#import + +extern void HandleCustomProtocol(char*); + +@interface CustomProtocolSchemeHandler : NSObject ++ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent; +@end + +void StartCustomProtocolHandler(void); + +#endif /* CustomProtocol_h */ diff --git a/v2/internal/frontend/desktop/darwin/CustomProtocol.m b/v2/internal/frontend/desktop/darwin/CustomProtocol.m new file mode 100644 index 000000000..7365e4f50 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/CustomProtocol.m @@ -0,0 +1,20 @@ +#include "CustomProtocol.h" + +@implementation CustomProtocolSchemeHandler ++ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + [event paramDescriptorForKeyword:keyDirectObject]; + + NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + + HandleCustomProtocol((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]); +} +@end + +void StartCustomProtocolHandler(void) { + NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; + + [appleEventManager setEventHandler:[CustomProtocolSchemeHandler class] + andSelector:@selector(handleGetURLEvent:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID: kAEGetURL]; +} diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index 7a4905eeb..c792ae9c8 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -8,6 +8,7 @@ package darwin #cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit #import #import "Application.h" +#import "CustomProtocol.h" #import "WailsContext.h" #include @@ -39,6 +40,7 @@ var messageBuffer = make(chan string, 100) var requestBuffer = make(chan webview.Request, 100) var callbackBuffer = make(chan uint, 10) var openFilepathBuffer = make(chan string, 100) +var openUrlBuffer = make(chan string, 100) var secondInstanceBuffer = make(chan options.SecondInstanceData, 1) type Frontend struct { @@ -79,6 +81,9 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. } result.startURL, _ = url.Parse(startURL) + // this should be initialized as early as possible to handle first instance launch + C.StartCustomProtocolHandler() + if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil { result.startURL = _starturl } else { @@ -110,6 +115,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. go result.startMessageProcessor() go result.startCallbackProcessor() go result.startFileOpenProcessor() + go result.startUrlOpenProcessor() go result.startSecondInstanceProcessor() return result @@ -121,6 +127,12 @@ func (f *Frontend) startFileOpenProcessor() { } } +func (f *Frontend) startUrlOpenProcessor() { + for url := range openUrlBuffer { + f.ProcessOpenUrlEvent(url) + } +} + func (f *Frontend) startSecondInstanceProcessor() { for secondInstanceData := range secondInstanceBuffer { if f.frontendOptions.SingleInstanceLock != nil && @@ -385,6 +397,12 @@ func (f *Frontend) ProcessOpenFileEvent(filePath string) { } } +func (f *Frontend) ProcessOpenUrlEvent(url string) { + if f.frontendOptions.Mac != nil && f.frontendOptions.Mac.OnUrlOpen != nil { + f.frontendOptions.Mac.OnUrlOpen(url) + } +} + func (f *Frontend) Callback(message string) { escaped, err := json.Marshal(message) if err != nil { @@ -434,3 +452,9 @@ func HandleOpenFile(filePath *C.char) { goFilepath := C.GoString(filePath) openFilepathBuffer <- goFilepath } + +//export HandleCustomProtocol +func HandleCustomProtocol(url *C.char) { + goUrl := C.GoString(url) + openUrlBuffer <- goUrl +} diff --git a/v2/internal/project/project.go b/v2/internal/project/project.go index a0bf518a9..d42977fee 100644 --- a/v2/internal/project/project.go +++ b/v2/internal/project/project.go @@ -222,6 +222,7 @@ type Info struct { Copyright *string `json:"copyright"` Comments *string `json:"comments"` FileAssociations []FileAssociation `json:"fileAssociations"` + Protocols []Protocol `json:"protocols"` } type FileAssociation struct { @@ -232,6 +233,12 @@ type FileAssociation struct { Role string `json:"role"` } +type Protocol struct { + Scheme string `json:"scheme"` + Description string `json:"description"` + Role string `json:"role"` +} + type Bindings struct { TsGeneration TsGeneration `json:"ts_generation"` } diff --git a/v2/pkg/buildassets/build/darwin/Info.dev.plist b/v2/pkg/buildassets/build/darwin/Info.dev.plist index 9d4096f4f..04727c23f 100644 --- a/v2/pkg/buildassets/build/darwin/Info.dev.plist +++ b/v2/pkg/buildassets/build/darwin/Info.dev.plist @@ -42,6 +42,23 @@ {{end}} {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} NSAppTransportSecurity NSAllowsLocalNetworking diff --git a/v2/pkg/buildassets/build/darwin/Info.plist b/v2/pkg/buildassets/build/darwin/Info.plist index 079bdafa9..19cc9370c 100644 --- a/v2/pkg/buildassets/build/darwin/Info.plist +++ b/v2/pkg/buildassets/build/darwin/Info.plist @@ -42,5 +42,22 @@ {{end}} {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} diff --git a/v2/pkg/buildassets/build/windows/installer/project.nsi b/v2/pkg/buildassets/build/windows/installer/project.nsi index f18f2b5df..654ae2e49 100644 --- a/v2/pkg/buildassets/build/windows/installer/project.nsi +++ b/v2/pkg/buildassets/build/windows/installer/project.nsi @@ -92,6 +92,7 @@ Section CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols !insertmacro wails.writeUninstaller SectionEnd @@ -107,6 +108,7 @@ Section "uninstall" Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols !insertmacro wails.deleteUninstaller SectionEnd diff --git a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh index bc79fb395..f9c0f8852 100644 --- a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh +++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh @@ -218,3 +218,32 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" Delete "$INSTDIR\{{.IconName}}.ico" {{end}} !macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + {{end}} +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/v2/pkg/options/mac/mac.go b/v2/pkg/options/mac/mac.go index ecd2923fb..5758befd0 100644 --- a/v2/pkg/options/mac/mac.go +++ b/v2/pkg/options/mac/mac.go @@ -24,5 +24,6 @@ type Options struct { //ActivationPolicy ActivationPolicy About *AboutInfo OnFileOpen func(filePath string) `json:"-"` + OnUrlOpen func(filePath string) `json:"-"` //URLHandlers map[string]func(string) } diff --git a/website/docs/guides/custom-protocol-schemes.mdx b/website/docs/guides/custom-protocol-schemes.mdx new file mode 100644 index 000000000..c56634f0e --- /dev/null +++ b/website/docs/guides/custom-protocol-schemes.mdx @@ -0,0 +1,189 @@ +# Custom Protocol Scheme association + +Custom Protocols feature allows you to associate specific custom protocol with your app so that when users open links with this protocol, +your app is launched to handle them. This can be particularly useful to connect your desktop app with your web app. +In this guide, we'll walk through the steps to implement custom protocols in Wails app. + + +## Set Up Custom Protocol Schemes Association: +To set up custom protocol, you need to modify your application's wails.json file. +In "info" section add a "protocols" section specifying the protocols your app should be associated with. + +For example: +```json +{ + "info": { + "protocols": [ + { + "scheme": "myapp", + "description": "My App Protocol", + "role": "Editor" + } + ] + } +} +``` + +| Property | Description | +|:------------|:--------------------------------------------------------------------------------------| +| scheme | Custom Protocol scheme. e.g. myapp | +| description | Windows-only. The description. | +| role | macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole. | + +## Platform Specifics: + +### macOS +When you open custom protocol with your app, the system will launch your app and call the `OnUrlOpen` function in your Wails app. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + Mac: &mac.Options{ + OnUrlOpen: func(url string) { println(url) }, + }, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} +``` + + +### Windows +On Windows Custom Protocol Schemes is supported only with NSIS installer. During installation, the installer will create a +registry entry for your schemes. When you open url with your app, new instance of app is launched and url is passed +as argument to your app. To handle this you should parse command line arguments in your app. Example: +```go title="main.go" +func main() { + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) != 0 { + println("launchArgs", argsWithoutProg) + } +} +``` + +You also can enable single instance lock for your app. In this case, when you open url with your app, new instance of app is not launched +and arguments are passed to already running instance. Check single instance lock guide for details. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` + +### Linux +Currently, Wails doesn't support bundling for Linux. So, you need to create file associations manually. +For example if you distribute your app as a .deb package, you can create file associations by adding required files in you bundle. +You can use [nfpm](https://nfpm.goreleaser.com/) to create .deb package for your app. + +1. Create a .desktop file for your app and specify file associations there (note that `%u` is important in Exec). Example: +```ini +[Desktop Entry] +Categories=Office +Exec=/usr/bin/wails-open-file %u +Icon=wails-open-file.png +Name=wails-open-file +Terminal=false +Type=Application +MimeType=x-scheme-handler/myapp; +``` + +2. Prepare postInstall/postRemove scripts for your package. Example: +```sh +# reload desktop database to load app in list of available +update-desktop-database /usr/share/applications +``` +3. Configure nfpm to use your scripts and files. Example: +```yaml +name: "wails-open-file" +arch: "arm64" +platform: "linux" +version: "1.0.0" +section: "default" +priority: "extra" +maintainer: "FooBarCorp " +description: "Sample Package" +vendor: "FooBarCorp" +homepage: "http://example.com" +license: "MIT" +contents: +- src: ../bin/wails-open-file + dst: /usr/bin/wails-open-file +- src: ./main.desktop + dst: /usr/share/applications/wails-open-file.desktop +- src: ../appicon.svg + dst: /usr/share/icons/hicolor/scalable/apps/wails-open-file.svg +# copy icons to Yaru theme as well. For some reason Ubuntu didn't pick up fileicons from hicolor theme +- src: ../appicon.svg + dst: /usr/share/icons/Yaru/scalable/apps/wails-open-file.svg +scripts: + postinstall: ./postInstall.sh + postremove: ./postRemove.sh +``` +6. Build your .deb package using nfpm: +```sh +nfpm pkg --packager deb --target . +``` +7. Now when your package is installed, your app will be associated with custom protocol scheme. When you open url with your app, +new instance of app is launched and file path is passed as argument to your app. +To handle this you should parse command line arguments in your app. Example: +```go title="main.go" +func main() { + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) != 0 { + println("launchArgs", argsWithoutProg) + } +} +``` + +You also can enable single instance lock for your app. In this case, when you open url with your app, new instance of app is not launched +and arguments are passed to already running instance. Check single instance lock guide for details. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index 7cfa072aa..8b65b5490 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added new community template wails-htmx-templ-chi-tailwind. Added by [@pylotlight](https://github.com/pylotlight) in [PR](https://github.com/wailsapp/wails/pull/2984) - Added CPU/GPU/Memory detection for `wails doctor`. Added by @leaanthony in #d51268b8d0680430f3a614775b13e6cd2b906d1c - The [AssetServer](/docs/reference/options#assetserver) now injects the runtime/IPC into all index html files and into all html files returned when requesting a folder path. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2203) +- Added Custom Protocol Schemes associations support for macOS and Windows. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/3000) ### Changed diff --git a/website/static/schemas/config.v2.json b/website/static/schemas/config.v2.json index 7ab40d95f..f215415e6 100644 --- a/website/static/schemas/config.v2.json +++ b/website/static/schemas/config.v2.json @@ -200,6 +200,31 @@ } } } + }, + "protocols": { + "type": "array", + "description": "Custom URI protocols that should be opened by the application", + "items": { + "type": "object", + "properties": { + "scheme": { + "type": "string", + "description": "protocol scheme. e.g. myapp" + }, + "description": { + "type": "string", + "description": "Windows-only. The description. It is displayed on the `Type` column on Windows Explorer." + }, + "role": { + "description": "macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole.", + "allOf": [ + { + "$ref": "#/definitions/BundleTypeRole" + } + ] + } + } + } } } },