5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 23:51:44 +08:00

Implement custom protocol association support (#3000)

* implement MacOS openFile/openFiles events

* wip: windows file association

* fix macro import

* add file icon copy

* try copy icon

* keep only required part of scripts

* update config schema

* fix json

* set fileAssociation for mac via config

* proper iconName handling

* add fileAssociation icon generator

* fix file association icons bundle

* don't break compatibility

* remove mimeType as not supported linux for now

* add documentation

* adjust config schema

* restore formatting

* try implement single instance lock with params passing

* fix focusing

* fix focusing

* formatting

* use channel buffer for second instance events

* handle errors

* add comment

* remove unused option in file association

* wip: linux single instance lock

* wip: linux single instance

* some experiments with making window active

* try to use unminimise

* remove unused

* try present for window

* try present for window

* fix build

* cleanup

* cleanup

* implement single instance lock on mac os

* implement proper show for windows

* proper unmimimise

* get rid of openFiles mac os. change configuration structure

* remove unused channel

* remove unused function

* add documentation for single instance lock

* add PR link

* wip mac os deeplinks

* put custom url listner on top to catch link on app opening

* put custom url listner on top to catch link on app opening

* try add custom url windows

* adjust custom url

* add docs

* merge master

* update documentation

* add comment for darwin

* add PR link

* change naming

* change naming

* change naming

* change naming

* fix formatting

* fix naming

* Fix typo

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
This commit is contained in:
Andrey Pshenkin 2023-11-02 09:26:05 +00:00 committed by GitHub
parent e960798e85
commit ae688aa07d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 346 additions and 0 deletions

View File

@ -0,0 +1,14 @@
#ifndef CustomProtocol_h
#define CustomProtocol_h
#import <Cocoa/Cocoa.h>
extern void HandleCustomProtocol(char*);
@interface CustomProtocolSchemeHandler : NSObject
+ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
@end
void StartCustomProtocolHandler(void);
#endif /* CustomProtocol_h */

View File

@ -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];
}

View File

@ -8,6 +8,7 @@ package darwin
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit #cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import "Application.h" #import "Application.h"
#import "CustomProtocol.h"
#import "WailsContext.h" #import "WailsContext.h"
#include <stdlib.h> #include <stdlib.h>
@ -39,6 +40,7 @@ var messageBuffer = make(chan string, 100)
var requestBuffer = make(chan webview.Request, 100) var requestBuffer = make(chan webview.Request, 100)
var callbackBuffer = make(chan uint, 10) var callbackBuffer = make(chan uint, 10)
var openFilepathBuffer = make(chan string, 100) var openFilepathBuffer = make(chan string, 100)
var openUrlBuffer = make(chan string, 100)
var secondInstanceBuffer = make(chan options.SecondInstanceData, 1) var secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
type Frontend struct { type Frontend struct {
@ -79,6 +81,9 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
} }
result.startURL, _ = url.Parse(startURL) 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 { if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
result.startURL = _starturl result.startURL = _starturl
} else { } else {
@ -110,6 +115,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
go result.startMessageProcessor() go result.startMessageProcessor()
go result.startCallbackProcessor() go result.startCallbackProcessor()
go result.startFileOpenProcessor() go result.startFileOpenProcessor()
go result.startUrlOpenProcessor()
go result.startSecondInstanceProcessor() go result.startSecondInstanceProcessor()
return result 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() { func (f *Frontend) startSecondInstanceProcessor() {
for secondInstanceData := range secondInstanceBuffer { for secondInstanceData := range secondInstanceBuffer {
if f.frontendOptions.SingleInstanceLock != nil && 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) { func (f *Frontend) Callback(message string) {
escaped, err := json.Marshal(message) escaped, err := json.Marshal(message)
if err != nil { if err != nil {
@ -434,3 +452,9 @@ func HandleOpenFile(filePath *C.char) {
goFilepath := C.GoString(filePath) goFilepath := C.GoString(filePath)
openFilepathBuffer <- goFilepath openFilepathBuffer <- goFilepath
} }
//export HandleCustomProtocol
func HandleCustomProtocol(url *C.char) {
goUrl := C.GoString(url)
openUrlBuffer <- goUrl
}

View File

@ -222,6 +222,7 @@ type Info struct {
Copyright *string `json:"copyright"` Copyright *string `json:"copyright"`
Comments *string `json:"comments"` Comments *string `json:"comments"`
FileAssociations []FileAssociation `json:"fileAssociations"` FileAssociations []FileAssociation `json:"fileAssociations"`
Protocols []Protocol `json:"protocols"`
} }
type FileAssociation struct { type FileAssociation struct {
@ -232,6 +233,12 @@ type FileAssociation struct {
Role string `json:"role"` Role string `json:"role"`
} }
type Protocol struct {
Scheme string `json:"scheme"`
Description string `json:"description"`
Role string `json:"role"`
}
type Bindings struct { type Bindings struct {
TsGeneration TsGeneration `json:"ts_generation"` TsGeneration TsGeneration `json:"ts_generation"`
} }

View File

@ -42,6 +42,23 @@
{{end}} {{end}}
</array> </array>
{{end}} {{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsLocalNetworking</key> <key>NSAllowsLocalNetworking</key>

View File

@ -42,5 +42,22 @@
{{end}} {{end}}
</array> </array>
{{end}} {{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
</dict> </dict>
</plist> </plist>

View File

@ -92,6 +92,7 @@ Section
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
!insertmacro wails.associateFiles !insertmacro wails.associateFiles
!insertmacro wails.associateCustomProtocols
!insertmacro wails.writeUninstaller !insertmacro wails.writeUninstaller
SectionEnd SectionEnd
@ -107,6 +108,7 @@ Section "uninstall"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
!insertmacro wails.unassociateFiles !insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols
!insertmacro wails.deleteUninstaller !insertmacro wails.deleteUninstaller
SectionEnd SectionEnd

View File

@ -218,3 +218,32 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
Delete "$INSTDIR\{{.IconName}}.ico" Delete "$INSTDIR\{{.IconName}}.ico"
{{end}} {{end}}
!macroend !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

View File

@ -24,5 +24,6 @@ type Options struct {
//ActivationPolicy ActivationPolicy //ActivationPolicy ActivationPolicy
About *AboutInfo About *AboutInfo
OnFileOpen func(filePath string) `json:"-"` OnFileOpen func(filePath string) `json:"-"`
OnUrlOpen func(filePath string) `json:"-"`
//URLHandlers map[string]func(string) //URLHandlers map[string]func(string)
} }

View File

@ -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 apps 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 <FooBarCorp@gmail.com>"
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,
},
})
}
```

View File

@ -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 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 - 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) - 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 ### Changed

View File

@ -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 apps role with respect to the type. Corresponds to CFBundleTypeRole.",
"allOf": [
{
"$ref": "#/definitions/BundleTypeRole"
}
]
}
}
}
} }
} }
}, },