From c40aee3db20fcf0ed39bc279a2e90ab74ad74993 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Thu, 14 Sep 2023 00:06:17 +0100 Subject: [PATCH 01/60] implement MacOS openFile/openFiles events --- v2/internal/app/app_bindings.go | 2 + v2/internal/app/app_dev.go | 2 + v2/internal/app/app_production.go | 2 + .../frontend/desktop/darwin/AppDelegate.h | 6 ++- .../frontend/desktop/darwin/AppDelegate.m | 26 ++++++++- .../frontend/desktop/darwin/frontend.go | 54 +++++++++++++++++++ v2/pkg/options/options.go | 2 + 7 files changed, 92 insertions(+), 2 deletions(-) diff --git a/v2/internal/app/app_bindings.go b/v2/internal/app/app_bindings.go index d079790aa7f..5b94eafad39 100644 --- a/v2/internal/app/app_bindings.go +++ b/v2/internal/app/app_bindings.go @@ -24,6 +24,8 @@ func (a *App) Run() error { a.options.OnShutdown, a.options.OnDomReady, a.options.OnBeforeClose, + a.options.OnFileOpen, + a.options.OnFilesOpen, } // Check for CLI Flags diff --git a/v2/internal/app/app_dev.go b/v2/internal/app/app_dev.go index 2e04f448bac..85313b556ba 100644 --- a/v2/internal/app/app_dev.go +++ b/v2/internal/app/app_dev.go @@ -208,6 +208,8 @@ func CreateApp(appoptions *options.App) (*App, error) { appoptions.OnShutdown, appoptions.OnDomReady, appoptions.OnBeforeClose, + appoptions.OnFileOpen, + appoptions.OnFilesOpen, } appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false) diff --git a/v2/internal/app/app_production.go b/v2/internal/app/app_production.go index 5b847260cd8..edc60582c44 100644 --- a/v2/internal/app/app_production.go +++ b/v2/internal/app/app_production.go @@ -71,6 +71,8 @@ func CreateApp(appoptions *options.App) (*App, error) { appoptions.OnShutdown, appoptions.OnDomReady, appoptions.OnBeforeClose, + appoptions.OnFileOpen, + appoptions.OnFilesOpen, } appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated()) eventHandler := runtime.NewEvents(myLogger) diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.h b/v2/internal/frontend/desktop/darwin/AppDelegate.h index e2dd841c91e..06c8f35a69e 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.h +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.h @@ -11,7 +11,7 @@ #import #import "WailsContext.h" -@interface AppDelegate : NSResponder +@interface AppDelegate : NSResponder @property bool alwaysOnTop; @property bool startHidden; @@ -20,4 +20,8 @@ @end +extern void HandleOpenFile(char *); + +extern void HandleOpenFiles(char**, int); + #endif /* AppDelegate_h */ diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.m b/v2/internal/frontend/desktop/darwin/AppDelegate.m index 6d46deae452..507bfc9e70b 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.m +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.m @@ -11,9 +11,33 @@ #import "AppDelegate.h" @implementation AppDelegate +// openFile implemented as well just in case, but it's not called (at least we don't know how to call it) +-(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename +{ + const char* utf8FileName = filename.UTF8String; + HandleOpenFile((char*)utf8FileName); + return YES; +} + +// for some reasons it's triggered even when only one file is opened, instead of openFile. +-(void)application:(NSApplication *)sender openFiles:(NSArray *)filenames +{ + int count = [filenames count]; + int i; + char **fileNamesArray = NULL; + fileNamesArray = (char**)realloc(fileNamesArray, i*sizeof(*fileNamesArray)); + for (i=0; i Date: Sat, 16 Sep 2023 12:27:24 +0100 Subject: [PATCH 02/60] wip: windows file association --- v2/internal/project/project.go | 20 ++- .../build/windows/installer/fileassoc.nsh | 135 ++++++++++++++++++ .../build/windows/installer/project.nsi | 20 ++- .../build/windows/installer/wails_tools.nsh | 23 ++- website/static/schemas/config.v2.json | 35 ++++- 5 files changed, 217 insertions(+), 16 deletions(-) create mode 100644 v2/pkg/buildassets/build/windows/installer/fileassoc.nsh diff --git a/v2/internal/project/project.go b/v2/internal/project/project.go index 023ca1dfed2..82ea54b7472 100644 --- a/v2/internal/project/project.go +++ b/v2/internal/project/project.go @@ -216,11 +216,21 @@ type Author struct { } type Info struct { - CompanyName string `json:"companyName"` - ProductName string `json:"productName"` - ProductVersion string `json:"productVersion"` - Copyright *string `json:"copyright"` - Comments *string `json:"comments"` + CompanyName string `json:"companyName"` + ProductName string `json:"productName"` + ProductVersion string `json:"productVersion"` + Copyright *string `json:"copyright"` + Comments *string `json:"comments"` + FileAssociations []FileAssociation `json:"fileAssociations"` +} + +type FileAssociation struct { + Ext string `json:"ext"` + Name string `json:"name"` + Description string `json:"description"` + MimeType string `json:"mimeType"` + IconPath string `json:"iconPath"` + Role string `json:"role"` } type Bindings struct { diff --git a/v2/pkg/buildassets/build/windows/installer/fileassoc.nsh b/v2/pkg/buildassets/build/windows/installer/fileassoc.nsh new file mode 100644 index 00000000000..6179727fbbb --- /dev/null +++ b/v2/pkg/buildassets/build/windows/installer/fileassoc.nsh @@ -0,0 +1,135 @@ +; from https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +; fileassoc.nsh +; File association helper macros +; Written by Saivert +; +; Improved by Nikku. +; +; Features automatic backup system and UPDATEFILEASSOC macro for +; shell change notification. +; +; |> How to use <| +; To associate a file with an application so you can double-click it in explorer, use +; the APP_ASSOCIATE macro like this: +; +; Example: +; !insertmacro APP_ASSOCIATE "txt" "myapp.textfile" "Description of txt files" \ +; "$INSTDIR\myapp.exe,0" "Open with myapp" "$INSTDIR\myapp.exe $\"%1$\"" +; +; Never insert the APP_ASSOCIATE macro multiple times, it is only ment +; to associate an application with a single file and using the +; the "open" verb as default. To add more verbs (actions) to a file +; use the APP_ASSOCIATE_ADDVERB macro. +; +; Example: +; !insertmacro APP_ASSOCIATE_ADDVERB "myapp.textfile" "edit" "Edit with myapp" \ +; "$INSTDIR\myapp.exe /edit $\"%1$\"" +; +; To have access to more options when registering the file association use the +; APP_ASSOCIATE_EX macro. Here you can specify the verb and what verb is to be the +; standard action (default verb). +; +; Note, that this script takes into account user versus global installs. +; To properly work you must initialize the SHELL_CONTEXT variable via SetShellVarContext. +; +; And finally: To remove the association from the registry use the APP_UNASSOCIATE +; macro. Here is another example just to wrap it up: +; !insertmacro APP_UNASSOCIATE "txt" "myapp.textfile" +; +; |> Note <| +; When defining your file class string always use the short form of your application title +; then a period (dot) and the type of file. This keeps the file class sort of unique. +; Examples: +; Winamp.Playlist +; NSIS.Script +; Photoshop.JPEGFile +; +; |> Tech info <| +; The registry key layout for a global file association is: +; +; HKEY_LOCAL_MACHINE\Software\Classes +; <".ext"> = +; = <"description"> +; shell +; = <"menu-item text"> +; command = <"command string"> +; +; +; The registry key layout for a per-user file association is: +; +; HKEY_CURRENT_USER\Software\Classes +; <".ext"> = +; = <"description"> +; shell +; = <"menu-item text"> +; command = <"command string"> +; + +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_ASSOCIATE_EX EXT FILECLASS DESCRIPTION ICON VERB DEFAULTVERB SHELLNEW COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + StrCmp "${SHELLNEW}" "0" +2 + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}\ShellNew" "NullFile" "" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" `${DEFAULTVERB}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}` +!macroend + +!macro APP_ASSOCIATE_ADDVERB FILECLASS VERB COMMANDTEXT COMMAND + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}` +!macroend + +!macro APP_ASSOCIATE_REMOVEVERB FILECLASS VERB + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}\shell\${VERB}` +!macroend + + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro APP_ASSOCIATE_GETFILECLASS OUTPUT EXT + ReadRegStr ${OUTPUT} SHELL_CONTEXT "Software\Classes\.${EXT}" "" +!macroend + + +; !defines for use with SHChangeNotify +!ifdef SHCNE_ASSOCCHANGED +!undef SHCNE_ASSOCCHANGED +!endif +!define SHCNE_ASSOCCHANGED 0x08000000 +!ifdef SHCNF_FLUSH +!undef SHCNF_FLUSH +!endif +!define SHCNF_FLUSH 0x1000 + +!macro UPDATEFILEASSOC +; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we +; can update the shell. + System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)" +!macroend diff --git a/v2/pkg/buildassets/build/windows/installer/project.nsi b/v2/pkg/buildassets/build/windows/installer/project.nsi index 13cc4f023bc..29b824d0730 100644 --- a/v2/pkg/buildassets/build/windows/installer/project.nsi +++ b/v2/pkg/buildassets/build/windows/installer/project.nsi @@ -3,10 +3,10 @@ Unicode true #### ## Please note: Template replacements don't work in this file. They are provided with default defines like ## mentioned underneath. -## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. -## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually ## from outside of Wails for debugging and development of the installer. -## +## ## For development first make a wails nsis build to populate the "wails_tools.nsh": ## > wails build --target windows/amd64 --nsis ## Then you can call makensis on this file with specifying the path to your binary: @@ -17,7 +17,7 @@ Unicode true ## For a installer with both architectures: ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe #### -## The following information is taken from the ProjectInfo file, but they can be overwritten here. +## The following information is taken from the ProjectInfo file, but they can be overwritten here. #### ## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" ## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" @@ -33,6 +33,10 @@ Unicode true ## Include the wails tools #### !include "wails_tools.nsh" +#### +## Include the file association tools +#### +!include "fileassoc.nsh" # The version information for this two must consist of 4 parts VIProductVersion "${INFO_PRODUCTVERSION}.0" @@ -85,16 +89,18 @@ Section !insertmacro wails.webview2runtime SetOutPath $INSTDIR - + !insertmacro wails.files CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + !insertmacro wails.associateFiles + !insertmacro wails.writeUninstaller SectionEnd -Section "uninstall" +Section "uninstall" !insertmacro wails.setShellContext RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath @@ -104,5 +110,7 @@ Section "uninstall" Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + !insertmacro wails.unassociateFiles + !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 467c349ac53..07aa04ecb5a 100644 --- a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh +++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh @@ -3,6 +3,7 @@ !include "x64.nsh" !include "WinVer.nsh" !include "FileFunc.nsh" +!include "fileassoc.nsh" !ifndef INFO_PROJECTNAME !define INFO_PROJECTNAME "{{.Name}}" @@ -163,17 +164,31 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" Goto ok ${EndIf} ${EndIf} - + SetDetailsPrint both DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" SetDetailsPrint listonly - + InitPluginsDir CreateDirectory "$pluginsdir\webview2bootstrapper" SetOutPath "$pluginsdir\webview2bootstrapper" File "tmp\MicrosoftEdgeWebview2Setup.exe" ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' - + SetDetailsPrint both ok: -!macroend \ No newline at end of file +!macroend + +!macro wails.associateFiles + ; Create file associations + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconPath}}" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + {{end}} +!macroend diff --git a/website/static/schemas/config.v2.json b/website/static/schemas/config.v2.json index 22a74d56258..0ca2c49d694 100644 --- a/website/static/schemas/config.v2.json +++ b/website/static/schemas/config.v2.json @@ -156,6 +156,39 @@ "type": "string", "description": "A short comment for the app", "default": "Built using Wails (https://wails.io)" + }, + "fileAssociations": { + "type": "array", + "description": "File associations for the app", + "items": { + "type": "object", + "properties": { + "ext": { + "type": "string", + "description": "The extension (minus the leading period). e.g. png" + }, + "name": { + "type": "string", + "description": "The name. e.g. PNG File" + }, + "description": { + "type": "string", + "description": "windows-only. The description." + }, + "mimeType": { + "type": "string", + "description": "linux-only. The mime-type" + }, + "iconPath": { + "type": "string", + "description": "The path to icon (.icns for MacOS and .ico for Windows), relative to build (build resources directory)" + }, + "role": { + "type": "string", + "description": "macOS-only The app’s role with respect to the type. The value can be Editor, Viewer, Shell, or None. Corresponds to CFBundleTypeRole." + } + } + } } } }, @@ -231,7 +264,7 @@ "type": "object", "description": "Bindings configurations", "properties": { - "ts_generation": { + "ts_generation": { "type": "object", "description": "model.ts file generation config", "properties": { From 006c38340fa7c7bb153ad566a8feacf1c0c3aea4 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 12:50:19 +0100 Subject: [PATCH 03/60] fix macro import --- v2/pkg/buildassets/build/windows/installer/project.nsi | 4 ---- 1 file changed, 4 deletions(-) diff --git a/v2/pkg/buildassets/build/windows/installer/project.nsi b/v2/pkg/buildassets/build/windows/installer/project.nsi index 29b824d0730..f18f2b5dfb9 100644 --- a/v2/pkg/buildassets/build/windows/installer/project.nsi +++ b/v2/pkg/buildassets/build/windows/installer/project.nsi @@ -33,10 +33,6 @@ Unicode true ## Include the wails tools #### !include "wails_tools.nsh" -#### -## Include the file association tools -#### -!include "fileassoc.nsh" # The version information for this two must consist of 4 parts VIProductVersion "${INFO_PRODUCTVERSION}.0" From 77f7a6725ccc17320f95c04f9b23a41cb7d6167a Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 13:05:19 +0100 Subject: [PATCH 04/60] add file icon copy --- v2/pkg/buildassets/build/windows/installer/wails_tools.nsh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh index 07aa04ecb5a..447e631a93f 100644 --- a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh +++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh @@ -183,6 +183,8 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" ; Create file associations {{range .Info.FileAssociations}} !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconPath}}" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + CopyFiles "..\{{.IconPath}}" "$INSTDIR\{{.IconPath}}" {{end}} !macroend @@ -190,5 +192,7 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" ; Delete app associations {{range .Info.FileAssociations}} !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconPath}}" {{end}} !macroend From 54ea8bbf9955bcab6810d652937285a68ba39afd Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 13:14:46 +0100 Subject: [PATCH 05/60] try copy icon --- v2/pkg/buildassets/build/windows/installer/wails_tools.nsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh index 447e631a93f..273f96d416d 100644 --- a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh +++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh @@ -184,7 +184,7 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" {{range .Info.FileAssociations}} !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconPath}}" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" - CopyFiles "..\{{.IconPath}}" "$INSTDIR\{{.IconPath}}" + File "..\{{.IconPath}}" {{end}} !macroend From 8e0543a6c03588d86d6459f8c31c417d5b2ba802 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 13:38:53 +0100 Subject: [PATCH 06/60] keep only required part of scripts --- .../build/windows/installer/fileassoc.nsh | 135 ------------------ .../build/windows/installer/wails_tools.nsh | 24 +++- 2 files changed, 23 insertions(+), 136 deletions(-) delete mode 100644 v2/pkg/buildassets/build/windows/installer/fileassoc.nsh diff --git a/v2/pkg/buildassets/build/windows/installer/fileassoc.nsh b/v2/pkg/buildassets/build/windows/installer/fileassoc.nsh deleted file mode 100644 index 6179727fbbb..00000000000 --- a/v2/pkg/buildassets/build/windows/installer/fileassoc.nsh +++ /dev/null @@ -1,135 +0,0 @@ -; from https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b -; fileassoc.nsh -; File association helper macros -; Written by Saivert -; -; Improved by Nikku. -; -; Features automatic backup system and UPDATEFILEASSOC macro for -; shell change notification. -; -; |> How to use <| -; To associate a file with an application so you can double-click it in explorer, use -; the APP_ASSOCIATE macro like this: -; -; Example: -; !insertmacro APP_ASSOCIATE "txt" "myapp.textfile" "Description of txt files" \ -; "$INSTDIR\myapp.exe,0" "Open with myapp" "$INSTDIR\myapp.exe $\"%1$\"" -; -; Never insert the APP_ASSOCIATE macro multiple times, it is only ment -; to associate an application with a single file and using the -; the "open" verb as default. To add more verbs (actions) to a file -; use the APP_ASSOCIATE_ADDVERB macro. -; -; Example: -; !insertmacro APP_ASSOCIATE_ADDVERB "myapp.textfile" "edit" "Edit with myapp" \ -; "$INSTDIR\myapp.exe /edit $\"%1$\"" -; -; To have access to more options when registering the file association use the -; APP_ASSOCIATE_EX macro. Here you can specify the verb and what verb is to be the -; standard action (default verb). -; -; Note, that this script takes into account user versus global installs. -; To properly work you must initialize the SHELL_CONTEXT variable via SetShellVarContext. -; -; And finally: To remove the association from the registry use the APP_UNASSOCIATE -; macro. Here is another example just to wrap it up: -; !insertmacro APP_UNASSOCIATE "txt" "myapp.textfile" -; -; |> Note <| -; When defining your file class string always use the short form of your application title -; then a period (dot) and the type of file. This keeps the file class sort of unique. -; Examples: -; Winamp.Playlist -; NSIS.Script -; Photoshop.JPEGFile -; -; |> Tech info <| -; The registry key layout for a global file association is: -; -; HKEY_LOCAL_MACHINE\Software\Classes -; <".ext"> = -; = <"description"> -; shell -; = <"menu-item text"> -; command = <"command string"> -; -; -; The registry key layout for a per-user file association is: -; -; HKEY_CURRENT_USER\Software\Classes -; <".ext"> = -; = <"description"> -; shell -; = <"menu-item text"> -; command = <"command string"> -; - -!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND - ; Backup the previously associated file class - ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" - WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" - - WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" - - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` -!macroend - -!macro APP_ASSOCIATE_EX EXT FILECLASS DESCRIPTION ICON VERB DEFAULTVERB SHELLNEW COMMANDTEXT COMMAND - ; Backup the previously associated file class - ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" - WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" - - WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" - StrCmp "${SHELLNEW}" "0" +2 - WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}\ShellNew" "NullFile" "" - - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" `${DEFAULTVERB}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}` -!macroend - -!macro APP_ASSOCIATE_ADDVERB FILECLASS VERB COMMANDTEXT COMMAND - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}` -!macroend - -!macro APP_ASSOCIATE_REMOVEVERB FILECLASS VERB - DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}\shell\${VERB}` -!macroend - - -!macro APP_UNASSOCIATE EXT FILECLASS - ; Backup the previously associated file class - ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` - WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" - - DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` -!macroend - -!macro APP_ASSOCIATE_GETFILECLASS OUTPUT EXT - ReadRegStr ${OUTPUT} SHELL_CONTEXT "Software\Classes\.${EXT}" "" -!macroend - - -; !defines for use with SHChangeNotify -!ifdef SHCNE_ASSOCCHANGED -!undef SHCNE_ASSOCCHANGED -!endif -!define SHCNE_ASSOCCHANGED 0x08000000 -!ifdef SHCNF_FLUSH -!undef SHCNF_FLUSH -!endif -!define SHCNF_FLUSH 0x1000 - -!macro UPDATEFILEASSOC -; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we -; can update the shell. - System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)" -!macroend diff --git a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh index 273f96d416d..0079a9c4b4a 100644 --- a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh +++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh @@ -3,7 +3,6 @@ !include "x64.nsh" !include "WinVer.nsh" !include "FileFunc.nsh" -!include "fileassoc.nsh" !ifndef INFO_PROJECTNAME !define INFO_PROJECTNAME "{{.Name}}" @@ -179,6 +178,29 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" ok: !macroend +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + !macro wails.associateFiles ; Create file associations {{range .Info.FileAssociations}} From 48d02bf992bcfa8aedf138c16397109487517c4a Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 13:51:48 +0100 Subject: [PATCH 07/60] update config schema --- website/static/schemas/config.v2.json | 583 ++++++++++++++------------ 1 file changed, 325 insertions(+), 258 deletions(-) diff --git a/website/static/schemas/config.v2.json b/website/static/schemas/config.v2.json index 0ca2c49d694..089d82179ff 100644 --- a/website/static/schemas/config.v2.json +++ b/website/static/schemas/config.v2.json @@ -1,283 +1,350 @@ { - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "https://wails.io/schemas/config.v2.json", - "required": [], - "title": "Wails configuration schema", - "description": "A JSON representation of a Wails project config file.", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The project name", - "default": "wailsapp" + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://wails.io/schemas/config.v2.json", + "required": [], + "title": "Wails configuration schema", + "description": "A JSON representation of a Wails project config file.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The project name", + "default": "wailsapp" + }, + "assetdir": { + "type": "string", + "description": "Relative path to the directory containing the compiled assets. This is normally inferred, and can be left empty." + }, + "reloaddirs": { + "type": "string", + "description": "Additional directories to trigger reloads (comma separated). Often, this is only used for advanced asset configurations." + }, + "build:dir": { + "type": "string", + "description": "The directory where the build files reside.", + "default": "build", + "examples": [ + "./build", + "/absolute/path/to/build", + "C:\\absolute\\path\\to\\build" + ] + }, + "frontend:dir": { + "type": "string", + "description": "The directory where the frontend files reside.", + "default": "frontend", + "examples": [ + "./src", + "/absolute/path/to/frontend", + "C:\\absolute\\path\\to\\frontend" + ] + }, + "frontend:install": { + "type": "string", + "description": "The command to install dependencies. Run in the frontend directory.", + "examples": [ + "npm install" + ] + }, + "frontend:build": { + "type": "string", + "description": "The command to build the assets. Run in the frontend directory.", + "examples": [ + "npm run build" + ] + }, + "frontend:dev": { + "type": "string", + "description": "[Deprecated] This command has been replaced by `frontend:dev:build`. If `frontend:dev:build` is not specified, Wails will fall back to this command. If this command is also not specified, Wails will fall back to `frontend:build`." + }, + "frontend:dev:build": { + "type": "string", + "description": "The equivalent of `frontend:build` during development. If not specified, it falls back to `frontend:dev`." + }, + "frontend:dev:install": { + "type": "string", + "description": "The equivalent of `frontend:install` during development. If not specified, it falls back to `frontend:install`." + }, + "frontend:dev:watcher": { + "type": "string", + "description": "This command is run in a separate process on `wails dev`. Useful for third-party watchers or for starting third-party dev servers." + }, + "frontend:dev:serverUrl": { + "type": "string", + "description": "URL to a 3rd party dev server to be used to serve assets (eg. Vite). If this is set to 'auto', then the devServerUrl will be inferred from the Vite output", + "examples": [ + "auto", + "http://localhost:3000" + ], + "oneOf": [ + { + "format": "uri" }, - "assetdir": { - "type": "string", - "description": "Relative path to the directory containing the compiled assets. This is normally inferred, and can be left empty." + { + "const": "auto" + } + ] + }, + "wailsjsdir": { + "type": "string", + "description": "Relative path to the directory where the auto-generated JS modules will be created.", + "format": "uri-reference", + "default": "The value of frontend:dir" + }, + "version": { + "description": "Project config version", + "default": "2", + "enum": [ + "2" + ] + }, + "outputfilename": { + "type": "string", + "description": "The name of the binary" + }, + "debounceMS": { + "type": "number", + "description": "The debounce time for hot-reload of the built-in dev server. Measured in milliseconds.", + "default": 100 + }, + "devServer": { + "type": "string", + "description": "The address to bind the wails dev server to.", + "default": "localhost:34115", + "format": "uri" + }, + "appargs": { + "type": "string", + "description": "Arguments passed to the application in shell style when in dev mode." + }, + "runNonNativeBuildHooks": { + "type": "boolean", + "description": "Whether to run build hooks that are defined for an OS other than the host OS.", + "default": false + }, + "preBuildHooks": { + "$ref": "#/definitions/buildHooks" + }, + "postBuildHooks": { + "$ref": "#/definitions/buildHooks" + }, + "author": { + "type": "object", + "description": "The application author", + "properties": { + "name": { + "type": "string" }, - "reloaddirs": { - "type": "string", - "description": "Additional directories to trigger reloads (comma separated). Often, this is only used for advanced asset configurations." + "email": { + "type": "string", + "format": "email" + } + } + }, + "info": { + "type": "object", + "description": "Data used to populate manifests and version info.", + "properties": { + "companyName": { + "type": "string", + "description": "The company name. Defaults to the project name." }, - "build:dir": { - "type": "string", - "description": "The directory where the build files reside.", - "default": "build", - "examples": [ - "./build", - "/absolute/path/to/build", - "C:\\absolute\\path\\to\\build" - ] + "productName": { + "type": "string", + "description": "The product name. Defaults to the project name." }, - "frontend:dir": { - "type": "string", - "description": "The directory where the frontend files reside.", - "default": "frontend", - "examples": [ - "./src", - "/absolute/path/to/frontend", - "C:\\absolute\\path\\to\\frontend" - ] + "productVersion": { + "type": "string", + "description": "The version of the product", + "default": "1.0.0" }, - "frontend:install": { - "type": "string", - "description": "The command to install dependencies. Run in the frontend directory.", - "examples": [ - "npm install" - ] + "copyright": { + "type": "string", + "description": "A copyright string for the product", + "default": "Copyright........." }, - "frontend:build": { - "type": "string", - "description": "The command to build the assets. Run in the frontend directory.", - "examples": [ - "npm run build" - ] + "comments": { + "type": "string", + "description": "A short comment for the app", + "default": "Built using Wails (https://wails.io)" }, - "frontend:dev": { - "type": "string", - "description": "[Deprecated] This command has been replaced by `frontend:dev:build`. If `frontend:dev:build` is not specified, Wails will fall back to this command. If this command is also not specified, Wails will fall back to `frontend:build`." + "fileAssociations": { + "type": "array", + "description": "File associations for the app", + "items": { + "type": "object", + "properties": { + "ext": { + "type": "string", + "description": "The extension (minus the leading period). e.g. png" + }, + "name": { + "type": "string", + "description": "The name. e.g. PNG File" + }, + "description": { + "type": "string", + "description": "Windows-only. The description. It is displayed on the `Type` column on Windows Explorer." + }, + "mimeType": { + "type": "string", + "description": "Linux-only. The mime-type e.g. 'image/png' or 'text/plain'" + }, + "iconPath": { + "type": "string", + "description": "The path to icon (.icns for MacOS and .ico for Windows), relative to build (build resources directory)" + }, + "role": { + "description": "MacOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole.", + "allOf": [ + { + "$ref": "#/definitions/BundleTypeRole" + } + ] + } + } + } + } + }, + }, + "nsisType": { + "type": "string", + "default": "multiple", + "description": "Type of NSIS Installer for Windows", + "oneOf": [ + { + "const": "multiple", + "description": "One installer per architecture" }, - "frontend:dev:build": { - "type": "string", - "description": "The equivalent of `frontend:build` during development. If not specified, it falls back to `frontend:dev`." + { + "const": "single", + "description": "Single universal installer for all architectures being built" + } + ] + }, + "obfuscated": { + "type": "boolean", + "default": false, + "description": "Whether the binary should be obfuscated. Uses ." + }, + "garbleargs": { + "type": "string", + "description": "The arguments to pass to the garble command when using the obfuscated flag" + } + }, + "dependencies": { + "garbleargs": [ + "obfuscated" + ] + }, + "definitions": { + "OsHook": { + "title": "GOOS/*", + "type": "string", + "description": "Executed at build level before/after a build of the specific platform" + }, + "OsArchHook": { + "title": "GOOS/GOARCH", + "type": "string", + "description": "Executed at build level before/after a build of the specific platform and arch" + }, + "buildHooks": { + "type": "object", + "description": "Build hooks for different targets.", + "additionalProperties": false, + "properties": { + "{GOOS}/{GOARCH}": { + "$ref": "#/definitions/OsArchHook" }, - "frontend:dev:install": { - "type": "string", - "description": "The equivalent of `frontend:install` during development. If not specified, it falls back to `frontend:install`." + "{GOOS}/*": { + "$ref": "#/definitions/OsHook" }, - "frontend:dev:watcher": { - "type": "string", - "description": "This command is run in a separate process on `wails dev`. Useful for third-party watchers or for starting third-party dev servers." + "windows/*": { + "$ref": "#/definitions/OsHook" }, - "frontend:dev:serverUrl": { - "type": "string", - "description": "URL to a 3rd party dev server to be used to serve assets (eg. Vite). If this is set to 'auto', then the devServerUrl will be inferred from the Vite output", - "examples": [ "auto", "http://localhost:3000" ], - "oneOf": [ - { "format": "uri" }, - { "const": "auto" } - ] + "linux/*": { + "$ref": "#/definitions/OsHook" }, - "wailsjsdir": { - "type": "string", - "description": "Relative path to the directory where the auto-generated JS modules will be created.", - "format": "uri-reference", - "default": "The value of frontend:dir" + "darwin/*": { + "$ref": "#/definitions/OsHook" }, - "version": { - "description": "Project config version", - "default": "2", - "enum": [ "2" ] + "*/*": { + "type": "string", + "description": "Executed at build level before/after a build" + } + }, + "patternProperties": { + "^[a-zA-Z0-9]+/[a-zA-Z0-9]+$": { + "type": "string", + "title": "GOOS/GOARCH", + "description": "Executed at build level before/after a build of the specific platform and arch" }, - "outputfilename": { + "^[a-zA-Z0-9]+/\\*$": { + "type": "string", + "title": "GOOS/*", + "description": "Executed at build level before/after a build of the specific platform" + } + } + }, + "BundleTypeRole": { + "description": "macOS-only. Corresponds to CFBundleTypeRole", + "oneOf": [ + { + "description": "CFBundleTypeRole.Editor. Files can be read and edited.", "type": "string", - "description": "The name of the binary" - }, - "debounceMS": { - "type": "number", - "description": "The debounce time for hot-reload of the built-in dev server. Measured in milliseconds.", - "default": 100 - }, - "devServer": { + "enum": [ + "Editor" + ] + }, + { + "description": "CFBundleTypeRole.Viewer. Files can be read.", "type": "string", - "description": "The address to bind the wails dev server to.", - "default": "localhost:34115", - "format": "uri" - }, - "appargs": { + "enum": [ + "Viewer" + ] + }, + { + "description": "CFBundleTypeRole.Shell", "type": "string", - "description": "Arguments passed to the application in shell style when in dev mode." - }, - "runNonNativeBuildHooks": { - "type": "boolean", - "description": "Whether to run build hooks that are defined for an OS other than the host OS.", - "default": false - }, - "preBuildHooks": { - "$ref": "#/definitions/buildHooks" - }, - "postBuildHooks": { - "$ref": "#/definitions/buildHooks" - }, - "author": { - "type": "object", - "description": "The application author", - "properties": { - "name": { "type": "string" }, - "email": { - "type": "string", - "format": "email" - } - } - }, - "info": { - "type": "object", - "description": "Data used to populate manifests and version info.", - "properties": { - "companyName": { - "type": "string", - "description": "The company name. Defaults to the project name." - }, - "productName": { - "type": "string", - "description": "The product name. Defaults to the project name." - }, - "productVersion": { - "type": "string", - "description": "The version of the product", - "default": "1.0.0" - }, - "copyright": { - "type": "string", - "description": "A copyright string for the product", - "default": "Copyright........." - }, - "comments": { - "type": "string", - "description": "A short comment for the app", - "default": "Built using Wails (https://wails.io)" - }, - "fileAssociations": { - "type": "array", - "description": "File associations for the app", - "items": { - "type": "object", - "properties": { - "ext": { - "type": "string", - "description": "The extension (minus the leading period). e.g. png" - }, - "name": { - "type": "string", - "description": "The name. e.g. PNG File" - }, - "description": { - "type": "string", - "description": "windows-only. The description." - }, - "mimeType": { - "type": "string", - "description": "linux-only. The mime-type" - }, - "iconPath": { - "type": "string", - "description": "The path to icon (.icns for MacOS and .ico for Windows), relative to build (build resources directory)" - }, - "role": { - "type": "string", - "description": "macOS-only The app’s role with respect to the type. The value can be Editor, Viewer, Shell, or None. Corresponds to CFBundleTypeRole." - } - } - } - } - } - }, - "nsisType": { + "enum": [ + "Shell" + ] + }, + { + "description": "CFBundleTypeRole.QLGenerator", "type": "string", - "default": "multiple", - "description": "Type of NSIS Installer for Windows", - "oneOf": [ - { - "const": "multiple", - "description": "One installer per architecture" - }, - { - "const": "single", - "description": "Single universal installer for all architectures being built" - } + "enum": [ + "QLGenerator" ] - }, - "obfuscated": { - "type": "boolean", - "default": false, - "description": "Whether the binary should be obfuscated. Uses ." - }, - "garbleargs": { + }, + { + "description": "CFBundleTypeRole.None", "type": "string", - "description": "The arguments to pass to the garble command when using the obfuscated flag" - } - }, - "dependencies": { - "garbleargs": ["obfuscated"] - }, - "definitions": { - "OsHook": { - "title": "GOOS/*", + "enum": [ + "None" + ] + } + ] + } + }, + "bindings": { + "type": "object", + "description": "Bindings configurations", + "properties": { + "ts_generation": { + "type": "object", + "description": "model.ts file generation config", + "properties": { + "prefix": { "type": "string", - "description": "Executed at build level before/after a build of the specific platform" - }, - "OsArchHook": { - "title": "GOOS/GOARCH", + "description": "All generated JavaScript entities will be prefixed with this value" + }, + "suffix": { "type": "string", - "description": "Executed at build level before/after a build of the specific platform and arch" - }, - "buildHooks": { - "type": "object", - "description": "Build hooks for different targets.", - "additionalProperties": false, - "properties": { - "{GOOS}/{GOARCH}": { "$ref": "#/definitions/OsArchHook" }, - "{GOOS}/*": { "$ref": "#/definitions/OsHook" }, - "windows/*": { "$ref": "#/definitions/OsHook" }, - "linux/*": { "$ref": "#/definitions/OsHook" }, - "darwin/*": { "$ref": "#/definitions/OsHook" }, - "*/*": { - "type": "string", - "description": "Executed at build level before/after a build" - } - }, - "patternProperties": { - "^[a-zA-Z0-9]+/[a-zA-Z0-9]+$": { - "type": "string", - "title": "GOOS/GOARCH", - "description": "Executed at build level before/after a build of the specific platform and arch" - }, - "^[a-zA-Z0-9]+/\\*$": { - "type": "string", - "title": "GOOS/*", - "description": "Executed at build level before/after a build of the specific platform" - } - } + "description": "All generated JavaScript entities will be suffixed with this value" + } } - }, - "bindings": { - "type": "object", - "description": "Bindings configurations", - "properties": { - "ts_generation": { - "type": "object", - "description": "model.ts file generation config", - "properties": { - "prefix": { - "type": "string", - "description": "All generated JavaScript entities will be prefixed with this value" - }, - "suffix": { - "type": "string", - "description": "All generated JavaScript entities will be suffixed with this value" - } - } - } - } - } + } + } + } } From 51d391f42bd8fc2764fc108e44b3d10dbbacc211 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 13:52:06 +0100 Subject: [PATCH 08/60] fix json --- website/static/schemas/config.v2.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/static/schemas/config.v2.json b/website/static/schemas/config.v2.json index 089d82179ff..c5f4370d2ac 100644 --- a/website/static/schemas/config.v2.json +++ b/website/static/schemas/config.v2.json @@ -205,7 +205,7 @@ } } } - }, + } }, "nsisType": { "type": "string", From 329b9a51b6550152317604ac5a294d0da3196238 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 14:07:27 +0100 Subject: [PATCH 09/60] set fileAssociation for mac via config --- .../buildassets/build/darwin/Info.dev.plist | 21 ++++++++++++++++++- v2/pkg/buildassets/build/darwin/Info.plist | 21 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/v2/pkg/buildassets/build/darwin/Info.dev.plist b/v2/pkg/buildassets/build/darwin/Info.dev.plist index 02e7358ee86..a64ec1b7407 100644 --- a/v2/pkg/buildassets/build/darwin/Info.dev.plist +++ b/v2/pkg/buildassets/build/darwin/Info.dev.plist @@ -23,10 +23,29 @@ true NSHumanReadableCopyright {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconPath}} + + {{end}} + + {{end}} NSAppTransportSecurity NSAllowsLocalNetworking - \ No newline at end of file + diff --git a/v2/pkg/buildassets/build/darwin/Info.plist b/v2/pkg/buildassets/build/darwin/Info.plist index e7819a7e877..160d8ad470e 100644 --- a/v2/pkg/buildassets/build/darwin/Info.plist +++ b/v2/pkg/buildassets/build/darwin/Info.plist @@ -23,5 +23,24 @@ true NSHumanReadableCopyright {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconPath}} + + {{end}} + + {{end}} - \ No newline at end of file + From 2670bdeade5a4f21a2c935205c6570c9abc65a05 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 14:17:15 +0100 Subject: [PATCH 10/60] proper iconName handling --- v2/internal/project/project.go | 2 +- v2/pkg/buildassets/build/darwin/Info.dev.plist | 2 +- v2/pkg/buildassets/build/darwin/Info.plist | 2 +- v2/pkg/buildassets/build/windows/installer/wails_tools.nsh | 6 +++--- website/static/schemas/config.v2.json | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/v2/internal/project/project.go b/v2/internal/project/project.go index 82ea54b7472..91b6d5eec2e 100644 --- a/v2/internal/project/project.go +++ b/v2/internal/project/project.go @@ -229,7 +229,7 @@ type FileAssociation struct { Name string `json:"name"` Description string `json:"description"` MimeType string `json:"mimeType"` - IconPath string `json:"iconPath"` + IconName string `json:"iconName"` Role string `json:"role"` } diff --git a/v2/pkg/buildassets/build/darwin/Info.dev.plist b/v2/pkg/buildassets/build/darwin/Info.dev.plist index a64ec1b7407..9d4096f4fb2 100644 --- a/v2/pkg/buildassets/build/darwin/Info.dev.plist +++ b/v2/pkg/buildassets/build/darwin/Info.dev.plist @@ -37,7 +37,7 @@ CFBundleTypeRole {{.Role}} CFBundleTypeIconFile - {{.IconPath}} + {{.IconName}} {{end}} diff --git a/v2/pkg/buildassets/build/darwin/Info.plist b/v2/pkg/buildassets/build/darwin/Info.plist index 160d8ad470e..079bdafa962 100644 --- a/v2/pkg/buildassets/build/darwin/Info.plist +++ b/v2/pkg/buildassets/build/darwin/Info.plist @@ -37,7 +37,7 @@ CFBundleTypeRole {{.Role}} CFBundleTypeIconFile - {{.IconPath}} + {{.IconName}} {{end}} diff --git a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh index 0079a9c4b4a..bc79fb395ac 100644 --- a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh +++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh @@ -204,9 +204,9 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" !macro wails.associateFiles ; Create file associations {{range .Info.FileAssociations}} - !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconPath}}" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" - File "..\{{.IconPath}}" + File "..\{{.IconName}}.ico" {{end}} !macroend @@ -215,6 +215,6 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" {{range .Info.FileAssociations}} !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" - Delete "$INSTDIR\{{.IconPath}}" + Delete "$INSTDIR\{{.IconName}}.ico" {{end}} !macroend diff --git a/website/static/schemas/config.v2.json b/website/static/schemas/config.v2.json index c5f4370d2ac..78cf3558753 100644 --- a/website/static/schemas/config.v2.json +++ b/website/static/schemas/config.v2.json @@ -190,9 +190,9 @@ "type": "string", "description": "Linux-only. The mime-type e.g. 'image/png' or 'text/plain'" }, - "iconPath": { + "iconName": { "type": "string", - "description": "The path to icon (.icns for MacOS and .ico for Windows), relative to build (build resources directory)" + "description": "The icon Name without extension. (.icns for MacOS and .ico for Windows)" }, "role": { "description": "MacOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole.", From 82bc909c74a421cceebbf24cff5914e43a24e606 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 14:47:07 +0100 Subject: [PATCH 11/60] add fileAssociation icon generator --- v2/pkg/commands/build/packager.go | 37 ++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/v2/pkg/commands/build/packager.go b/v2/pkg/commands/build/packager.go index 92ce37e9056..82ec8086370 100644 --- a/v2/pkg/commands/build/packager.go +++ b/v2/pkg/commands/build/packager.go @@ -6,6 +6,7 @@ import ( "github.com/leaanthony/winicon" "github.com/tc-hib/winres" "github.com/tc-hib/winres/version" + "github.com/wailsapp/wails/v2/internal/project" "image" "os" "path/filepath" @@ -95,12 +96,20 @@ func packageApplicationForDarwin(options *Options) error { return err } - // Generate Icons - err = processApplicationIcon(options, resourceDir) + // Generate App Icon + err = processDarwinIcon(options.ProjectData, resourceDir, "appicon") if err != nil { return err } + // Generate FileAssociation Icons + for _, fileAssociation := range options.ProjectData.Info.FileAssociations { + err = processDarwinIcon(options.ProjectData, resourceDir, fileAssociation.IconName) + if err != nil { + return err + } + } + options.CompiledBinary = packedBinaryPath return nil @@ -124,8 +133,8 @@ func processPList(options *Options, contentsDirectory string) error { return os.WriteFile(targetFile, content, 0644) } -func processApplicationIcon(options *Options, resourceDir string) (err error) { - appIcon, err := buildassets.ReadFile(options.ProjectData, "appicon.png") +func processDarwinIcon(projectData *project.Project, iconName string, resourceDir string) (err error) { + appIcon, err := buildassets.ReadFile(projectData, iconName+".png") if err != nil { return err } @@ -135,7 +144,7 @@ func processApplicationIcon(options *Options, resourceDir string) (err error) { return err } - tgtBundle := filepath.Join(resourceDir, "iconfile.icns") + tgtBundle := filepath.Join(resourceDir, iconName+".icns") dest, err := os.Create(tgtBundle) if err != nil { return err @@ -151,13 +160,21 @@ func processApplicationIcon(options *Options, resourceDir string) (err error) { } func packageApplicationForWindows(options *Options) error { - // Generate icon + // Generate app icon var err error - err = generateIcoFile(options) + err = generateIcoFile(options, "appicon") if err != nil { return err } + // Generate FileAssociation Icons + for _, fileAssociation := range options.ProjectData.Info.FileAssociations { + err = generateIcoFile(options, fileAssociation.IconName) + if err != nil { + return err + } + } + // Create syso file err = compileResources(options) if err != nil { @@ -171,13 +188,13 @@ func packageApplicationForLinux(_ *Options) error { return nil } -func generateIcoFile(options *Options) error { - content, err := buildassets.ReadFile(options.ProjectData, "appicon.png") +func generateIcoFile(options *Options, iconName string) error { + content, err := buildassets.ReadFile(options.ProjectData, iconName+".png") if err != nil { return err } // Check ico file exists already - icoFile := buildassets.GetLocalPath(options.ProjectData, "windows/icon.ico") + icoFile := buildassets.GetLocalPath(options.ProjectData, "windows/"+iconName+".ico") if !fs.FileExists(icoFile) { if dir := filepath.Dir(icoFile); !fs.DirExists(dir) { if err := fs.MkDirs(dir, 0755); err != nil { From 2a836c8fb39767decad47e0cb8ade819b3ac2f8b Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 15:15:07 +0100 Subject: [PATCH 12/60] fix file association icons bundle --- v2/pkg/buildassets/build/darwin/Info.dev.plist | 2 +- v2/pkg/buildassets/build/darwin/Info.plist | 2 +- v2/pkg/commands/build/packager.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/v2/pkg/buildassets/build/darwin/Info.dev.plist b/v2/pkg/buildassets/build/darwin/Info.dev.plist index 9d4096f4fb2..ff6f96fff57 100644 --- a/v2/pkg/buildassets/build/darwin/Info.dev.plist +++ b/v2/pkg/buildassets/build/darwin/Info.dev.plist @@ -16,7 +16,7 @@ CFBundleShortVersionString {{.Info.ProductVersion}} CFBundleIconFile - iconfile + appicon LSMinimumSystemVersion 10.13.0 NSHighResolutionCapable diff --git a/v2/pkg/buildassets/build/darwin/Info.plist b/v2/pkg/buildassets/build/darwin/Info.plist index 079bdafa962..68690fcf419 100644 --- a/v2/pkg/buildassets/build/darwin/Info.plist +++ b/v2/pkg/buildassets/build/darwin/Info.plist @@ -16,7 +16,7 @@ CFBundleShortVersionString {{.Info.ProductVersion}} CFBundleIconFile - iconfile + appicon LSMinimumSystemVersion 10.13.0 NSHighResolutionCapable diff --git a/v2/pkg/commands/build/packager.go b/v2/pkg/commands/build/packager.go index 82ec8086370..89747bb00c0 100644 --- a/v2/pkg/commands/build/packager.go +++ b/v2/pkg/commands/build/packager.go @@ -97,14 +97,14 @@ func packageApplicationForDarwin(options *Options) error { } // Generate App Icon - err = processDarwinIcon(options.ProjectData, resourceDir, "appicon") + err = processDarwinIcon(options.ProjectData, "appicon", resourceDir) if err != nil { return err } // Generate FileAssociation Icons for _, fileAssociation := range options.ProjectData.Info.FileAssociations { - err = processDarwinIcon(options.ProjectData, resourceDir, fileAssociation.IconName) + err = processDarwinIcon(options.ProjectData, fileAssociation.IconName, resourceDir) if err != nil { return err } From 3b1c6008e5ddd49ec4844eb1c2d66b5fa718db61 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sat, 16 Sep 2023 15:23:17 +0100 Subject: [PATCH 13/60] don't break compatibility --- .../buildassets/build/darwin/Info.dev.plist | 2 +- v2/pkg/buildassets/build/darwin/Info.plist | 2 +- v2/pkg/commands/build/packager.go | 25 +++++++++++++------ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/v2/pkg/buildassets/build/darwin/Info.dev.plist b/v2/pkg/buildassets/build/darwin/Info.dev.plist index ff6f96fff57..9d4096f4fb2 100644 --- a/v2/pkg/buildassets/build/darwin/Info.dev.plist +++ b/v2/pkg/buildassets/build/darwin/Info.dev.plist @@ -16,7 +16,7 @@ CFBundleShortVersionString {{.Info.ProductVersion}} CFBundleIconFile - appicon + iconfile LSMinimumSystemVersion 10.13.0 NSHighResolutionCapable diff --git a/v2/pkg/buildassets/build/darwin/Info.plist b/v2/pkg/buildassets/build/darwin/Info.plist index 68690fcf419..079bdafa962 100644 --- a/v2/pkg/buildassets/build/darwin/Info.plist +++ b/v2/pkg/buildassets/build/darwin/Info.plist @@ -16,7 +16,7 @@ CFBundleShortVersionString {{.Info.ProductVersion}} CFBundleIconFile - appicon + iconfile LSMinimumSystemVersion 10.13.0 NSHighResolutionCapable diff --git a/v2/pkg/commands/build/packager.go b/v2/pkg/commands/build/packager.go index 89747bb00c0..beca47c5bea 100644 --- a/v2/pkg/commands/build/packager.go +++ b/v2/pkg/commands/build/packager.go @@ -97,14 +97,14 @@ func packageApplicationForDarwin(options *Options) error { } // Generate App Icon - err = processDarwinIcon(options.ProjectData, "appicon", resourceDir) + err = processDarwinIcon(options.ProjectData, "appicon", resourceDir, "iconfile") if err != nil { return err } // Generate FileAssociation Icons for _, fileAssociation := range options.ProjectData.Info.FileAssociations { - err = processDarwinIcon(options.ProjectData, fileAssociation.IconName, resourceDir) + err = processDarwinIcon(options.ProjectData, fileAssociation.IconName, resourceDir, "") if err != nil { return err } @@ -133,7 +133,7 @@ func processPList(options *Options, contentsDirectory string) error { return os.WriteFile(targetFile, content, 0644) } -func processDarwinIcon(projectData *project.Project, iconName string, resourceDir string) (err error) { +func processDarwinIcon(projectData *project.Project, iconName string, resourceDir string, destIconName string) (err error) { appIcon, err := buildassets.ReadFile(projectData, iconName+".png") if err != nil { return err @@ -144,7 +144,11 @@ func processDarwinIcon(projectData *project.Project, iconName string, resourceDi return err } - tgtBundle := filepath.Join(resourceDir, iconName+".icns") + if destIconName == "" { + destIconName = iconName + } + + tgtBundle := filepath.Join(resourceDir, destIconName+".icns") dest, err := os.Create(tgtBundle) if err != nil { return err @@ -162,14 +166,14 @@ func processDarwinIcon(projectData *project.Project, iconName string, resourceDi func packageApplicationForWindows(options *Options) error { // Generate app icon var err error - err = generateIcoFile(options, "appicon") + err = generateIcoFile(options, "appicon", "icon") if err != nil { return err } // Generate FileAssociation Icons for _, fileAssociation := range options.ProjectData.Info.FileAssociations { - err = generateIcoFile(options, fileAssociation.IconName) + err = generateIcoFile(options, fileAssociation.IconName, "") if err != nil { return err } @@ -188,13 +192,18 @@ func packageApplicationForLinux(_ *Options) error { return nil } -func generateIcoFile(options *Options, iconName string) error { +func generateIcoFile(options *Options, iconName string, destIconName string) error { content, err := buildassets.ReadFile(options.ProjectData, iconName+".png") if err != nil { return err } + + if destIconName == "" { + destIconName = iconName + } + // Check ico file exists already - icoFile := buildassets.GetLocalPath(options.ProjectData, "windows/"+iconName+".ico") + icoFile := buildassets.GetLocalPath(options.ProjectData, "windows/"+destIconName+".ico") if !fs.FileExists(icoFile) { if dir := filepath.Dir(icoFile); !fs.DirExists(dir) { if err := fs.MkDirs(dir, 0755); err != nil { From 8f242ece15731b1907e7b329e5f90e7327c7b925 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Thu, 21 Sep 2023 01:48:43 +0100 Subject: [PATCH 14/60] remove mimeType as not supported linux for now --- website/static/schemas/config.v2.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/website/static/schemas/config.v2.json b/website/static/schemas/config.v2.json index 78cf3558753..b0737d2109c 100644 --- a/website/static/schemas/config.v2.json +++ b/website/static/schemas/config.v2.json @@ -186,10 +186,6 @@ "type": "string", "description": "Windows-only. The description. It is displayed on the `Type` column on Windows Explorer." }, - "mimeType": { - "type": "string", - "description": "Linux-only. The mime-type e.g. 'image/png' or 'text/plain'" - }, "iconName": { "type": "string", "description": "The icon Name without extension. (.icns for MacOS and .ico for Windows)" From 93265626fa287ed762b8c7b7aca5edc84921febb Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Fri, 22 Sep 2023 21:55:21 +0100 Subject: [PATCH 15/60] add documentation --- website/docs/guides/file-association.mdx | 183 +++++++++++++++++++++++ website/src/pages/changelog.mdx | 1 + 2 files changed, 184 insertions(+) create mode 100644 website/docs/guides/file-association.mdx diff --git a/website/docs/guides/file-association.mdx b/website/docs/guides/file-association.mdx new file mode 100644 index 00000000000..71763bb1037 --- /dev/null +++ b/website/docs/guides/file-association.mdx @@ -0,0 +1,183 @@ +# File Association + +File association feature allows you to associate specific file types with your app so that when users open those files, +your app is launched to handle them. This can be particularly useful for text editors, image viewers, or any application +that works with specific file formats. In this guide, we'll walk through the steps to implement file association in Wails app. + + +## Set Up File Association: +To set up file association, you need to modify your application's wails.json file. +In "info" section add a "fileAssociations" section specifying the file types your app should be associated with. + +For example: +```json +{ + "info": { + "fileAssociations": [ + { + "ext": "wails", + "name": "Wails", + "description": "Wails Application File", + "iconName": "wailsFileIcon", + "role": "Editor" + }, + { + "ext": "jpg", + "name": "JPEG", + "description": "Image File", + "iconName": "jpegFileIcon", + "role": "Editor" + } + ] + } +} +``` + +| Property | Description | +|:------------|:---------------------------------------------------------------------------------------------------------------------------------------------------| +| ext | The extension (minus the leading period). e.g. png | +| name | The name. e.g. PNG File | +| iconName | The icon name without extension. Icons should be located in build folder. Proper icons will be generated from .png file for both macOS and Windows | +| description | Windows-only. The description. It is displayed on the `Type` column on Windows Explorer. | +| role | macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole. | + +## Platform Specifics: + +### macOS +When you open file (or files) with your app, the system will launch your app and call the `openFile` `openFiles` functions 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}, + OnFileOpen: func(filePath string) { println(filestring) }, + OnFilesOpen: func(filePaths []string) { println(filestring) }, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} +``` + + +### Windows +On Windows file association is supported only with NSIS installer. During installation, the installer will create a +registry entry for your file associations. When you open file 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) + } +} +``` + +### 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. Example: +```ini +[Desktop Entry] +Categories=Office +Exec=/usr/bin/wails-open-file +Icon=wails-open-file.png +Name=wails-open-file +Terminal=false +Type=Application +MimeType=application/x-wails;application/x-test +``` + +2. Create mime types file. Example: +```xml + + + + Wails Application File + + + +``` + +3. Create icons for your file types. SVG icons are recommended. +4. Prepare postInstall/postRemove scripts for your package. Example: +```sh +# reload mime types to register file associations +update-mime-database /usr/share/mime +# reload desktop database to load app in list of available +update-desktop-database /usr/share/applications +# update icons +update-icon-caches /usr/share/icons/* +``` +5. 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: ./application-wails-mime.xml + dst: /usr/share/mime/packages/application-x-wails.xml +- src: ./application-test-mime.xml + dst: /usr/share/mime/packages/application-x-test.xml +- src: ../appicon.svg + dst: /usr/share/icons/hicolor/scalable/apps/wails-open-file.svg +- src: ../wailsFileIcon.svg + dst: /usr/share/icons/hicolor/scalable/mimetypes/application-x-wails.svg +- src: ../testFileIcon.svg + dst: /usr/share/icons/hicolor/scalable/mimetypes/application-x-test.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 +- src: ../wailsFileIcon.svg + dst: /usr/share/icons/Yaru/scalable/mimetypes/application-x-wails.svg +- src: ../testFileIcon.svg + dst: /usr/share/icons/Yaru/scalable/mimetypes/application-x-test.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 specified file types. When you open file 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) + } +} +``` + +## Limitations: +On Windows and Linux when associated file is opened, new instance of your app is launched. +Currently, Wails doesn't support opening files in already running app. There is plugin for single instance support for v3 in development. diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index 9f8cf8559c1..2e8d77dedba 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for enabling/disabling swipe gestures for Windows WebView2. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2878) - When building with `-devtools` flag, CMD/CTRL+SHIFT+F12 can be used to open the devtools. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2915) +– Added file association support for macOS and Windows. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/2918) ### Changed From e695663f9514c9b87ed2ac8065ac9902d0bad2bf Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Fri, 22 Sep 2023 21:57:22 +0100 Subject: [PATCH 16/60] adjust config schema --- website/static/schemas/config.v2.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/static/schemas/config.v2.json b/website/static/schemas/config.v2.json index b0737d2109c..4fed64cebbb 100644 --- a/website/static/schemas/config.v2.json +++ b/website/static/schemas/config.v2.json @@ -188,10 +188,10 @@ }, "iconName": { "type": "string", - "description": "The icon Name without extension. (.icns for MacOS and .ico for Windows)" + "description": "The icon name without extension. Icons should be located in build folder. Proper icons will be generated from .png file for both macOS and Windows)" }, "role": { - "description": "MacOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole.", + "description": "macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole.", "allOf": [ { "$ref": "#/definitions/BundleTypeRole" From 045aa05183d4c85e582ccf947a073dfc60144a11 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Fri, 22 Sep 2023 22:05:30 +0100 Subject: [PATCH 17/60] restore formatting --- website/static/schemas/config.v2.json | 636 +++++++++++++------------- 1 file changed, 318 insertions(+), 318 deletions(-) diff --git a/website/static/schemas/config.v2.json b/website/static/schemas/config.v2.json index 4fed64cebbb..7ab40d95fbd 100644 --- a/website/static/schemas/config.v2.json +++ b/website/static/schemas/config.v2.json @@ -1,346 +1,346 @@ { - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "https://wails.io/schemas/config.v2.json", - "required": [], - "title": "Wails configuration schema", - "description": "A JSON representation of a Wails project config file.", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The project name", - "default": "wailsapp" - }, - "assetdir": { - "type": "string", - "description": "Relative path to the directory containing the compiled assets. This is normally inferred, and can be left empty." - }, - "reloaddirs": { - "type": "string", - "description": "Additional directories to trigger reloads (comma separated). Often, this is only used for advanced asset configurations." - }, - "build:dir": { - "type": "string", - "description": "The directory where the build files reside.", - "default": "build", - "examples": [ - "./build", - "/absolute/path/to/build", - "C:\\absolute\\path\\to\\build" - ] - }, - "frontend:dir": { - "type": "string", - "description": "The directory where the frontend files reside.", - "default": "frontend", - "examples": [ - "./src", - "/absolute/path/to/frontend", - "C:\\absolute\\path\\to\\frontend" - ] - }, - "frontend:install": { - "type": "string", - "description": "The command to install dependencies. Run in the frontend directory.", - "examples": [ - "npm install" - ] - }, - "frontend:build": { - "type": "string", - "description": "The command to build the assets. Run in the frontend directory.", - "examples": [ - "npm run build" - ] - }, - "frontend:dev": { - "type": "string", - "description": "[Deprecated] This command has been replaced by `frontend:dev:build`. If `frontend:dev:build` is not specified, Wails will fall back to this command. If this command is also not specified, Wails will fall back to `frontend:build`." - }, - "frontend:dev:build": { - "type": "string", - "description": "The equivalent of `frontend:build` during development. If not specified, it falls back to `frontend:dev`." - }, - "frontend:dev:install": { - "type": "string", - "description": "The equivalent of `frontend:install` during development. If not specified, it falls back to `frontend:install`." - }, - "frontend:dev:watcher": { - "type": "string", - "description": "This command is run in a separate process on `wails dev`. Useful for third-party watchers or for starting third-party dev servers." - }, - "frontend:dev:serverUrl": { - "type": "string", - "description": "URL to a 3rd party dev server to be used to serve assets (eg. Vite). If this is set to 'auto', then the devServerUrl will be inferred from the Vite output", - "examples": [ - "auto", - "http://localhost:3000" - ], - "oneOf": [ - { - "format": "uri" - }, - { - "const": "auto" - } - ] - }, - "wailsjsdir": { - "type": "string", - "description": "Relative path to the directory where the auto-generated JS modules will be created.", - "format": "uri-reference", - "default": "The value of frontend:dir" - }, - "version": { - "description": "Project config version", - "default": "2", - "enum": [ - "2" - ] - }, - "outputfilename": { - "type": "string", - "description": "The name of the binary" - }, - "debounceMS": { - "type": "number", - "description": "The debounce time for hot-reload of the built-in dev server. Measured in milliseconds.", - "default": 100 - }, - "devServer": { - "type": "string", - "description": "The address to bind the wails dev server to.", - "default": "localhost:34115", - "format": "uri" - }, - "appargs": { - "type": "string", - "description": "Arguments passed to the application in shell style when in dev mode." - }, - "runNonNativeBuildHooks": { - "type": "boolean", - "description": "Whether to run build hooks that are defined for an OS other than the host OS.", - "default": false - }, - "preBuildHooks": { - "$ref": "#/definitions/buildHooks" - }, - "postBuildHooks": { - "$ref": "#/definitions/buildHooks" - }, - "author": { - "type": "object", - "description": "The application author", - "properties": { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://wails.io/schemas/config.v2.json", + "required": [], + "title": "Wails configuration schema", + "description": "A JSON representation of a Wails project config file.", + "type": "object", + "properties": { "name": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - } - } - }, - "info": { - "type": "object", - "description": "Data used to populate manifests and version info.", - "properties": { - "companyName": { - "type": "string", - "description": "The company name. Defaults to the project name." - }, - "productName": { - "type": "string", - "description": "The product name. Defaults to the project name." + "type": "string", + "description": "The project name", + "default": "wailsapp" }, - "productVersion": { - "type": "string", - "description": "The version of the product", - "default": "1.0.0" + "assetdir": { + "type": "string", + "description": "Relative path to the directory containing the compiled assets. This is normally inferred, and can be left empty." }, - "copyright": { - "type": "string", - "description": "A copyright string for the product", - "default": "Copyright........." + "reloaddirs": { + "type": "string", + "description": "Additional directories to trigger reloads (comma separated). Often, this is only used for advanced asset configurations." }, - "comments": { - "type": "string", - "description": "A short comment for the app", - "default": "Built using Wails (https://wails.io)" + "build:dir": { + "type": "string", + "description": "The directory where the build files reside.", + "default": "build", + "examples": [ + "./build", + "/absolute/path/to/build", + "C:\\absolute\\path\\to\\build" + ] }, - "fileAssociations": { - "type": "array", - "description": "File associations for the app", - "items": { - "type": "object", - "properties": { - "ext": { - "type": "string", - "description": "The extension (minus the leading period). e.g. png" - }, - "name": { - "type": "string", - "description": "The name. e.g. PNG File" - }, - "description": { - "type": "string", - "description": "Windows-only. The description. It is displayed on the `Type` column on Windows Explorer." - }, - "iconName": { - "type": "string", - "description": "The icon name without extension. Icons should be located in build folder. Proper icons will be generated from .png file for both macOS and Windows)" - }, - "role": { - "description": "macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole.", - "allOf": [ - { - "$ref": "#/definitions/BundleTypeRole" - } - ] - } - } - } - } - } - }, - "nsisType": { - "type": "string", - "default": "multiple", - "description": "Type of NSIS Installer for Windows", - "oneOf": [ - { - "const": "multiple", - "description": "One installer per architecture" + "frontend:dir": { + "type": "string", + "description": "The directory where the frontend files reside.", + "default": "frontend", + "examples": [ + "./src", + "/absolute/path/to/frontend", + "C:\\absolute\\path\\to\\frontend" + ] }, - { - "const": "single", - "description": "Single universal installer for all architectures being built" - } - ] - }, - "obfuscated": { - "type": "boolean", - "default": false, - "description": "Whether the binary should be obfuscated. Uses ." - }, - "garbleargs": { - "type": "string", - "description": "The arguments to pass to the garble command when using the obfuscated flag" - } - }, - "dependencies": { - "garbleargs": [ - "obfuscated" - ] - }, - "definitions": { - "OsHook": { - "title": "GOOS/*", - "type": "string", - "description": "Executed at build level before/after a build of the specific platform" - }, - "OsArchHook": { - "title": "GOOS/GOARCH", - "type": "string", - "description": "Executed at build level before/after a build of the specific platform and arch" - }, - "buildHooks": { - "type": "object", - "description": "Build hooks for different targets.", - "additionalProperties": false, - "properties": { - "{GOOS}/{GOARCH}": { - "$ref": "#/definitions/OsArchHook" + "frontend:install": { + "type": "string", + "description": "The command to install dependencies. Run in the frontend directory.", + "examples": [ + "npm install" + ] }, - "{GOOS}/*": { - "$ref": "#/definitions/OsHook" + "frontend:build": { + "type": "string", + "description": "The command to build the assets. Run in the frontend directory.", + "examples": [ + "npm run build" + ] }, - "windows/*": { - "$ref": "#/definitions/OsHook" + "frontend:dev": { + "type": "string", + "description": "[Deprecated] This command has been replaced by `frontend:dev:build`. If `frontend:dev:build` is not specified, Wails will fall back to this command. If this command is also not specified, Wails will fall back to `frontend:build`." }, - "linux/*": { - "$ref": "#/definitions/OsHook" + "frontend:dev:build": { + "type": "string", + "description": "The equivalent of `frontend:build` during development. If not specified, it falls back to `frontend:dev`." }, - "darwin/*": { - "$ref": "#/definitions/OsHook" + "frontend:dev:install": { + "type": "string", + "description": "The equivalent of `frontend:install` during development. If not specified, it falls back to `frontend:install`." }, - "*/*": { - "type": "string", - "description": "Executed at build level before/after a build" - } - }, - "patternProperties": { - "^[a-zA-Z0-9]+/[a-zA-Z0-9]+$": { - "type": "string", - "title": "GOOS/GOARCH", - "description": "Executed at build level before/after a build of the specific platform and arch" + "frontend:dev:watcher": { + "type": "string", + "description": "This command is run in a separate process on `wails dev`. Useful for third-party watchers or for starting third-party dev servers." }, - "^[a-zA-Z0-9]+/\\*$": { - "type": "string", - "title": "GOOS/*", - "description": "Executed at build level before/after a build of the specific platform" - } - } - }, - "BundleTypeRole": { - "description": "macOS-only. Corresponds to CFBundleTypeRole", - "oneOf": [ - { - "description": "CFBundleTypeRole.Editor. Files can be read and edited.", + "frontend:dev:serverUrl": { "type": "string", - "enum": [ - "Editor" + "description": "URL to a 3rd party dev server to be used to serve assets (eg. Vite). If this is set to 'auto', then the devServerUrl will be inferred from the Vite output", + "examples": [ + "auto", + "http://localhost:3000" + ], + "oneOf": [ + { + "format": "uri" + }, + { + "const": "auto" + } ] - }, - { - "description": "CFBundleTypeRole.Viewer. Files can be read.", + }, + "wailsjsdir": { "type": "string", + "description": "Relative path to the directory where the auto-generated JS modules will be created.", + "format": "uri-reference", + "default": "The value of frontend:dir" + }, + "version": { + "description": "Project config version", + "default": "2", "enum": [ - "Viewer" + "2" ] - }, - { - "description": "CFBundleTypeRole.Shell", + }, + "outputfilename": { "type": "string", - "enum": [ - "Shell" - ] - }, - { - "description": "CFBundleTypeRole.QLGenerator", + "description": "The name of the binary" + }, + "debounceMS": { + "type": "number", + "description": "The debounce time for hot-reload of the built-in dev server. Measured in milliseconds.", + "default": 100 + }, + "devServer": { "type": "string", - "enum": [ - "QLGenerator" - ] - }, - { - "description": "CFBundleTypeRole.None", + "description": "The address to bind the wails dev server to.", + "default": "localhost:34115", + "format": "uri" + }, + "appargs": { "type": "string", - "enum": [ - "None" + "description": "Arguments passed to the application in shell style when in dev mode." + }, + "runNonNativeBuildHooks": { + "type": "boolean", + "description": "Whether to run build hooks that are defined for an OS other than the host OS.", + "default": false + }, + "preBuildHooks": { + "$ref": "#/definitions/buildHooks" + }, + "postBuildHooks": { + "$ref": "#/definitions/buildHooks" + }, + "author": { + "type": "object", + "description": "The application author", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + } + } + }, + "info": { + "type": "object", + "description": "Data used to populate manifests and version info.", + "properties": { + "companyName": { + "type": "string", + "description": "The company name. Defaults to the project name." + }, + "productName": { + "type": "string", + "description": "The product name. Defaults to the project name." + }, + "productVersion": { + "type": "string", + "description": "The version of the product", + "default": "1.0.0" + }, + "copyright": { + "type": "string", + "description": "A copyright string for the product", + "default": "Copyright........." + }, + "comments": { + "type": "string", + "description": "A short comment for the app", + "default": "Built using Wails (https://wails.io)" + }, + "fileAssociations": { + "type": "array", + "description": "File associations for the app", + "items": { + "type": "object", + "properties": { + "ext": { + "type": "string", + "description": "The extension (minus the leading period). e.g. png" + }, + "name": { + "type": "string", + "description": "The name. e.g. PNG File" + }, + "description": { + "type": "string", + "description": "Windows-only. The description. It is displayed on the `Type` column on Windows Explorer." + }, + "iconName": { + "type": "string", + "description": "The icon name without extension. Icons should be located in build folder. Proper icons will be generated from .png file for both macOS and Windows)" + }, + "role": { + "description": "macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole.", + "allOf": [ + { + "$ref": "#/definitions/BundleTypeRole" + } + ] + } + } + } + } + } + }, + "nsisType": { + "type": "string", + "default": "multiple", + "description": "Type of NSIS Installer for Windows", + "oneOf": [ + { + "const": "multiple", + "description": "One installer per architecture" + }, + { + "const": "single", + "description": "Single universal installer for all architectures being built" + } ] - } + }, + "obfuscated": { + "type": "boolean", + "default": false, + "description": "Whether the binary should be obfuscated. Uses ." + }, + "garbleargs": { + "type": "string", + "description": "The arguments to pass to the garble command when using the obfuscated flag" + } + }, + "dependencies": { + "garbleargs": [ + "obfuscated" ] - } - }, - "bindings": { - "type": "object", - "description": "Bindings configurations", - "properties": { - "ts_generation": { - "type": "object", - "description": "model.ts file generation config", - "properties": { - "prefix": { + }, + "definitions": { + "OsHook": { + "title": "GOOS/*", "type": "string", - "description": "All generated JavaScript entities will be prefixed with this value" - }, - "suffix": { + "description": "Executed at build level before/after a build of the specific platform" + }, + "OsArchHook": { + "title": "GOOS/GOARCH", "type": "string", - "description": "All generated JavaScript entities will be suffixed with this value" - } + "description": "Executed at build level before/after a build of the specific platform and arch" + }, + "buildHooks": { + "type": "object", + "description": "Build hooks for different targets.", + "additionalProperties": false, + "properties": { + "{GOOS}/{GOARCH}": { + "$ref": "#/definitions/OsArchHook" + }, + "{GOOS}/*": { + "$ref": "#/definitions/OsHook" + }, + "windows/*": { + "$ref": "#/definitions/OsHook" + }, + "linux/*": { + "$ref": "#/definitions/OsHook" + }, + "darwin/*": { + "$ref": "#/definitions/OsHook" + }, + "*/*": { + "type": "string", + "description": "Executed at build level before/after a build" + } + }, + "patternProperties": { + "^[a-zA-Z0-9]+/[a-zA-Z0-9]+$": { + "type": "string", + "title": "GOOS/GOARCH", + "description": "Executed at build level before/after a build of the specific platform and arch" + }, + "^[a-zA-Z0-9]+/\\*$": { + "type": "string", + "title": "GOOS/*", + "description": "Executed at build level before/after a build of the specific platform" + } + } + }, + "BundleTypeRole": { + "description": "macOS-only. Corresponds to CFBundleTypeRole", + "oneOf": [ + { + "description": "CFBundleTypeRole.Editor. Files can be read and edited.", + "type": "string", + "enum": [ + "Editor" + ] + }, + { + "description": "CFBundleTypeRole.Viewer. Files can be read.", + "type": "string", + "enum": [ + "Viewer" + ] + }, + { + "description": "CFBundleTypeRole.Shell", + "type": "string", + "enum": [ + "Shell" + ] + }, + { + "description": "CFBundleTypeRole.QLGenerator", + "type": "string", + "enum": [ + "QLGenerator" + ] + }, + { + "description": "CFBundleTypeRole.None", + "type": "string", + "enum": [ + "None" + ] + } + ] + } + }, + "bindings": { + "type": "object", + "description": "Bindings configurations", + "properties": { + "ts_generation": { + "type": "object", + "description": "model.ts file generation config", + "properties": { + "prefix": { + "type": "string", + "description": "All generated JavaScript entities will be prefixed with this value" + }, + "suffix": { + "type": "string", + "description": "All generated JavaScript entities will be suffixed with this value" + } + } + } } - } } - } } From ce0554d006e4006262de323dc73ffebbeeaadd92 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sun, 24 Sep 2023 02:49:34 +0100 Subject: [PATCH 18/60] try implement single instance lock with params passing --- .../frontend/desktop/windows/frontend.go | 8 + .../desktop/windows/winc/single_instance.go | 174 ++++++++++++++++++ .../desktop/windows/winc/w32/user32.go | 9 + v2/pkg/options/options.go | 13 ++ 4 files changed, 204 insertions(+) create mode 100644 v2/internal/frontend/desktop/windows/winc/single_instance.go diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index e51be56cb1b..609a7ef31ec 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -137,9 +137,17 @@ func (f *Frontend) Run(ctx context.Context) error { f.chromium = edge.NewChromium() + if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.Enabled { + winc.SetupSingleInstance(f.frontendOptions.Title, f.frontendOptions.SingleInstanceLock) + } + mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo, f.chromium) f.mainWindow = mainWindow + if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.Enabled { + winc.SingleInstanceHWND(f.mainWindow.Handle()) + } + var _debug = ctx.Value("debug") var _devtoolsEnabled = ctx.Value("devtoolsEnabled") diff --git a/v2/internal/frontend/desktop/windows/winc/single_instance.go b/v2/internal/frontend/desktop/windows/winc/single_instance.go new file mode 100644 index 00000000000..cfa29ce6ec5 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/single_instance.go @@ -0,0 +1,174 @@ +//go:build windows +// +build windows + +package winc + +import ( + "encoding/json" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + "github.com/wailsapp/wails/v2/pkg/options" + "golang.org/x/sys/windows" + "os" + "syscall" + "unicode/utf16" + "unsafe" +) + +type COPYDATASTRUCT struct { + dwData uintptr + cbData uint32 + lpData uintptr +} + +var mainHWND w32.HWND + +const WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542 +const SC_RESTORE = 0xF120 + +func SendMessage(hWnd w32.HWND, data string) { + arrUtf16, _ := syscall.UTF16FromString(data) + + pCopyData := new(COPYDATASTRUCT) + pCopyData.dwData = WMCOPYDATA_SINGLE_INSTANCE_DATA + pCopyData.cbData = uint32(len(arrUtf16)*2 + 1) + pCopyData.lpData = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(data))) + + w32.SendMessage(hWnd, w32.WM_COPYDATA, 0, uintptr(unsafe.Pointer(pCopyData))) +} + +// single instance windows app +func SetupSingleInstance(title string, lock *options.SingleInstanceLock) { + // TODO: create unique id based on title. Maybe better to have something more unique? + id := "wails-app-" + title + + className := id + "-sic" + windowName := id + "-siw" + mutexName := id + "sim" + + _, err := windows.CreateMutex(nil, false, windows.StringToUTF16Ptr(mutexName)) + + if err != nil { + if err == windows.ERROR_ALREADY_EXISTS { + // app is already running + hwnd := w32.FindWindowW(windows.StringToUTF16Ptr(className), windows.StringToUTF16Ptr(windowName)) + + if hwnd != 0 { + data := options.SecondInstanceData{ + Args: os.Args[1:], + } + serialized, _ := json.Marshal(data) + + SendMessage(hwnd, string(serialized)) + // exit second instance of app after sending message + os.Exit(0) + } + + panic("unknown error on mutex") + } + } + + createEventTargetWindow(className, windowName, lock) +} + +func SingleInstanceHWND(hwnd w32.HWND) { + mainHWND = hwnd +} + +func createEventTargetWindow(className string, windowName string, lock *options.SingleInstanceLock) w32.HWND { + // callback handler in the event target window + wndProc := func( + hwnd w32.HWND, msg uint32, wparam w32.WPARAM, lparam w32.LPARAM, + ) w32.LRESULT { + if msg == w32.WM_COPYDATA { + ldata := (*COPYDATASTRUCT)(unsafe.Pointer(lparam)) + + if ldata.dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA { + serialized := uintptrToString(ldata.lpData) + + var secondInstanceData options.SecondInstanceData + + json.Unmarshal([]byte(serialized), &secondInstanceData) + + go lock.OnSecondInstanceLaunch(secondInstanceData) + + if lock.ActivateAppOnSubsequentLaunch && mainHWND != 0 { + w32.SendMessage(hwnd, w32.WM_SYSCOMMAND, SC_RESTORE, 0) // restore the minimize window + w32.SetWindowPos(hwnd, w32.HWND_TOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // force set our main window on top + w32.SetWindowPos(hwnd, w32.HWND_NOTOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // remove topmost to allow normal windows manipulations + w32.SetForegroundWindow(hwnd) // put window on tops + } + } + + return w32.LRESULT(0) + } + + return w32.DefWindowProc(hwnd, msg, wparam, lparam) + } + + var class w32.WNDCLASSEX + class.Size = uint32(unsafe.Sizeof(class)) + class.Style = 0 + class.WndProc = syscall.NewCallback(wndProc) + class.ClsExtra = 0 + class.WndExtra = 0 + class.Instance = w32.GetModuleHandle("") + class.Icon = 0 + class.Cursor = 0 + class.Background = 0 + class.MenuName = nil + class.ClassName = windows.StringToUTF16Ptr(className) + class.IconSm = 0 + + w32.RegisterClassEx(&class) + + // create event window that will not be visible for user + hwnd := w32.CreateWindowEx( + w32.WS_EX_NOACTIVATE| + w32.WS_EX_TRANSPARENT| + w32.WS_EX_LAYERED| + // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which + // we want to avoid. If you remove this style, this window won't show up in the + // taskbar *initially*, but it can show up at some later point. This can sometimes + // happen on its own after several hours have passed, although this has proven + // difficult to reproduce. Alternatively, it can be manually triggered by killing + // `explorer.exe` and then starting the process back up. + // It is unclear why the bug is triggered by waiting for several hours. + w32.WS_EX_TOOLWINDOW, + windows.StringToUTF16Ptr(className), + windows.StringToUTF16Ptr(windowName), + w32.WS_OVERLAPPED, + 0, + 0, + 0, + 0, + 0, + 0, + w32.GetModuleHandle(""), + nil, + ) + + w32.SetWindowLongPtr( + hwnd, + w32.GWL_STYLE, + // The window technically has to be visible to receive WM_PAINT messages (which are used + // for delivering events during resizes), but it isn't displayed to the user because of + // the LAYERED style. + w32.WS_VISIBLE|w32.WS_POPUP, + ) + + return hwnd +} + +func uintptrToString(cstr uintptr) string { + if cstr != 0 { + us := make([]uint16, 0, 256) + for p := cstr; ; p += 2 { + u := *(*uint16)(unsafe.Pointer(p)) + if u == 0 { + return string(utf16.Decode(us)) + } + us = append(us, u) + } + } + return "" +} diff --git a/v2/internal/frontend/desktop/windows/winc/w32/user32.go b/v2/internal/frontend/desktop/windows/winc/w32/user32.go index ec5e4a5960c..8ca72ce4b44 100644 --- a/v2/internal/frontend/desktop/windows/winc/w32/user32.go +++ b/v2/internal/frontend/desktop/windows/winc/w32/user32.go @@ -24,6 +24,7 @@ var ( procShowWindowAsync = moduser32.NewProc("ShowWindowAsync") procUpdateWindow = moduser32.NewProc("UpdateWindow") procCreateWindowEx = moduser32.NewProc("CreateWindowExW") + procFindWindowW = moduser32.NewProc("FindWindowW") procAdjustWindowRect = moduser32.NewProc("AdjustWindowRect") procAdjustWindowRectEx = moduser32.NewProc("AdjustWindowRectEx") procDestroyWindow = moduser32.NewProc("DestroyWindow") @@ -263,6 +264,14 @@ func CreateWindowEx(exStyle uint, className, windowName *uint16, return HWND(ret) } +func FindWindowW(className, windowName *uint16) HWND { + ret, _, _ := procFindWindowW.Call( + uintptr(unsafe.Pointer(className)), + uintptr(unsafe.Pointer(windowName))) + + return HWND(ret) +} + func AdjustWindowRectEx(rect *RECT, style uint, menu bool, exStyle uint) bool { ret, _, _ := procAdjustWindowRectEx.Call( uintptr(unsafe.Pointer(rect)), diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go index 07337095693..d0de0851e3e 100644 --- a/v2/pkg/options/options.go +++ b/v2/pkg/options/options.go @@ -29,6 +29,17 @@ const ( type Experimental struct { } +type SingleInstanceLock struct { + Enabled bool + // When true, the original app will be activated when a second instance is launched + ActivateAppOnSubsequentLaunch bool + OnSecondInstanceLaunch func(secondInstanceData SecondInstanceData) +} + +type SecondInstanceData struct { + Args []string +} + // App contains options for creating the App type App struct { Title string @@ -84,6 +95,8 @@ type App struct { // services of Apple and Microsoft. EnableFraudulentWebsiteDetection bool + SingleInstanceLock *SingleInstanceLock + Windows *windows.Options Mac *mac.Options Linux *linux.Options From a4d0050674838e2b8ca7211f59bd44b77e4a0694 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sun, 24 Sep 2023 03:08:03 +0100 Subject: [PATCH 19/60] fix focusing --- .../desktop/windows/winc/single_instance.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/v2/internal/frontend/desktop/windows/winc/single_instance.go b/v2/internal/frontend/desktop/windows/winc/single_instance.go index cfa29ce6ec5..3a1bcc0e002 100644 --- a/v2/internal/frontend/desktop/windows/winc/single_instance.go +++ b/v2/internal/frontend/desktop/windows/winc/single_instance.go @@ -20,7 +20,7 @@ type COPYDATASTRUCT struct { lpData uintptr } -var mainHWND w32.HWND +var mainHWND *w32.HWND const WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542 const SC_RESTORE = 0xF120 @@ -70,7 +70,7 @@ func SetupSingleInstance(title string, lock *options.SingleInstanceLock) { createEventTargetWindow(className, windowName, lock) } -func SingleInstanceHWND(hwnd w32.HWND) { +func SingleInstanceHWND(hwnd *w32.HWND) { mainHWND = hwnd } @@ -91,11 +91,11 @@ func createEventTargetWindow(className string, windowName string, lock *options. go lock.OnSecondInstanceLaunch(secondInstanceData) - if lock.ActivateAppOnSubsequentLaunch && mainHWND != 0 { - w32.SendMessage(hwnd, w32.WM_SYSCOMMAND, SC_RESTORE, 0) // restore the minimize window - w32.SetWindowPos(hwnd, w32.HWND_TOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // force set our main window on top - w32.SetWindowPos(hwnd, w32.HWND_NOTOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // remove topmost to allow normal windows manipulations - w32.SetForegroundWindow(hwnd) // put window on tops + if lock.ActivateAppOnSubsequentLaunch && mainHWND != nil { + w32.SendMessage(*mainHWND, w32.WM_SYSCOMMAND, SC_RESTORE, 0) // restore the minimize window + w32.SetWindowPos(*mainHWND, w32.HWND_TOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // force set our main window on top + w32.SetWindowPos(*mainHWND, w32.HWND_NOTOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // remove topmost to allow normal windows manipulations + w32.SetForegroundWindow(*mainHWND) // put window on tops } } From a89cbb1d20d3675e1204a3aaa07388ab05b2f797 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sun, 24 Sep 2023 03:10:13 +0100 Subject: [PATCH 20/60] fix focusing --- .../desktop/windows/winc/single_instance.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/v2/internal/frontend/desktop/windows/winc/single_instance.go b/v2/internal/frontend/desktop/windows/winc/single_instance.go index 3a1bcc0e002..56c46340a9d 100644 --- a/v2/internal/frontend/desktop/windows/winc/single_instance.go +++ b/v2/internal/frontend/desktop/windows/winc/single_instance.go @@ -20,7 +20,7 @@ type COPYDATASTRUCT struct { lpData uintptr } -var mainHWND *w32.HWND +var mainHWND w32.HWND const WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542 const SC_RESTORE = 0xF120 @@ -70,7 +70,7 @@ func SetupSingleInstance(title string, lock *options.SingleInstanceLock) { createEventTargetWindow(className, windowName, lock) } -func SingleInstanceHWND(hwnd *w32.HWND) { +func SingleInstanceHWND(hwnd w32.HWND) { mainHWND = hwnd } @@ -91,11 +91,11 @@ func createEventTargetWindow(className string, windowName string, lock *options. go lock.OnSecondInstanceLaunch(secondInstanceData) - if lock.ActivateAppOnSubsequentLaunch && mainHWND != nil { - w32.SendMessage(*mainHWND, w32.WM_SYSCOMMAND, SC_RESTORE, 0) // restore the minimize window - w32.SetWindowPos(*mainHWND, w32.HWND_TOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // force set our main window on top - w32.SetWindowPos(*mainHWND, w32.HWND_NOTOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // remove topmost to allow normal windows manipulations - w32.SetForegroundWindow(*mainHWND) // put window on tops + if lock.ActivateAppOnSubsequentLaunch && mainHWND != 0 { + w32.SendMessage(mainHWND, w32.WM_SYSCOMMAND, SC_RESTORE, 0) // restore the minimize window + w32.SetWindowPos(mainHWND, w32.HWND_TOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // force set our main window on top + w32.SetWindowPos(mainHWND, w32.HWND_NOTOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // remove topmost to allow normal windows manipulations + w32.SetForegroundWindow(mainHWND) // put window on tops } } From 9b9be204d06d398163edff220a908ed54c91d1ea Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sun, 24 Sep 2023 03:16:56 +0100 Subject: [PATCH 21/60] formatting --- .../frontend/desktop/windows/frontend.go | 2 +- .../desktop/windows/winc/single_instance.go | 41 ++++++++++++++----- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index 609a7ef31ec..fdf99685f68 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -145,7 +145,7 @@ func (f *Frontend) Run(ctx context.Context) error { f.mainWindow = mainWindow if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.Enabled { - winc.SingleInstanceHWND(f.mainWindow.Handle()) + winc.SingleInstanceMainWindowHWND(f.mainWindow.Handle()) } var _debug = ctx.Value("debug") diff --git a/v2/internal/frontend/desktop/windows/winc/single_instance.go b/v2/internal/frontend/desktop/windows/winc/single_instance.go index 56c46340a9d..13ca97cbd15 100644 --- a/v2/internal/frontend/desktop/windows/winc/single_instance.go +++ b/v2/internal/frontend/desktop/windows/winc/single_instance.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package winc @@ -20,12 +19,12 @@ type COPYDATASTRUCT struct { lpData uintptr } -var mainHWND w32.HWND +var mainWindowHWND w32.HWND const WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542 const SC_RESTORE = 0xF120 -func SendMessage(hWnd w32.HWND, data string) { +func SendMessage(hwnd w32.HWND, data string) { arrUtf16, _ := syscall.UTF16FromString(data) pCopyData := new(COPYDATASTRUCT) @@ -33,7 +32,7 @@ func SendMessage(hWnd w32.HWND, data string) { pCopyData.cbData = uint32(len(arrUtf16)*2 + 1) pCopyData.lpData = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(data))) - w32.SendMessage(hWnd, w32.WM_COPYDATA, 0, uintptr(unsafe.Pointer(pCopyData))) + w32.SendMessage(hwnd, w32.WM_COPYDATA, 0, uintptr(unsafe.Pointer(pCopyData))) } // single instance windows app @@ -70,8 +69,8 @@ func SetupSingleInstance(title string, lock *options.SingleInstanceLock) { createEventTargetWindow(className, windowName, lock) } -func SingleInstanceHWND(hwnd w32.HWND) { - mainHWND = hwnd +func SingleInstanceMainWindowHWND(hwnd w32.HWND) { + mainWindowHWND = hwnd } func createEventTargetWindow(className string, windowName string, lock *options.SingleInstanceLock) w32.HWND { @@ -91,11 +90,31 @@ func createEventTargetWindow(className string, windowName string, lock *options. go lock.OnSecondInstanceLaunch(secondInstanceData) - if lock.ActivateAppOnSubsequentLaunch && mainHWND != 0 { - w32.SendMessage(mainHWND, w32.WM_SYSCOMMAND, SC_RESTORE, 0) // restore the minimize window - w32.SetWindowPos(mainHWND, w32.HWND_TOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // force set our main window on top - w32.SetWindowPos(mainHWND, w32.HWND_NOTOPMOST, 0, 0, 0, 0, w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE) // remove topmost to allow normal windows manipulations - w32.SetForegroundWindow(mainHWND) // put window on tops + if lock.ActivateAppOnSubsequentLaunch && mainWindowHWND != 0 { + // restore the minimized window, if it is + w32.SendMessage( + mainWindowHWND, + w32.WM_SYSCOMMAND, + SC_RESTORE, + 0, + ) + // WindowPos is used with HWND_TOPMOST to guarantee bring our app on top + // force set our main window on top + w32.SetWindowPos( + mainWindowHWND, + w32.HWND_TOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // remove topmost to allow normal windows manipulations + w32.SetWindowPos( + mainWindowHWND, + w32.HWND_NOTOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // put main window on tops foreground + w32.SetForegroundWindow(mainWindowHWND) } } From 967f6d6744db18e4b34a7f046a74b76de395729b Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sun, 24 Sep 2023 12:36:54 +0100 Subject: [PATCH 22/60] use channel buffer for second instance events --- .../frontend/desktop/windows/frontend.go | 22 ++++++++++++++++++- .../desktop/windows/winc/single_instance.go | 15 ++++++------- v2/pkg/options/options.go | 4 +++- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index fdf99685f68..27a631771c2 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -35,6 +35,8 @@ import ( const startURL = "http://wails.localhost/" +var secondInstanceBuffer = make(chan options.SecondInstanceData, 100) + type Screen = frontend.Screen type Frontend struct { @@ -113,6 +115,8 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. } result.assets = assets + go result.startSecondInstanceProcessor() + return result } @@ -138,7 +142,11 @@ func (f *Frontend) Run(ctx context.Context) error { f.chromium = edge.NewChromium() if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.Enabled { - winc.SetupSingleInstance(f.frontendOptions.Title, f.frontendOptions.SingleInstanceLock) + winc.SetupSingleInstance( + f.frontendOptions.SingleInstanceLock.UniqueID, + f.frontendOptions.SingleInstanceLock.ActivateAppOnSubsequentLaunch, + addSecondInstanceDataToBuffer, + ) } mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo, f.chromium) @@ -833,3 +841,15 @@ func (f *Frontend) ShowWindow() { func (f *Frontend) onFocus(arg *winc.Event) { f.chromium.Focus() } + +func addSecondInstanceDataToBuffer(data options.SecondInstanceData) { + secondInstanceBuffer <- data +} + +func (f *Frontend) startSecondInstanceProcessor() { + for secondInstanceData := range secondInstanceBuffer { + if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + } + } +} diff --git a/v2/internal/frontend/desktop/windows/winc/single_instance.go b/v2/internal/frontend/desktop/windows/winc/single_instance.go index 13ca97cbd15..4e368189c95 100644 --- a/v2/internal/frontend/desktop/windows/winc/single_instance.go +++ b/v2/internal/frontend/desktop/windows/winc/single_instance.go @@ -35,10 +35,9 @@ func SendMessage(hwnd w32.HWND, data string) { w32.SendMessage(hwnd, w32.WM_COPYDATA, 0, uintptr(unsafe.Pointer(pCopyData))) } -// single instance windows app -func SetupSingleInstance(title string, lock *options.SingleInstanceLock) { - // TODO: create unique id based on title. Maybe better to have something more unique? - id := "wails-app-" + title +// ingle instance Windows app +func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, callback func(data options.SecondInstanceData)) { + id := "wails-app-" + uniqueID className := id + "-sic" windowName := id + "-siw" @@ -66,14 +65,14 @@ func SetupSingleInstance(title string, lock *options.SingleInstanceLock) { } } - createEventTargetWindow(className, windowName, lock) + createEventTargetWindow(className, windowName, activateAppOnSubsequentLaunch, callback) } func SingleInstanceMainWindowHWND(hwnd w32.HWND) { mainWindowHWND = hwnd } -func createEventTargetWindow(className string, windowName string, lock *options.SingleInstanceLock) w32.HWND { +func createEventTargetWindow(className string, windowName string, activateAppOnSubsequentLaunch bool, callback func(data options.SecondInstanceData)) w32.HWND { // callback handler in the event target window wndProc := func( hwnd w32.HWND, msg uint32, wparam w32.WPARAM, lparam w32.LPARAM, @@ -88,9 +87,9 @@ func createEventTargetWindow(className string, windowName string, lock *options. json.Unmarshal([]byte(serialized), &secondInstanceData) - go lock.OnSecondInstanceLaunch(secondInstanceData) + go callback(secondInstanceData) - if lock.ActivateAppOnSubsequentLaunch && mainWindowHWND != 0 { + if activateAppOnSubsequentLaunch && mainWindowHWND != 0 { // restore the minimized window, if it is w32.SendMessage( mainWindowHWND, diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go index d0de0851e3e..e73d2bc5307 100644 --- a/v2/pkg/options/options.go +++ b/v2/pkg/options/options.go @@ -30,7 +30,9 @@ type Experimental struct { } type SingleInstanceLock struct { - Enabled bool + // TODO: create unique id based on title? Maybe require user to set unique id? + UniqueID string + Enabled bool // When true, the original app will be activated when a second instance is launched ActivateAppOnSubsequentLaunch bool OnSecondInstanceLaunch func(secondInstanceData SecondInstanceData) From 4a75912a437f5c259f82e3279abb943209c0e70a Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sun, 24 Sep 2023 12:49:38 +0100 Subject: [PATCH 23/60] handle errors --- .../desktop/windows/winc/single_instance.go | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/v2/internal/frontend/desktop/windows/winc/single_instance.go b/v2/internal/frontend/desktop/windows/winc/single_instance.go index 4e368189c95..a2e08962cb8 100644 --- a/v2/internal/frontend/desktop/windows/winc/single_instance.go +++ b/v2/internal/frontend/desktop/windows/winc/single_instance.go @@ -21,7 +21,10 @@ type COPYDATASTRUCT struct { var mainWindowHWND w32.HWND +// WMCOPYDATA_SINGLE_INSTANCE_DATA we define our own type for WM_COPYDATA message const WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542 + +// SC_RESTORE param for WM_SYSCOMMAND to restore app if it is minimized const SC_RESTORE = 0xF120 func SendMessage(hwnd w32.HWND, data string) { @@ -35,7 +38,7 @@ func SendMessage(hwnd w32.HWND, data string) { w32.SendMessage(hwnd, w32.WM_COPYDATA, 0, uintptr(unsafe.Pointer(pCopyData))) } -// ingle instance Windows app +// SetupSingleInstance single instance Windows app func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, callback func(data options.SecondInstanceData)) { id := "wails-app-" + uniqueID @@ -60,12 +63,11 @@ func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, ca // exit second instance of app after sending message os.Exit(0) } - - panic("unknown error on mutex") + // if we got any other unknown error we will just start new application instance } + } else { + createEventTargetWindow(className, windowName, activateAppOnSubsequentLaunch, callback) } - - createEventTargetWindow(className, windowName, activateAppOnSubsequentLaunch, callback) } func SingleInstanceMainWindowHWND(hwnd w32.HWND) { @@ -85,35 +87,39 @@ func createEventTargetWindow(className string, windowName string, activateAppOnS var secondInstanceData options.SecondInstanceData - json.Unmarshal([]byte(serialized), &secondInstanceData) - - go callback(secondInstanceData) - - if activateAppOnSubsequentLaunch && mainWindowHWND != 0 { - // restore the minimized window, if it is - w32.SendMessage( - mainWindowHWND, - w32.WM_SYSCOMMAND, - SC_RESTORE, - 0, - ) - // WindowPos is used with HWND_TOPMOST to guarantee bring our app on top - // force set our main window on top - w32.SetWindowPos( - mainWindowHWND, - w32.HWND_TOPMOST, - 0, 0, 0, 0, - w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, - ) - // remove topmost to allow normal windows manipulations - w32.SetWindowPos( - mainWindowHWND, - w32.HWND_NOTOPMOST, - 0, 0, 0, 0, - w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, - ) - // put main window on tops foreground - w32.SetForegroundWindow(mainWindowHWND) + err := json.Unmarshal([]byte(serialized), &secondInstanceData) + + if err == nil { + // pass callback to first instance + go callback(secondInstanceData) + + // if activateAppOnSubsequentLaunch is true, we will try to activate the first instance + if activateAppOnSubsequentLaunch && mainWindowHWND != 0 { + // restore the minimized window, if it is + w32.SendMessage( + mainWindowHWND, + w32.WM_SYSCOMMAND, + SC_RESTORE, + 0, + ) + // WindowPos is used with HWND_TOPMOST to guarantee bring our app on top + // force set our main window on top + w32.SetWindowPos( + mainWindowHWND, + w32.HWND_TOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // remove topmost to allow normal windows manipulations + w32.SetWindowPos( + mainWindowHWND, + w32.HWND_NOTOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // put main window on tops foreground + w32.SetForegroundWindow(mainWindowHWND) + } } } From 340d969cc7a15aafad0afe1cb226f73ac461b7f7 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Sun, 24 Sep 2023 12:53:13 +0100 Subject: [PATCH 24/60] add comment --- v2/internal/frontend/desktop/windows/frontend.go | 1 + 1 file changed, 1 insertion(+) diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index 27a631771c2..a48fa98c699 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -152,6 +152,7 @@ func (f *Frontend) Run(ctx context.Context) error { mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo, f.chromium) f.mainWindow = mainWindow + // we should pass HWND to single_instance so that when we want to make the window active we can do this correctly for client only window if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.Enabled { winc.SingleInstanceMainWindowHWND(f.mainWindow.Handle()) } From ab1e0a9f4befdc800937cbfe1512c114a1c7a98f Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 25 Sep 2023 21:00:37 +0100 Subject: [PATCH 25/60] remove unused option in file association --- v2/internal/project/project.go | 1 - 1 file changed, 1 deletion(-) diff --git a/v2/internal/project/project.go b/v2/internal/project/project.go index 91b6d5eec2e..a0bf518a9c9 100644 --- a/v2/internal/project/project.go +++ b/v2/internal/project/project.go @@ -228,7 +228,6 @@ type FileAssociation struct { Ext string `json:"ext"` Name string `json:"name"` Description string `json:"description"` - MimeType string `json:"mimeType"` IconName string `json:"iconName"` Role string `json:"role"` } From 5fb1603d364bd3c34629db7c88677ed0562d64ff Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Tue, 26 Sep 2023 21:23:18 +0100 Subject: [PATCH 26/60] wip: linux single instance lock --- v2/go.mod | 1 + v2/go.sum | 2 + .../frontend/desktop/linux/frontend.go | 14 +++++ .../frontend/desktop/linux/single_instance.go | 52 +++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 v2/internal/frontend/desktop/linux/single_instance.go diff --git a/v2/go.mod b/v2/go.mod index 93e178d810c..e1615159611 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -12,6 +12,7 @@ require ( github.com/fsnotify/fsnotify v1.4.9 github.com/go-git/go-git/v5 v5.3.0 github.com/go-ole/go-ole v1.2.6 + github.com/godbus/dbus/v5 v5.1.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.3.0 github.com/jackmordaunt/icns v1.0.0 diff --git a/v2/go.sum b/v2/go.sum index 59a62541150..ab8255fafa7 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -65,6 +65,8 @@ github.com/go-git/go-git/v5 v5.3.0 h1:8WKMtJR2j8RntEXR/uvTKagfEt4GYlwQ7mntE4+0GW github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index 70f8468b674..730671493ff 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -102,6 +102,8 @@ var initOnce = sync.Once{} const startURL = "wails://wails/" +var secondInstanceBuffer = make(chan options.SecondInstanceData, 100) + type Frontend struct { // Context @@ -235,6 +237,14 @@ func (f *Frontend) Run(ctx context.Context) error { } }() + if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.Enabled { + SetupSingleInstance( + f.frontendOptions.SingleInstanceLock.UniqueID, + f.frontendOptions.SingleInstanceLock.ActivateAppOnSubsequentLaunch, + addSecondInstanceDataToBuffer, + ) + } + f.mainWindow.Run(f.startURL.String()) return nil @@ -505,3 +515,7 @@ func (f *Frontend) startRequestProcessor() { func processURLRequest(request unsafe.Pointer) { requestBuffer <- webview.NewRequest(request) } + +func addSecondInstanceDataToBuffer(data options.SecondInstanceData) { + secondInstanceBuffer <- data +} diff --git a/v2/internal/frontend/desktop/linux/single_instance.go b/v2/internal/frontend/desktop/linux/single_instance.go new file mode 100644 index 00000000000..9872be28fe9 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/single_instance.go @@ -0,0 +1,52 @@ +//go:build linux +// +build linux + +package linux + +import ( + "encoding/json" + "github.com/godbus/dbus/v5" + "github.com/wailsapp/wails/v2/pkg/options" + "os" + "strings" +) + +func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, callback func(data options.SecondInstanceData)) { + id := "wails_app_" + strings.ReplaceAll(uniqueID, "-", "_") + + dbusName := "org." + id + ".SingleInstance" + dbusPath := "/org/" + id + "/SingleInstance" + var err error + + conn, err := dbus.ConnectSessionBus() + if err != nil { + + } + + f := func(message string) { + var secondInstanceData options.SecondInstanceData + + err := json.Unmarshal([]byte(message), &secondInstanceData) + + if err != nil { + go callback(secondInstanceData) + } + } + + conn.Export(f, dbus.ObjectPath(dbusPath), dbusName) + + reply, err := conn.RequestName(dbusName, dbus.NameFlagDoNotQueue) + if err != nil { + panic(err) + } + + // name already taken, so send args using dbus to existing instance and exit + if reply != dbus.RequestNameReplyPrimaryOwner { + data := options.SecondInstanceData{ + Args: os.Args[1:], + } + serialized, _ := json.Marshal(data) + conn.Object(dbusName, dbus.ObjectPath(dbusPath)).Call(dbusName, 0, serialized) + os.Exit(1) + } +} From a8e41e8b1e4e995db5ee4dd2aa894a70b04242ae Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Tue, 26 Sep 2023 23:12:42 +0100 Subject: [PATCH 27/60] wip: linux single instance --- .../frontend/desktop/linux/frontend.go | 10 +++++++ .../frontend/desktop/linux/single_instance.go | 30 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index 730671493ff..52a7a9af7ec 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -203,6 +203,8 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. C.free(unsafe.Pointer(prgname)) } + go result.startSecondInstanceProcessor() + return result } @@ -519,3 +521,11 @@ func processURLRequest(request unsafe.Pointer) { func addSecondInstanceDataToBuffer(data options.SecondInstanceData) { secondInstanceBuffer <- data } + +func (f *Frontend) startSecondInstanceProcessor() { + for secondInstanceData := range secondInstanceBuffer { + if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + } + } +} diff --git a/v2/internal/frontend/desktop/linux/single_instance.go b/v2/internal/frontend/desktop/linux/single_instance.go index 9872be28fe9..f730c541174 100644 --- a/v2/internal/frontend/desktop/linux/single_instance.go +++ b/v2/internal/frontend/desktop/linux/single_instance.go @@ -5,12 +5,20 @@ package linux import ( "encoding/json" + "fmt" "github.com/godbus/dbus/v5" "github.com/wailsapp/wails/v2/pkg/options" "os" "strings" ) +type dbusHandler func(string) + +func (f dbusHandler) SendMessage(message string) *dbus.Error { + f(message) + return nil +} + func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, callback func(data options.SecondInstanceData)) { id := "wails_app_" + strings.ReplaceAll(uniqueID, "-", "_") @@ -19,11 +27,13 @@ func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, ca var err error conn, err := dbus.ConnectSessionBus() + println("dbus connected") if err != nil { } - f := func(message string) { + f := dbusHandler(func(message string) { + println("got message", message) var secondInstanceData options.SecondInstanceData err := json.Unmarshal([]byte(message), &secondInstanceData) @@ -31,22 +41,34 @@ func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, ca if err != nil { go callback(secondInstanceData) } - } + }) + println("try to export callback") conn.Export(f, dbus.ObjectPath(dbusPath), dbusName) + println("try to request name") reply, err := conn.RequestName(dbusName, dbus.NameFlagDoNotQueue) if err != nil { + println("error on requesting name") panic(err) } + println("got reply", reply) + // name already taken, so send args using dbus to existing instance and exit - if reply != dbus.RequestNameReplyPrimaryOwner { + if reply == dbus.RequestNameReplyExists { + println("name already taken, sending request to existing instance") data := options.SecondInstanceData{ Args: os.Args[1:], } serialized, _ := json.Marshal(data) - conn.Object(dbusName, dbus.ObjectPath(dbusPath)).Call(dbusName, 0, serialized) + + err = conn.Object(dbusName, dbus.ObjectPath(dbusPath)).Call(dbusName+".SendMessage", 0, string(serialized)).Store() + + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to call Foo function (is the server example running?):", err) + } + println("sent successfully", string(serialized)) os.Exit(1) } } From 98f75967f0fc477ce1a83e638c330fc3cf06b1cf Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Tue, 26 Sep 2023 23:46:29 +0100 Subject: [PATCH 28/60] some experiments with making window active --- v2/go.mod | 2 ++ v2/go.sum | 4 +++ .../frontend/desktop/linux/single_instance.go | 34 ++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/v2/go.mod b/v2/go.mod index e1615159611..3f47ecc2f6e 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -3,6 +3,7 @@ module github.com/wailsapp/wails/v2 go 1.18 require ( + github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 github.com/Masterminds/semver v1.5.0 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/bep/debounce v1.2.1 @@ -47,6 +48,7 @@ require ( atomicgo.dev/cursor v0.1.1 // indirect atomicgo.dev/keyboard v0.2.8 // indirect bitbucket.org/creachadair/shell v0.0.7 // indirect + github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect github.com/Microsoft/go-winio v0.4.16 // indirect github.com/alecthomas/chroma v0.10.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect diff --git a/v2/go.sum b/v2/go.sum index ab8255fafa7..270280aabe9 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -4,6 +4,10 @@ atomicgo.dev/keyboard v0.2.8 h1:Di09BitwZgdTV1hPyX/b9Cqxi8HVuJQwWivnZUEqlj4= atomicgo.dev/keyboard v0.2.8/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= bitbucket.org/creachadair/shell v0.0.7 h1:Z96pB6DkSb7F3Y3BBnJeOZH2gazyMTWlvecSD4vDqfk= bitbucket.org/creachadair/shell v0.0.7/go.mod h1:oqtXSSvSYr4624lnnabXHaBsYW6RD80caLi2b3hJk0U= +github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk= +github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= +github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= diff --git a/v2/internal/frontend/desktop/linux/single_instance.go b/v2/internal/frontend/desktop/linux/single_instance.go index f730c541174..7fd97ea4ae1 100644 --- a/v2/internal/frontend/desktop/linux/single_instance.go +++ b/v2/internal/frontend/desktop/linux/single_instance.go @@ -6,6 +6,8 @@ package linux import ( "encoding/json" "fmt" + "github.com/BurntSushi/xgbutil" + "github.com/BurntSushi/xgbutil/ewmh" "github.com/godbus/dbus/v5" "github.com/wailsapp/wails/v2/pkg/options" "os" @@ -26,6 +28,11 @@ func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, ca dbusPath := "/org/" + id + "/SingleInstance" var err error + xConn, err := xgbutil.NewConn() + if err != nil { + // fatal + } + conn, err := dbus.ConnectSessionBus() println("dbus connected") if err != nil { @@ -38,9 +45,34 @@ func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, ca err := json.Unmarshal([]byte(message), &secondInstanceData) - if err != nil { + if err == nil { + println("calling callback") go callback(secondInstanceData) + + if activateAppOnSubsequentLaunch { + clientids, err := ewmh.ClientListGet(xConn) + if err != nil { + //log.Fatal(err) + } + + for _, clientid := range clientids { + pid, err := ewmh.WmPidGet(xConn, clientid) + if err != nil { + //log.Fatal(err) + } + + if pid == uint(os.Getpid()) { + println("matched pid", pid) + + err := ewmh.ActiveWindowSet(xConn, clientid) + if err != nil { + //log.Fatal(err) + } + } + } + } } + fmt.Fprintln(os.Stderr, "Failed to deserialize", err) }) println("try to export callback") From 575a23e2d46e2114312f76bba724faf2324d7d31 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Wed, 27 Sep 2023 00:40:30 +0100 Subject: [PATCH 29/60] try to use unminimise --- .../frontend/desktop/linux/frontend.go | 3 +++ .../frontend/desktop/linux/single_instance.go | 26 ++----------------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index 52a7a9af7ec..3cf505957ee 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -526,6 +526,9 @@ func (f *Frontend) startSecondInstanceProcessor() { for secondInstanceData := range secondInstanceBuffer { if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + if f.frontendOptions.SingleInstanceLock.ActivateAppOnSubsequentLaunch { + f.mainWindow.UnMinimise() + } } } } diff --git a/v2/internal/frontend/desktop/linux/single_instance.go b/v2/internal/frontend/desktop/linux/single_instance.go index 7fd97ea4ae1..1e6e4201a61 100644 --- a/v2/internal/frontend/desktop/linux/single_instance.go +++ b/v2/internal/frontend/desktop/linux/single_instance.go @@ -7,7 +7,6 @@ import ( "encoding/json" "fmt" "github.com/BurntSushi/xgbutil" - "github.com/BurntSushi/xgbutil/ewmh" "github.com/godbus/dbus/v5" "github.com/wailsapp/wails/v2/pkg/options" "os" @@ -49,30 +48,9 @@ func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, ca println("calling callback") go callback(secondInstanceData) - if activateAppOnSubsequentLaunch { - clientids, err := ewmh.ClientListGet(xConn) - if err != nil { - //log.Fatal(err) - } - - for _, clientid := range clientids { - pid, err := ewmh.WmPidGet(xConn, clientid) - if err != nil { - //log.Fatal(err) - } - - if pid == uint(os.Getpid()) { - println("matched pid", pid) - - err := ewmh.ActiveWindowSet(xConn, clientid) - if err != nil { - //log.Fatal(err) - } - } - } - } + } else { + fmt.Fprintln(os.Stderr, "Failed to deserialize", err) } - fmt.Fprintln(os.Stderr, "Failed to deserialize", err) }) println("try to export callback") From b1a0745654bf16e0d78f007d8574217ec19d79c0 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Wed, 27 Sep 2023 00:41:43 +0100 Subject: [PATCH 30/60] remove unused --- v2/internal/frontend/desktop/linux/single_instance.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/v2/internal/frontend/desktop/linux/single_instance.go b/v2/internal/frontend/desktop/linux/single_instance.go index 1e6e4201a61..c80a3789a2c 100644 --- a/v2/internal/frontend/desktop/linux/single_instance.go +++ b/v2/internal/frontend/desktop/linux/single_instance.go @@ -6,7 +6,6 @@ package linux import ( "encoding/json" "fmt" - "github.com/BurntSushi/xgbutil" "github.com/godbus/dbus/v5" "github.com/wailsapp/wails/v2/pkg/options" "os" @@ -27,11 +26,6 @@ func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, ca dbusPath := "/org/" + id + "/SingleInstance" var err error - xConn, err := xgbutil.NewConn() - if err != nil { - // fatal - } - conn, err := dbus.ConnectSessionBus() println("dbus connected") if err != nil { From dac6decb458cf9dd97b023ee9e63eaf59f3f0918 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Wed, 27 Sep 2023 00:47:17 +0100 Subject: [PATCH 31/60] try present for window --- v2/internal/frontend/desktop/linux/frontend.go | 2 +- v2/internal/frontend/desktop/linux/window.go | 4 ++++ v2/internal/frontend/desktop/linux/window.h | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index 3cf505957ee..829eae854de 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -527,7 +527,7 @@ func (f *Frontend) startSecondInstanceProcessor() { if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) if f.frontendOptions.SingleInstanceLock.ActivateAppOnSubsequentLaunch { - f.mainWindow.UnMinimise() + f.mainWindow.Present() } } } diff --git a/v2/internal/frontend/desktop/linux/window.go b/v2/internal/frontend/desktop/linux/window.go index b10ac1ebd7e..b57461f72b7 100644 --- a/v2/internal/frontend/desktop/linux/window.go +++ b/v2/internal/frontend/desktop/linux/window.go @@ -246,6 +246,10 @@ func (w *Window) Minimise() { C.ExecuteOnMainThread(C.Minimise, C.gpointer(w.asGTKWindow())) } +func (w *Window) Present() { + C.ExecuteOnMainThread(C.Present, C.gpointer(w.asGTKWindow())) +} + func (w *Window) UnMinimise() { C.ExecuteOnMainThread(C.UnMinimise, C.gpointer(w.asGTKWindow())) } diff --git a/v2/internal/frontend/desktop/linux/window.h b/v2/internal/frontend/desktop/linux/window.h index aa9499d73ed..db912e6bbd8 100644 --- a/v2/internal/frontend/desktop/linux/window.h +++ b/v2/internal/frontend/desktop/linux/window.h @@ -101,6 +101,7 @@ gboolean Hide(gpointer data); gboolean Maximise(gpointer data); gboolean UnMaximise(gpointer data); gboolean Minimise(gpointer data); +gboolean Present(gpointer data); gboolean UnMinimise(gpointer data); gboolean Fullscreen(gpointer data); gboolean UnFullscreen(gpointer data); From 0d0e88a83c9f11f8a9cc3f1e29a06f2ccf4d17a7 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Wed, 27 Sep 2023 00:49:55 +0100 Subject: [PATCH 32/60] try present for window --- v2/internal/frontend/desktop/linux/frontend.go | 3 ++- v2/internal/frontend/desktop/linux/window.go | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index 829eae854de..bab17345926 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -527,7 +527,8 @@ func (f *Frontend) startSecondInstanceProcessor() { if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) if f.frontendOptions.SingleInstanceLock.ActivateAppOnSubsequentLaunch { - f.mainWindow.Present() + println("try to activate the app") + f.mainWindow.UnMinimise() } } } diff --git a/v2/internal/frontend/desktop/linux/window.go b/v2/internal/frontend/desktop/linux/window.go index b57461f72b7..b10ac1ebd7e 100644 --- a/v2/internal/frontend/desktop/linux/window.go +++ b/v2/internal/frontend/desktop/linux/window.go @@ -246,10 +246,6 @@ func (w *Window) Minimise() { C.ExecuteOnMainThread(C.Minimise, C.gpointer(w.asGTKWindow())) } -func (w *Window) Present() { - C.ExecuteOnMainThread(C.Present, C.gpointer(w.asGTKWindow())) -} - func (w *Window) UnMinimise() { C.ExecuteOnMainThread(C.UnMinimise, C.gpointer(w.asGTKWindow())) } From 2966157a205a7e8fedd2cbfaed0d726e8457dc9a Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Wed, 27 Sep 2023 00:50:20 +0100 Subject: [PATCH 33/60] fix build --- v2/internal/frontend/desktop/linux/window.h | 1 - 1 file changed, 1 deletion(-) diff --git a/v2/internal/frontend/desktop/linux/window.h b/v2/internal/frontend/desktop/linux/window.h index db912e6bbd8..aa9499d73ed 100644 --- a/v2/internal/frontend/desktop/linux/window.h +++ b/v2/internal/frontend/desktop/linux/window.h @@ -101,7 +101,6 @@ gboolean Hide(gpointer data); gboolean Maximise(gpointer data); gboolean UnMaximise(gpointer data); gboolean Minimise(gpointer data); -gboolean Present(gpointer data); gboolean UnMinimise(gpointer data); gboolean Fullscreen(gpointer data); gboolean UnFullscreen(gpointer data); From 097155b93efa942f35725fcec2845318a5c5f228 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Wed, 27 Sep 2023 01:22:05 +0100 Subject: [PATCH 34/60] cleanup --- .../frontend/desktop/linux/frontend.go | 8 ++-- .../frontend/desktop/linux/single_instance.go | 41 +++++++----------- .../frontend/desktop/windows/frontend.go | 15 +++---- .../frontend/desktop/windows/winc/form.go | 30 +++++++++++++ .../desktop/windows/winc/single_instance.go | 43 ++----------------- 5 files changed, 60 insertions(+), 77 deletions(-) diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index bab17345926..d1fedda421b 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -242,7 +242,6 @@ func (f *Frontend) Run(ctx context.Context) error { if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.Enabled { SetupSingleInstance( f.frontendOptions.SingleInstanceLock.UniqueID, - f.frontendOptions.SingleInstanceLock.ActivateAppOnSubsequentLaunch, addSecondInstanceDataToBuffer, ) } @@ -524,12 +523,13 @@ func addSecondInstanceDataToBuffer(data options.SecondInstanceData) { func (f *Frontend) startSecondInstanceProcessor() { for secondInstanceData := range secondInstanceBuffer { - if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { - f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + if f.frontendOptions.SingleInstanceLock != nil { if f.frontendOptions.SingleInstanceLock.ActivateAppOnSubsequentLaunch { - println("try to activate the app") f.mainWindow.UnMinimise() } + if f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + } } } } diff --git a/v2/internal/frontend/desktop/linux/single_instance.go b/v2/internal/frontend/desktop/linux/single_instance.go index c80a3789a2c..a9a1830108c 100644 --- a/v2/internal/frontend/desktop/linux/single_instance.go +++ b/v2/internal/frontend/desktop/linux/single_instance.go @@ -5,7 +5,6 @@ package linux import ( "encoding/json" - "fmt" "github.com/godbus/dbus/v5" "github.com/wailsapp/wails/v2/pkg/options" "os" @@ -19,60 +18,52 @@ func (f dbusHandler) SendMessage(message string) *dbus.Error { return nil } -func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, callback func(data options.SecondInstanceData)) { - id := "wails_app_" + strings.ReplaceAll(uniqueID, "-", "_") +func SetupSingleInstance(uniqueID string, callback func(data options.SecondInstanceData)) { + id := "wails_app_" + strings.ReplaceAll(strings.ReplaceAll(uniqueID, "-", "_"), ".", "_") dbusName := "org." + id + ".SingleInstance" dbusPath := "/org/" + id + "/SingleInstance" - var err error conn, err := dbus.ConnectSessionBus() - println("dbus connected") + // if we will reach any error during establishing connection or sending message we will just continue. + // It should not be the case that such thing will happen actually, but just in case. if err != nil { - + return } f := dbusHandler(func(message string) { - println("got message", message) var secondInstanceData options.SecondInstanceData err := json.Unmarshal([]byte(message), &secondInstanceData) - if err == nil { - println("calling callback") go callback(secondInstanceData) - - } else { - fmt.Fprintln(os.Stderr, "Failed to deserialize", err) } }) - println("try to export callback") - conn.Export(f, dbus.ObjectPath(dbusPath), dbusName) + err = conn.Export(f, dbus.ObjectPath(dbusPath), dbusName) + if err != nil { + return + } - println("try to request name") reply, err := conn.RequestName(dbusName, dbus.NameFlagDoNotQueue) if err != nil { - println("error on requesting name") - panic(err) + return } - println("got reply", reply) - - // name already taken, so send args using dbus to existing instance and exit + // if name already taken, try to send args to existing instance, if no success just launch new instance if reply == dbus.RequestNameReplyExists { - println("name already taken, sending request to existing instance") data := options.SecondInstanceData{ Args: os.Args[1:], } - serialized, _ := json.Marshal(data) + serialized, err := json.Marshal(data) + if err != nil { + return + } err = conn.Object(dbusName, dbus.ObjectPath(dbusPath)).Call(dbusName+".SendMessage", 0, string(serialized)).Store() - if err != nil { - fmt.Fprintln(os.Stderr, "Failed to call Foo function (is the server example running?):", err) + return } - println("sent successfully", string(serialized)) os.Exit(1) } } diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index a48fa98c699..ede18095d42 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -144,7 +144,6 @@ func (f *Frontend) Run(ctx context.Context) error { if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.Enabled { winc.SetupSingleInstance( f.frontendOptions.SingleInstanceLock.UniqueID, - f.frontendOptions.SingleInstanceLock.ActivateAppOnSubsequentLaunch, addSecondInstanceDataToBuffer, ) } @@ -152,11 +151,6 @@ func (f *Frontend) Run(ctx context.Context) error { mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo, f.chromium) f.mainWindow = mainWindow - // we should pass HWND to single_instance so that when we want to make the window active we can do this correctly for client only window - if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.Enabled { - winc.SingleInstanceMainWindowHWND(f.mainWindow.Handle()) - } - var _debug = ctx.Value("debug") var _devtoolsEnabled = ctx.Value("devtoolsEnabled") @@ -849,8 +843,13 @@ func addSecondInstanceDataToBuffer(data options.SecondInstanceData) { func (f *Frontend) startSecondInstanceProcessor() { for secondInstanceData := range secondInstanceBuffer { - if f.frontendOptions.SingleInstanceLock != nil && f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { - f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + if f.frontendOptions.SingleInstanceLock != nil { + if f.frontendOptions.SingleInstanceLock.ActivateAppOnSubsequentLaunch { + f.mainWindow.UnMinimise() + } + if f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + } } } } diff --git a/v2/internal/frontend/desktop/windows/winc/form.go b/v2/internal/frontend/desktop/windows/winc/form.go index 9b9cadb2ce4..886495d26a8 100644 --- a/v2/internal/frontend/desktop/windows/winc/form.go +++ b/v2/internal/frontend/desktop/windows/winc/form.go @@ -139,6 +139,36 @@ func (fm *Form) Restore() { w32.ShowWindow(fm.hwnd, w32.SW_RESTORE) } +func (fm *Form) UnMinimise() { + // SC_RESTORE param for WM_SYSCOMMAND to restore app if it is minimized + const SC_RESTORE = 0xF120 + + // restore the minimized window, if it is + w32.SendMessage( + fm.hwnd, + w32.WM_SYSCOMMAND, + SC_RESTORE, + 0, + ) + // WindowPos is used with HWND_TOPMOST to guarantee bring our app on top + // force set our main window on top + w32.SetWindowPos( + fm.hwnd, + w32.HWND_TOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // remove topmost to allow normal windows manipulations + w32.SetWindowPos( + fm.hwnd, + w32.HWND_NOTOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // put main window on tops foreground + w32.SetForegroundWindow(fm.hwnd) +} + // Public methods func (fm *Form) Center() { diff --git a/v2/internal/frontend/desktop/windows/winc/single_instance.go b/v2/internal/frontend/desktop/windows/winc/single_instance.go index a2e08962cb8..a4ffb60baa1 100644 --- a/v2/internal/frontend/desktop/windows/winc/single_instance.go +++ b/v2/internal/frontend/desktop/windows/winc/single_instance.go @@ -19,14 +19,9 @@ type COPYDATASTRUCT struct { lpData uintptr } -var mainWindowHWND w32.HWND - // WMCOPYDATA_SINGLE_INSTANCE_DATA we define our own type for WM_COPYDATA message const WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542 -// SC_RESTORE param for WM_SYSCOMMAND to restore app if it is minimized -const SC_RESTORE = 0xF120 - func SendMessage(hwnd w32.HWND, data string) { arrUtf16, _ := syscall.UTF16FromString(data) @@ -39,7 +34,7 @@ func SendMessage(hwnd w32.HWND, data string) { } // SetupSingleInstance single instance Windows app -func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, callback func(data options.SecondInstanceData)) { +func SetupSingleInstance(uniqueID string, callback func(data options.SecondInstanceData)) { id := "wails-app-" + uniqueID className := id + "-sic" @@ -66,15 +61,11 @@ func SetupSingleInstance(uniqueID string, activateAppOnSubsequentLaunch bool, ca // if we got any other unknown error we will just start new application instance } } else { - createEventTargetWindow(className, windowName, activateAppOnSubsequentLaunch, callback) + createEventTargetWindow(className, windowName, callback) } } -func SingleInstanceMainWindowHWND(hwnd w32.HWND) { - mainWindowHWND = hwnd -} - -func createEventTargetWindow(className string, windowName string, activateAppOnSubsequentLaunch bool, callback func(data options.SecondInstanceData)) w32.HWND { +func createEventTargetWindow(className string, windowName string, callback func(data options.SecondInstanceData)) w32.HWND { // callback handler in the event target window wndProc := func( hwnd w32.HWND, msg uint32, wparam w32.WPARAM, lparam w32.LPARAM, @@ -92,34 +83,6 @@ func createEventTargetWindow(className string, windowName string, activateAppOnS if err == nil { // pass callback to first instance go callback(secondInstanceData) - - // if activateAppOnSubsequentLaunch is true, we will try to activate the first instance - if activateAppOnSubsequentLaunch && mainWindowHWND != 0 { - // restore the minimized window, if it is - w32.SendMessage( - mainWindowHWND, - w32.WM_SYSCOMMAND, - SC_RESTORE, - 0, - ) - // WindowPos is used with HWND_TOPMOST to guarantee bring our app on top - // force set our main window on top - w32.SetWindowPos( - mainWindowHWND, - w32.HWND_TOPMOST, - 0, 0, 0, 0, - w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, - ) - // remove topmost to allow normal windows manipulations - w32.SetWindowPos( - mainWindowHWND, - w32.HWND_NOTOPMOST, - 0, 0, 0, 0, - w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, - ) - // put main window on tops foreground - w32.SetForegroundWindow(mainWindowHWND) - } } } From e2089c02b82a5baeac0d79d1a52a5c07b413e43f Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Wed, 27 Sep 2023 01:25:43 +0100 Subject: [PATCH 35/60] cleanup --- v2/go.mod | 2 -- v2/go.sum | 4 ---- 2 files changed, 6 deletions(-) diff --git a/v2/go.mod b/v2/go.mod index 3f47ecc2f6e..e1615159611 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -3,7 +3,6 @@ module github.com/wailsapp/wails/v2 go 1.18 require ( - github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 github.com/Masterminds/semver v1.5.0 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/bep/debounce v1.2.1 @@ -48,7 +47,6 @@ require ( atomicgo.dev/cursor v0.1.1 // indirect atomicgo.dev/keyboard v0.2.8 // indirect bitbucket.org/creachadair/shell v0.0.7 // indirect - github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect github.com/Microsoft/go-winio v0.4.16 // indirect github.com/alecthomas/chroma v0.10.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect diff --git a/v2/go.sum b/v2/go.sum index 270280aabe9..ab8255fafa7 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -4,10 +4,6 @@ atomicgo.dev/keyboard v0.2.8 h1:Di09BitwZgdTV1hPyX/b9Cqxi8HVuJQwWivnZUEqlj4= atomicgo.dev/keyboard v0.2.8/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= bitbucket.org/creachadair/shell v0.0.7 h1:Z96pB6DkSb7F3Y3BBnJeOZH2gazyMTWlvecSD4vDqfk= bitbucket.org/creachadair/shell v0.0.7/go.mod h1:oqtXSSvSYr4624lnnabXHaBsYW6RD80caLi2b3hJk0U= -github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk= -github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= -github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= From 9afd27a95942a984c521f39cf5fd221b2f1951c9 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Thu, 28 Sep 2023 22:06:48 +0100 Subject: [PATCH 36/60] implement single instance lock on mac os --- v2/internal/app/app_bindings.go | 2 - v2/internal/app/app_dev.go | 2 - v2/internal/app/app_production.go | 2 - .../frontend/desktop/darwin/AppDelegate.h | 6 ++ .../frontend/desktop/darwin/AppDelegate.m | 38 ++++++---- .../frontend/desktop/darwin/Application.h | 2 +- .../frontend/desktop/darwin/Application.m | 39 ++++++---- .../frontend/desktop/darwin/WailsContext.h | 3 + .../frontend/desktop/darwin/frontend.go | 46 ++++-------- .../desktop/darwin/single_instance.go | 75 +++++++++++++++++++ v2/internal/frontend/desktop/darwin/window.go | 11 ++- .../frontend/desktop/linux/frontend.go | 19 +---- .../frontend/desktop/linux/single_instance.go | 4 +- .../frontend/desktop/windows/frontend.go | 19 +---- .../windows/{winc => }/single_instance.go | 13 ++-- v2/pkg/options/mac/mac.go | 3 +- v2/pkg/options/options.go | 42 +++++++---- 17 files changed, 199 insertions(+), 127 deletions(-) create mode 100644 v2/internal/frontend/desktop/darwin/single_instance.go rename v2/internal/frontend/desktop/windows/{winc => }/single_instance.go (92%) diff --git a/v2/internal/app/app_bindings.go b/v2/internal/app/app_bindings.go index 5b94eafad39..d079790aa7f 100644 --- a/v2/internal/app/app_bindings.go +++ b/v2/internal/app/app_bindings.go @@ -24,8 +24,6 @@ func (a *App) Run() error { a.options.OnShutdown, a.options.OnDomReady, a.options.OnBeforeClose, - a.options.OnFileOpen, - a.options.OnFilesOpen, } // Check for CLI Flags diff --git a/v2/internal/app/app_dev.go b/v2/internal/app/app_dev.go index b5165d83b5c..51e3e63ba1c 100644 --- a/v2/internal/app/app_dev.go +++ b/v2/internal/app/app_dev.go @@ -208,8 +208,6 @@ func CreateApp(appoptions *options.App) (*App, error) { appoptions.OnShutdown, appoptions.OnDomReady, appoptions.OnBeforeClose, - appoptions.OnFileOpen, - appoptions.OnFilesOpen, } appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false) diff --git a/v2/internal/app/app_production.go b/v2/internal/app/app_production.go index efe09023657..96ed84e30c0 100644 --- a/v2/internal/app/app_production.go +++ b/v2/internal/app/app_production.go @@ -71,8 +71,6 @@ func CreateApp(appoptions *options.App) (*App, error) { appoptions.OnShutdown, appoptions.OnDomReady, appoptions.OnBeforeClose, - appoptions.OnFileOpen, - appoptions.OnFilesOpen, } appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated()) eventHandler := runtime.NewEvents(myLogger) diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.h b/v2/internal/frontend/desktop/darwin/AppDelegate.h index 06c8f35a69e..3203a34b332 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.h +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.h @@ -15,6 +15,8 @@ @property bool alwaysOnTop; @property bool startHidden; +@property (retain) NSString* singleInstanceUniqueId; +@property bool singleInstanceLockEnabled; @property bool startFullscreen; @property (retain) WailsWindow* mainWindow; @@ -24,4 +26,8 @@ extern void HandleOpenFile(char *); extern void HandleOpenFiles(char**, int); +extern void HandleSecondInstanceData(char * message); + +void SendDataToFirstInstance(char * singleInstanceUniqueId, char * text); + #endif /* AppDelegate_h */ diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.m b/v2/internal/frontend/desktop/darwin/AppDelegate.m index 507bfc9e70b..9b2cdafb4b8 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.m +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.m @@ -11,7 +11,6 @@ #import "AppDelegate.h" @implementation AppDelegate -// openFile implemented as well just in case, but it's not called (at least we don't know how to call it) -(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename { const char* utf8FileName = filename.UTF8String; @@ -19,21 +18,6 @@ -(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename return YES; } -// for some reasons it's triggered even when only one file is opened, instead of openFile. --(void)application:(NSApplication *)sender openFiles:(NSArray *)filenames -{ - int count = [filenames count]; - int i; - char **fileNamesArray = NULL; - fileNamesArray = (char**)realloc(fileNamesArray, i*sizeof(*fileNamesArray)); - for (i=0; i Date: Thu, 28 Sep 2023 22:20:03 +0100 Subject: [PATCH 37/60] implement proper show for windows --- .../desktop/windows/winc/controlbase.go | 18 ++++++++++- .../frontend/desktop/windows/winc/form.go | 30 ------------------- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/v2/internal/frontend/desktop/windows/winc/controlbase.go b/v2/internal/frontend/desktop/windows/winc/controlbase.go index b745cb1b01e..086609aedde 100644 --- a/v2/internal/frontend/desktop/windows/winc/controlbase.go +++ b/v2/internal/frontend/desktop/windows/winc/controlbase.go @@ -334,7 +334,23 @@ func (cba *ControlBase) ClientHeight() int { } func (cba *ControlBase) Show() { - w32.ShowWindow(cba.hwnd, w32.SW_SHOWDEFAULT) + // WindowPos is used with HWND_TOPMOST to guarantee bring our app on top + // force set our main window on top + w32.SetWindowPos( + cba.hwnd, + w32.HWND_TOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // remove topmost to allow normal windows manipulations + w32.SetWindowPos( + cba.hwnd, + w32.HWND_NOTOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // put main window on tops foreground + w32.SetForegroundWindow(cba.hwnd) } func (cba *ControlBase) Hide() { diff --git a/v2/internal/frontend/desktop/windows/winc/form.go b/v2/internal/frontend/desktop/windows/winc/form.go index 886495d26a8..9b9cadb2ce4 100644 --- a/v2/internal/frontend/desktop/windows/winc/form.go +++ b/v2/internal/frontend/desktop/windows/winc/form.go @@ -139,36 +139,6 @@ func (fm *Form) Restore() { w32.ShowWindow(fm.hwnd, w32.SW_RESTORE) } -func (fm *Form) UnMinimise() { - // SC_RESTORE param for WM_SYSCOMMAND to restore app if it is minimized - const SC_RESTORE = 0xF120 - - // restore the minimized window, if it is - w32.SendMessage( - fm.hwnd, - w32.WM_SYSCOMMAND, - SC_RESTORE, - 0, - ) - // WindowPos is used with HWND_TOPMOST to guarantee bring our app on top - // force set our main window on top - w32.SetWindowPos( - fm.hwnd, - w32.HWND_TOPMOST, - 0, 0, 0, 0, - w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, - ) - // remove topmost to allow normal windows manipulations - w32.SetWindowPos( - fm.hwnd, - w32.HWND_NOTOPMOST, - 0, 0, 0, 0, - w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, - ) - // put main window on tops foreground - w32.SetForegroundWindow(fm.hwnd) -} - // Public methods func (fm *Form) Center() { From 71fa5fa00d3ec4843609d8394b5fafb86380bc5c Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Thu, 28 Sep 2023 22:23:36 +0100 Subject: [PATCH 38/60] proper unmimimise --- v2/internal/frontend/desktop/windows/winc/form.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/v2/internal/frontend/desktop/windows/winc/form.go b/v2/internal/frontend/desktop/windows/winc/form.go index 9b9cadb2ce4..8a42d63f399 100644 --- a/v2/internal/frontend/desktop/windows/winc/form.go +++ b/v2/internal/frontend/desktop/windows/winc/form.go @@ -136,6 +136,15 @@ func (fm *Form) Minimise() { } func (fm *Form) Restore() { + // SC_RESTORE param for WM_SYSCOMMAND to restore app if it is minimized + const SC_RESTORE = 0xF120 + // restore the minimized window, if it is + w32.SendMessage( + fm.hwnd, + w32.WM_SYSCOMMAND, + SC_RESTORE, + 0, + ) w32.ShowWindow(fm.hwnd, w32.SW_RESTORE) } From faed07a52ffa23460246c033bdb3c98e8bfdb8a4 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Thu, 28 Sep 2023 22:49:39 +0100 Subject: [PATCH 39/60] get rid of openFiles mac os. change configuration structure --- v2/internal/app/app_bindings.go | 2 -- v2/internal/app/app_dev.go | 2 -- v2/internal/app/app_production.go | 2 -- .../frontend/desktop/darwin/AppDelegate.h | 2 -- .../frontend/desktop/darwin/AppDelegate.m | 16 --------- .../frontend/desktop/darwin/frontend.go | 36 ++----------------- v2/pkg/options/mac/mac.go | 3 +- v2/pkg/options/options.go | 2 -- website/docs/guides/file-association.mdx | 5 +-- 9 files changed, 7 insertions(+), 63 deletions(-) diff --git a/v2/internal/app/app_bindings.go b/v2/internal/app/app_bindings.go index 5b94eafad39..d079790aa7f 100644 --- a/v2/internal/app/app_bindings.go +++ b/v2/internal/app/app_bindings.go @@ -24,8 +24,6 @@ func (a *App) Run() error { a.options.OnShutdown, a.options.OnDomReady, a.options.OnBeforeClose, - a.options.OnFileOpen, - a.options.OnFilesOpen, } // Check for CLI Flags diff --git a/v2/internal/app/app_dev.go b/v2/internal/app/app_dev.go index b5165d83b5c..51e3e63ba1c 100644 --- a/v2/internal/app/app_dev.go +++ b/v2/internal/app/app_dev.go @@ -208,8 +208,6 @@ func CreateApp(appoptions *options.App) (*App, error) { appoptions.OnShutdown, appoptions.OnDomReady, appoptions.OnBeforeClose, - appoptions.OnFileOpen, - appoptions.OnFilesOpen, } appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false) diff --git a/v2/internal/app/app_production.go b/v2/internal/app/app_production.go index efe09023657..96ed84e30c0 100644 --- a/v2/internal/app/app_production.go +++ b/v2/internal/app/app_production.go @@ -71,8 +71,6 @@ func CreateApp(appoptions *options.App) (*App, error) { appoptions.OnShutdown, appoptions.OnDomReady, appoptions.OnBeforeClose, - appoptions.OnFileOpen, - appoptions.OnFilesOpen, } appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated()) eventHandler := runtime.NewEvents(myLogger) diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.h b/v2/internal/frontend/desktop/darwin/AppDelegate.h index 06c8f35a69e..7b533ad5ff7 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.h +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.h @@ -22,6 +22,4 @@ extern void HandleOpenFile(char *); -extern void HandleOpenFiles(char**, int); - #endif /* AppDelegate_h */ diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.m b/v2/internal/frontend/desktop/darwin/AppDelegate.m index 507bfc9e70b..6d613d86e6d 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.m +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.m @@ -11,7 +11,6 @@ #import "AppDelegate.h" @implementation AppDelegate -// openFile implemented as well just in case, but it's not called (at least we don't know how to call it) -(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename { const char* utf8FileName = filename.UTF8String; @@ -19,21 +18,6 @@ -(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename return YES; } -// for some reasons it's triggered even when only one file is opened, instead of openFile. --(void)application:(NSApplication *)sender openFiles:(NSArray *)filenames -{ - int count = [filenames count]; - int i; - char **fileNamesArray = NULL; - fileNamesArray = (char**)realloc(fileNamesArray, i*sizeof(*fileNamesArray)); - for (i=0; i Date: Thu, 28 Sep 2023 22:51:09 +0100 Subject: [PATCH 40/60] remove unused channel --- v2/internal/frontend/desktop/darwin/frontend.go | 1 - 1 file changed, 1 deletion(-) diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index 8f0b4ad7226..66e5dc0cf91 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -39,7 +39,6 @@ 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 openFilepathsBuffer = make(chan []string, 100) type Frontend struct { From 785b4328623afa90860a8f87108acdebfbfe8581 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Thu, 28 Sep 2023 22:57:05 +0100 Subject: [PATCH 41/60] remove unused function --- v2/internal/frontend/desktop/darwin/AppDelegate.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.h b/v2/internal/frontend/desktop/darwin/AppDelegate.h index 3203a34b332..4fa32233e8b 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.h +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.h @@ -24,8 +24,6 @@ extern void HandleOpenFile(char *); -extern void HandleOpenFiles(char**, int); - extern void HandleSecondInstanceData(char * message); void SendDataToFirstInstance(char * singleInstanceUniqueId, char * text); From 8a3fd43b3280c4aee5fc1e15f87738e8666b9ed6 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Thu, 28 Sep 2023 23:33:47 +0100 Subject: [PATCH 42/60] add documentation for single instance lock --- website/docs/guides/file-association.mdx | 52 ++++++++++++- website/docs/guides/single-instance-lock.mdx | 82 ++++++++++++++++++++ website/src/pages/changelog.mdx | 1 + 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 website/docs/guides/single-instance-lock.mdx diff --git a/website/docs/guides/file-association.mdx b/website/docs/guides/file-association.mdx index c7b8da5b742..601be559011 100644 --- a/website/docs/guides/file-association.mdx +++ b/website/docs/guides/file-association.mdx @@ -85,6 +85,31 @@ func main() { } ``` +You also can enable single instance lock for your app. In this case, when you open file 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", + Enabled: true, + 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. @@ -179,6 +204,27 @@ func main() { } ``` -## Limitations: -On Windows and Linux when associated file is opened, new instance of your app is launched. -Currently, Wails doesn't support opening files in already running app. There is plugin for single instance support for v3 in development. +You also can enable single instance lock for your app. In this case, when you open file 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", + Enabled: true, + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` diff --git a/website/docs/guides/single-instance-lock.mdx b/website/docs/guides/single-instance-lock.mdx new file mode 100644 index 00000000000..5a7d3d81f60 --- /dev/null +++ b/website/docs/guides/single-instance-lock.mdx @@ -0,0 +1,82 @@ +# Single Instance Lock + +Single instance lock is a mechanism that allows you to prevent multiple instances of your app from running at the same time. +It is useful for apps that are designed to open files from the command line or from the OS file explorer. + +## Important + +Single Instance Lock does not implement a secure communications protocol between instances. When using single instance lock, +your app should treat any data passed to it from second instance callback as untrusted. +You should verify that args that you receive are valid and don't contain any malicious data. + +## How it works + +Windows: Single instance lock is implemented using a named mutex. The mutex name is generated from the unique id that you provide. Data is passed to the first instance via a shared window using [SendMessage](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessage) +macOS: Single instance lock is implemented using a named mutex. The mutex name is generated from the unique id that you provide. Data is passed to the first instance via [NSDistributedNotificationCenter](https://developer.apple.com/documentation/foundation/nsdistributednotificationcenter) +Linux: Single instance lock is implemented using [dbus](https://www.freedesktop.org/wiki/Software/dbus/). The dbus name is generated from the unique id that you provide. Data is passed to the first instance via [dbus](https://www.freedesktop.org/wiki/Software/dbus/) + +## Usage +When creating your app, you can enable single instance lock by passing a `SingleInstanceLock` struct to the `App` struct. +Use the `UniqueId` field to specify a unique id for your app. +This id is used to generate the mutex name on Windows and macOS and the dbus name on Linux. Use a UUID to ensure that the id is unique. +The `Enabled` field is used to enable or disable single instance lock. +The `OnSecondInstanceLaunch` field is used to specify a callback that is called when a second instance of your app is launched. +The callback receives a `SecondInstanceData` struct that contains the command line arguments passed to the second instance and the working directory of the second instance. + +Note that OnSecondInstanceLaunch don't trigger windows focus. +You need to call `runtime.WindowUnminimise` and `runtime.Show` to bring your app to the front. +Note that on linux systems window managers may prevent your app from being brought to the front to avoid stealing focus. + +```go title="main.go" +var wailsContext *context.Context + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + wailsContext = &ctx +} + +func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) { + secondInstanceArgs = secondInstanceData.Args + + println("user opened second instance", strings.Join(secondInstanceData.Args, ",")) + println("user opened second from", secondInstanceData.WorkingDirectory) + runtime.WindowUnminimise(*wailsContext) + runtime.Show(*wailsContext) + go runtime.EventsEmit(*wailsContext, "launchArgs", secondInstanceArgs) +} + +func main() { + // Create an instance of the app structure + app := NewApp() + + // 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}, + OnStartup: app.startup, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + Enabled: true, + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} +``` diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index b9c54d1eccb..4743e164dcf 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added Single Instance Lock support with passing arguments to first instance. Added by @APshenkin in [PR]() - Added support for enabling/disabling swipe gestures for Windows WebView2. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2878) - When building with `-devtools` flag, CMD/CTRL+SHIFT+F12 can be used to open the devtools. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2915) – Added file association support for macOS and Windows. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/2918) From 954a42a5d636932f199d2854acdbc4f7df8660ee Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Thu, 28 Sep 2023 23:53:06 +0100 Subject: [PATCH 43/60] add PR link --- website/src/pages/changelog.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index 4743e164dcf..fe6dd63d5ea 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added Single Instance Lock support with passing arguments to first instance. Added by @APshenkin in [PR]() +- Added Single Instance Lock support with passing arguments to first instance. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/2951) - Added support for enabling/disabling swipe gestures for Windows WebView2. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2878) - When building with `-devtools` flag, CMD/CTRL+SHIFT+F12 can be used to open the devtools. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2915) – Added file association support for macOS and Windows. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/2918) From d7f3328039ea335cce3366ba725c83893dc65aca Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Fri, 29 Sep 2023 00:21:37 +0100 Subject: [PATCH 44/60] wip mac os deeplinks --- .../frontend/desktop/darwin/AppDelegate.h | 2 ++ .../frontend/desktop/darwin/AppDelegate.m | 10 ++++++++ .../frontend/desktop/darwin/frontend.go | 20 +++++++++++++++ v2/internal/project/project.go | 7 ++++++ .../buildassets/build/darwin/Info.dev.plist | 17 +++++++++++++ v2/pkg/buildassets/build/darwin/Info.plist | 17 +++++++++++++ v2/pkg/options/mac/mac.go | 1 + website/static/schemas/config.v2.json | 25 +++++++++++++++++++ 8 files changed, 99 insertions(+) diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.h b/v2/internal/frontend/desktop/darwin/AppDelegate.h index 4fa32233e8b..5907e87904b 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.h +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.h @@ -28,4 +28,6 @@ extern void HandleSecondInstanceData(char * message); void SendDataToFirstInstance(char * singleInstanceUniqueId, char * text); +extern void HandleOpenUrl(char*); + #endif /* AppDelegate_h */ diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.m b/v2/internal/frontend/desktop/darwin/AppDelegate.m index 9b2cdafb4b8..0ca8464209b 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.m +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.m @@ -41,6 +41,9 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [self.mainWindow toggleFullScreen:nil]; } + NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; + [appleEventManager setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID: kAEGetURL]; + if ( self.singleInstanceLockEnabled ) { [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleSecondInstanceNotification:) name:self.singleInstanceUniqueId object:nil]; @@ -64,6 +67,13 @@ - (void)handleSecondInstanceNotification:(NSNotification *)note; } } +- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + [event paramDescriptorForKeyword:keyDirectObject]; + NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + + HandleOpenUrl((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]); +} + - (void)dealloc { [super dealloc]; } diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index 8c06f883f32..ad74fe6b52f 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -39,6 +39,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, 100) type Frontend struct { @@ -110,6 +111,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 +123,12 @@ func (f *Frontend) startFileOpenProcessor() { } } +func (f *Frontend) startUrlOpenProcessor() { + for url := range openFilepathBuffer { + f.ProcessOpenUrlEvent(url) + } +} + func (f *Frontend) startSecondInstanceProcessor() { for secondInstanceData := range secondInstanceBuffer { if f.frontendOptions.SingleInstanceLock != nil && @@ -385,6 +393,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 +448,9 @@ func HandleOpenFile(filePath *C.char) { goFilepath := C.GoString(filePath) openFilepathBuffer <- goFilepath } + +//export HandleOpenUrl +func HandleOpenUrl(url *C.char) { + goUrl := C.GoString(url) + openUrlBuffer <- goUrl +} diff --git a/v2/internal/project/project.go b/v2/internal/project/project.go index a0bf518a9c9..7224e4c32bc 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"` + Name string `json:"name"` + 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 9d4096f4fb2..11a7dc1e718 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.{{.Name}} + 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 079bdafa962..ee046d1c19c 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.{{.Name}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} diff --git a/v2/pkg/options/mac/mac.go b/v2/pkg/options/mac/mac.go index ecd2923fbbc..5758befd011 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/static/schemas/config.v2.json b/website/static/schemas/config.v2.json index 7ab40d95fbd..3228030c1c1 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" + }, + "name": { + "type": "string", + "description": "The name. e.g. My App" + }, + "role": { + "description": "macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole.", + "allOf": [ + { + "$ref": "#/definitions/BundleTypeRole" + } + ] + } + } + } } } }, From bd3395f3639523e702dda6de9cc3170c97052239 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Fri, 29 Sep 2023 00:52:37 +0100 Subject: [PATCH 45/60] put custom url listner on top to catch link on app opening --- v2/internal/frontend/desktop/darwin/CustomUrl.h | 0 v2/internal/frontend/desktop/darwin/CustomUrl.m | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 v2/internal/frontend/desktop/darwin/CustomUrl.h create mode 100644 v2/internal/frontend/desktop/darwin/CustomUrl.m diff --git a/v2/internal/frontend/desktop/darwin/CustomUrl.h b/v2/internal/frontend/desktop/darwin/CustomUrl.h new file mode 100644 index 00000000000..e69de29bb2d diff --git a/v2/internal/frontend/desktop/darwin/CustomUrl.m b/v2/internal/frontend/desktop/darwin/CustomUrl.m new file mode 100644 index 00000000000..e69de29bb2d From 7cc4b0863cd1a1f7b26fa5730ebcb4bf95a310ae Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Fri, 29 Sep 2023 00:52:56 +0100 Subject: [PATCH 46/60] put custom url listner on top to catch link on app opening --- .../frontend/desktop/darwin/AppDelegate.h | 2 -- .../frontend/desktop/darwin/AppDelegate.m | 10 ---------- .../frontend/desktop/darwin/CustomUrl.h | 14 +++++++++++++ .../frontend/desktop/darwin/CustomUrl.m | 20 +++++++++++++++++++ .../frontend/desktop/darwin/frontend.go | 9 ++++++--- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.h b/v2/internal/frontend/desktop/darwin/AppDelegate.h index 5907e87904b..4fa32233e8b 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.h +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.h @@ -28,6 +28,4 @@ extern void HandleSecondInstanceData(char * message); void SendDataToFirstInstance(char * singleInstanceUniqueId, char * text); -extern void HandleOpenUrl(char*); - #endif /* AppDelegate_h */ diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.m b/v2/internal/frontend/desktop/darwin/AppDelegate.m index 0ca8464209b..9b2cdafb4b8 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.m +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.m @@ -41,9 +41,6 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [self.mainWindow toggleFullScreen:nil]; } - NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; - [appleEventManager setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID: kAEGetURL]; - if ( self.singleInstanceLockEnabled ) { [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleSecondInstanceNotification:) name:self.singleInstanceUniqueId object:nil]; @@ -67,13 +64,6 @@ - (void)handleSecondInstanceNotification:(NSNotification *)note; } } -- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { - [event paramDescriptorForKeyword:keyDirectObject]; - NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; - - HandleOpenUrl((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]); -} - - (void)dealloc { [super dealloc]; } diff --git a/v2/internal/frontend/desktop/darwin/CustomUrl.h b/v2/internal/frontend/desktop/darwin/CustomUrl.h index e69de29bb2d..0e6c879511f 100644 --- a/v2/internal/frontend/desktop/darwin/CustomUrl.h +++ b/v2/internal/frontend/desktop/darwin/CustomUrl.h @@ -0,0 +1,14 @@ +#ifndef CustomUrl_h +#define CustomUrl_h + +#import + +extern void HandleCustomURL(char*); + +@interface CustomUrlSchemeHandler : NSObject ++ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent; +@end + +void StartCustomURLHandler(void); + +#endif /* CustomUrl_h */ diff --git a/v2/internal/frontend/desktop/darwin/CustomUrl.m b/v2/internal/frontend/desktop/darwin/CustomUrl.m index e69de29bb2d..64515f59c1b 100644 --- a/v2/internal/frontend/desktop/darwin/CustomUrl.m +++ b/v2/internal/frontend/desktop/darwin/CustomUrl.m @@ -0,0 +1,20 @@ +#include "CustomUrl.h" + +@implementation CustomUrlSchemeHandler ++ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + [event paramDescriptorForKeyword:keyDirectObject]; + + NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + + HandleCustomURL((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]); +} +@end + +void StartCustomURLHandler(void) { + NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; + + [appleEventManager setEventHandler:[CustomUrlSchemeHandler 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 ad74fe6b52f..29a29876852 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 "CustomUrl.h" #import "WailsContext.h" #include @@ -80,6 +81,8 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. } result.startURL, _ = url.Parse(startURL) + C.StartCustomURLHandler() + if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil { result.startURL = _starturl } else { @@ -124,7 +127,7 @@ func (f *Frontend) startFileOpenProcessor() { } func (f *Frontend) startUrlOpenProcessor() { - for url := range openFilepathBuffer { + for url := range openUrlBuffer { f.ProcessOpenUrlEvent(url) } } @@ -449,8 +452,8 @@ func HandleOpenFile(filePath *C.char) { openFilepathBuffer <- goFilepath } -//export HandleOpenUrl -func HandleOpenUrl(url *C.char) { +//export HandleCustomURL +func HandleCustomURL(url *C.char) { goUrl := C.GoString(url) openUrlBuffer <- goUrl } From 98dedb5b18283b09744f8ef4ae6435ba710cfb9a Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Fri, 29 Sep 2023 01:08:42 +0100 Subject: [PATCH 47/60] try add custom url windows --- .../build/windows/installer/project.nsi | 2 ++ .../build/windows/installer/wails_tools.nsh | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/v2/pkg/buildassets/build/windows/installer/project.nsi b/v2/pkg/buildassets/build/windows/installer/project.nsi index f18f2b5dfb9..654ae2e4957 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 bc79fb395ac..64cc317a4c9 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_URL_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_URL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create file associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_URL_ASSOCIATE "{{.Scheme}}" "{{.Scheme}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + {{end}} +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_URL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend From 355de4f1c69d44709936b26401c083c781b38f8c Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Fri, 29 Sep 2023 01:46:30 +0100 Subject: [PATCH 48/60] adjust custom url --- v2/internal/project/project.go | 6 +++--- v2/pkg/buildassets/build/darwin/Info.dev.plist | 2 +- v2/pkg/buildassets/build/darwin/Info.plist | 2 +- v2/pkg/buildassets/build/windows/installer/wails_tools.nsh | 6 +++--- website/static/schemas/config.v2.json | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/v2/internal/project/project.go b/v2/internal/project/project.go index 7224e4c32bc..d42977fee51 100644 --- a/v2/internal/project/project.go +++ b/v2/internal/project/project.go @@ -234,9 +234,9 @@ type FileAssociation struct { } type Protocol struct { - Scheme string `json:"scheme"` - Name string `json:"name"` - Role string `json:"role"` + Scheme string `json:"scheme"` + Description string `json:"description"` + Role string `json:"role"` } type Bindings struct { diff --git a/v2/pkg/buildassets/build/darwin/Info.dev.plist b/v2/pkg/buildassets/build/darwin/Info.dev.plist index 11a7dc1e718..04727c23f95 100644 --- a/v2/pkg/buildassets/build/darwin/Info.dev.plist +++ b/v2/pkg/buildassets/build/darwin/Info.dev.plist @@ -48,7 +48,7 @@ {{range .Info.Protocols}} CFBundleURLName - com.wails.{{.Name}} + com.wails.{{.Scheme}} CFBundleURLSchemes {{.Scheme}} diff --git a/v2/pkg/buildassets/build/darwin/Info.plist b/v2/pkg/buildassets/build/darwin/Info.plist index ee046d1c19c..19cc9370cb8 100644 --- a/v2/pkg/buildassets/build/darwin/Info.plist +++ b/v2/pkg/buildassets/build/darwin/Info.plist @@ -48,7 +48,7 @@ {{range .Info.Protocols}} CFBundleURLName - com.wails.{{.Name}} + com.wails.{{.Scheme}} CFBundleURLSchemes {{.Scheme}} diff --git a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh index 64cc317a4c9..9f1dbd90907 100644 --- a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh +++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh @@ -234,15 +234,15 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" !macroend !macro wails.associateCustomProtocols - ; Create file associations + ; Create custom protocols associations {{range .Info.Protocols}} - !insertmacro CUSTOM_URL_ASSOCIATE "{{.Scheme}}" "{{.Scheme}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + !insertmacro CUSTOM_URL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" {{end}} !macroend !macro wails.unassociateCustomProtocols - ; Delete app associations + ; Delete app custom protocol associations {{range .Info.Protocols}} !insertmacro CUSTOM_URL_UNASSOCIATE "{{.Scheme}}" {{end}} diff --git a/website/static/schemas/config.v2.json b/website/static/schemas/config.v2.json index 3228030c1c1..f215415e641 100644 --- a/website/static/schemas/config.v2.json +++ b/website/static/schemas/config.v2.json @@ -211,9 +211,9 @@ "type": "string", "description": "protocol scheme. e.g. myapp" }, - "name": { + "description": { "type": "string", - "description": "The name. e.g. My App" + "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.", From 8657dd6c0ce9bbb4d4dc2d06a9e947511768d4a9 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Fri, 29 Sep 2023 02:37:05 +0100 Subject: [PATCH 49/60] add docs --- website/docs/guides/custom-uri-schemes.mdx | 191 +++++++++++++++++++++ website/src/pages/changelog.mdx | 3 +- 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 website/docs/guides/custom-uri-schemes.mdx diff --git a/website/docs/guides/custom-uri-schemes.mdx b/website/docs/guides/custom-uri-schemes.mdx new file mode 100644 index 00000000000..f6755122907 --- /dev/null +++ b/website/docs/guides/custom-uri-schemes.mdx @@ -0,0 +1,191 @@ +# Custom URL Schemes association + +File association feature allows you to associate specific file types with your app so that when users open those files, +your app is launched to handle them. This can be particularly useful for text editors, image viewers, or any application +that works with specific file formats. In this guide, we'll walk through the steps to implement file association in Wails app. + + +## Set Up Custom URL Schemes Association: +To set up file association, you need to modify your application's wails.json file. +In "info" section add a "fileAssociations" section specifying the file types your app should be associated with. + +For example: +```json +{ + "info": { + "protocols": [ + { + "scheme": "myapp", + "description": "My App Protocol", + "role": "Editor" + } + ] + } +} +``` + +| Property | Description | +|:------------|:--------------------------------------------------------------------------------------| +| scheme | Custom URL 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 file (or files) with your app, the system will launch your app and call the `OnUrlOpen` functions 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 URL 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", + Enabled: true, + 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` us 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 URL 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", + Enabled: true, + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index fe6dd63d5ea..ffd89e03c9e 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -16,11 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added Single Instance Lock support with passing arguments to first instance. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/2951) - Added support for enabling/disabling swipe gestures for Windows WebView2. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2878) - When building with `-devtools` flag, CMD/CTRL+SHIFT+F12 can be used to open the devtools. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2915) – Added file association support for macOS and Windows. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/2918) - Added support for setting some of the Webview preferences, `textInteractionEnabled` and `tabFocusesLinks` on Mac. Added by @fkhadra in [PR](https://github.com/wailsapp/wails/pull/2937) +- Added Single Instance Lock support with passing arguments to first instance. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/2951) +- Added custom URL Schemes associations support for macOS and Windows. Added by @APshenkin in [PR]() ### Changed From ec15949eb3b72f026d6cef86ac45c0bb13f66e15 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 11:52:56 +0100 Subject: [PATCH 50/60] merge master --- v2/pkg/options/options.go | 1 - 1 file changed, 1 deletion(-) diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go index 12745699d5d..1ecad7fb919 100644 --- a/v2/pkg/options/options.go +++ b/v2/pkg/options/options.go @@ -172,7 +172,6 @@ func MergeDefaults(appoptions *App) { type SingleInstanceLock struct { // uniqueId that will be used for setting up messaging between instances UniqueId string - Enabled bool OnSecondInstanceLaunch func(secondInstanceData SecondInstanceData) } From aeef1835a553c37966d5ae9aa6f5f51deac2fc4f Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 11:58:38 +0100 Subject: [PATCH 51/60] update documentation --- ...chemes.mdx => custom-protocol-schemes.mdx} | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) rename website/docs/guides/{custom-uri-schemes.mdx => custom-protocol-schemes.mdx} (83%) diff --git a/website/docs/guides/custom-uri-schemes.mdx b/website/docs/guides/custom-protocol-schemes.mdx similarity index 83% rename from website/docs/guides/custom-uri-schemes.mdx rename to website/docs/guides/custom-protocol-schemes.mdx index f6755122907..a4f2ff9a0ae 100644 --- a/website/docs/guides/custom-uri-schemes.mdx +++ b/website/docs/guides/custom-protocol-schemes.mdx @@ -1,13 +1,13 @@ -# Custom URL Schemes association +# Custom Protocol Scheme association -File association feature allows you to associate specific file types with your app so that when users open those files, -your app is launched to handle them. This can be particularly useful for text editors, image viewers, or any application -that works with specific file formats. In this guide, we'll walk through the steps to implement file association in Wails app. +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 URL Schemes Association: -To set up file association, you need to modify your application's wails.json file. -In "info" section add a "fileAssociations" section specifying the file types your app should be associated with. +## 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 @@ -26,14 +26,14 @@ For example: | Property | Description | |:------------|:--------------------------------------------------------------------------------------| -| scheme | Custom URL scheme. e.g. myapp | +| 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 file (or files) with your app, the system will launch your app and call the `OnUrlOpen` functions in your Wails app. Example: +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 @@ -61,7 +61,7 @@ func main() { ### Windows -On Windows Custom URL Schemes is supported only with NSIS installer. During installation, the installer will create a +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" @@ -89,7 +89,6 @@ func main() { BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, SingleInstanceLock: &options.SingleInstanceLock{ UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", - Enabled: true, OnSecondInstanceLaunch: app.onSecondInstanceLaunch, }, Bind: []interface{}{ @@ -152,7 +151,7 @@ scripts: ```sh nfpm pkg --packager deb --target . ``` -7. Now when your package is installed, your app will be associated with custom URL scheme. When you open url with your app, +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" @@ -180,7 +179,6 @@ func main() { BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, SingleInstanceLock: &options.SingleInstanceLock{ UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", - Enabled: true, OnSecondInstanceLaunch: app.onSecondInstanceLaunch, }, Bind: []interface{}{ From a6ac4e3f5198b5760652b79bf5235de9847afbe7 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 12:00:56 +0100 Subject: [PATCH 52/60] add comment for darwin --- v2/internal/frontend/desktop/darwin/frontend.go | 1 + 1 file changed, 1 insertion(+) diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index 59ab741b371..465b8bb640b 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -81,6 +81,7 @@ 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.StartCustomURLHandler() if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil { From 825e0a96171aefc181538dd5a1fcc79375f1709d Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 12:08:18 +0100 Subject: [PATCH 53/60] add PR link --- website/src/pages/changelog.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index 83bf1ece3ba..9f3da5bb434 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -27,7 +27,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 URL Schemes associations support for macOS and Windows. Added by @APshenkin in [PR]() +- Added Custom Protocol Schemes associations support for macOS and Windows. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/3000) ### Changed From 022ee4957e24dd95b301a799244bd754e83658c9 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 12:36:25 +0100 Subject: [PATCH 54/60] change naming --- .../frontend/desktop/darwin/CustomProtocol.h | 14 ++++++++++++++ .../darwin/{CustomUrl.m => CustomProtocol.m} | 6 +++--- v2/internal/frontend/desktop/darwin/CustomUrl.h | 14 -------------- v2/internal/frontend/desktop/darwin/frontend.go | 6 +++--- .../build/windows/installer/wails_tools.nsh | 8 ++++---- 5 files changed, 24 insertions(+), 24 deletions(-) create mode 100644 v2/internal/frontend/desktop/darwin/CustomProtocol.h rename v2/internal/frontend/desktop/darwin/{CustomUrl.m => CustomProtocol.m} (74%) delete mode 100644 v2/internal/frontend/desktop/darwin/CustomUrl.h diff --git a/v2/internal/frontend/desktop/darwin/CustomProtocol.h b/v2/internal/frontend/desktop/darwin/CustomProtocol.h new file mode 100644 index 00000000000..35d64e0296a --- /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 /* CustomUrl_h */ diff --git a/v2/internal/frontend/desktop/darwin/CustomUrl.m b/v2/internal/frontend/desktop/darwin/CustomProtocol.m similarity index 74% rename from v2/internal/frontend/desktop/darwin/CustomUrl.m rename to v2/internal/frontend/desktop/darwin/CustomProtocol.m index 64515f59c1b..42af0a9e350 100644 --- a/v2/internal/frontend/desktop/darwin/CustomUrl.m +++ b/v2/internal/frontend/desktop/darwin/CustomProtocol.m @@ -1,16 +1,16 @@ #include "CustomUrl.h" -@implementation CustomUrlSchemeHandler +@implementation CustomProtocolSchemeHandler + (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { [event paramDescriptorForKeyword:keyDirectObject]; NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; - HandleCustomURL((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]); + HandleCustomProtocol((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]); } @end -void StartCustomURLHandler(void) { +void StartCustomProtocolHandler(void) { NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; [appleEventManager setEventHandler:[CustomUrlSchemeHandler class] diff --git a/v2/internal/frontend/desktop/darwin/CustomUrl.h b/v2/internal/frontend/desktop/darwin/CustomUrl.h deleted file mode 100644 index 0e6c879511f..00000000000 --- a/v2/internal/frontend/desktop/darwin/CustomUrl.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef CustomUrl_h -#define CustomUrl_h - -#import - -extern void HandleCustomURL(char*); - -@interface CustomUrlSchemeHandler : NSObject -+ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent; -@end - -void StartCustomURLHandler(void); - -#endif /* CustomUrl_h */ diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index 465b8bb640b..cbdbd211739 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -8,7 +8,7 @@ package darwin #cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit #import #import "Application.h" -#import "CustomUrl.h" +#import "CustomProtocol.h" #import "WailsContext.h" #include @@ -453,8 +453,8 @@ func HandleOpenFile(filePath *C.char) { openFilepathBuffer <- goFilepath } -//export HandleCustomURL -func HandleCustomURL(url *C.char) { +//export HandleCustomProtocol +func HandleCustomProtocol(url *C.char) { goUrl := C.GoString(url) openUrlBuffer <- goUrl } diff --git a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh index 9f1dbd90907..f9c0f885225 100644 --- a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh +++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh @@ -219,7 +219,7 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" {{end}} !macroend -!macro CUSTOM_URL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND +!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" "" @@ -229,14 +229,14 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" !macroend -!macro CUSTOM_URL_UNASSOCIATE PROTOCOL +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" !macroend !macro wails.associateCustomProtocols ; Create custom protocols associations {{range .Info.Protocols}} - !insertmacro CUSTOM_URL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" {{end}} !macroend @@ -244,6 +244,6 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" !macro wails.unassociateCustomProtocols ; Delete app custom protocol associations {{range .Info.Protocols}} - !insertmacro CUSTOM_URL_UNASSOCIATE "{{.Scheme}}" + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" {{end}} !macroend From e6fe53fd93f9b2506519877c165568a4f7aea46a Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 12:38:34 +0100 Subject: [PATCH 55/60] change naming --- v2/internal/frontend/desktop/darwin/frontend.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index cbdbd211739..c792ae9c88e 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -82,7 +82,7 @@ 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.StartCustomURLHandler() + C.StartCustomProtocolHandler() if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil { result.startURL = _starturl From fa3b5673887e7f9112f0959614276c5de04a5144 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 12:39:10 +0100 Subject: [PATCH 56/60] change naming --- v2/internal/frontend/desktop/darwin/CustomProtocol.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/internal/frontend/desktop/darwin/CustomProtocol.m b/v2/internal/frontend/desktop/darwin/CustomProtocol.m index 42af0a9e350..3740a68ed57 100644 --- a/v2/internal/frontend/desktop/darwin/CustomProtocol.m +++ b/v2/internal/frontend/desktop/darwin/CustomProtocol.m @@ -1,4 +1,4 @@ -#include "CustomUrl.h" +#include "CustomProtocol.h" @implementation CustomProtocolSchemeHandler + (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { From 8365d68403c3c54c21f75d1f6bf519d3dd49c209 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 12:40:32 +0100 Subject: [PATCH 57/60] change naming --- v2/internal/frontend/desktop/darwin/CustomProtocol.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/internal/frontend/desktop/darwin/CustomProtocol.m b/v2/internal/frontend/desktop/darwin/CustomProtocol.m index 3740a68ed57..7365e4f5042 100644 --- a/v2/internal/frontend/desktop/darwin/CustomProtocol.m +++ b/v2/internal/frontend/desktop/darwin/CustomProtocol.m @@ -13,7 +13,7 @@ + (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppl void StartCustomProtocolHandler(void) { NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; - [appleEventManager setEventHandler:[CustomUrlSchemeHandler class] + [appleEventManager setEventHandler:[CustomProtocolSchemeHandler class] andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID: kAEGetURL]; From e6c716bc3882d77e94a60b0ed42f0bfcd9cd3782 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 12:42:20 +0100 Subject: [PATCH 58/60] fix formatting --- v2/internal/frontend/desktop/darwin/window.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/internal/frontend/desktop/darwin/window.go b/v2/internal/frontend/desktop/darwin/window.go index dbe619a03be..740848df144 100644 --- a/v2/internal/frontend/desktop/darwin/window.go +++ b/v2/internal/frontend/desktop/darwin/window.go @@ -118,7 +118,7 @@ func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled, windowStartState, startsHidden, minWidth, minHeight, maxWidth, maxHeight, enableFraudulentWebsiteWarnings, preferences, singleInstanceEnabled, singleInstanceUniqueId, - ) + ) // Create menu result := &Window{ From 1a064a21abb6aa655efe5a3c061aa2f1972c8c40 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 12:49:06 +0100 Subject: [PATCH 59/60] fix naming --- v2/internal/frontend/desktop/darwin/CustomProtocol.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/internal/frontend/desktop/darwin/CustomProtocol.h b/v2/internal/frontend/desktop/darwin/CustomProtocol.h index 35d64e0296a..da0e7079f1d 100644 --- a/v2/internal/frontend/desktop/darwin/CustomProtocol.h +++ b/v2/internal/frontend/desktop/darwin/CustomProtocol.h @@ -11,4 +11,4 @@ extern void HandleCustomProtocol(char*); void StartCustomProtocolHandler(void); -#endif /* CustomUrl_h */ +#endif /* CustomProtocol_h */ From 0cb0cfe6d3dca76ea3f09ac0263f40d255fe3fa1 Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Mon, 23 Oct 2023 13:25:49 +0100 Subject: [PATCH 60/60] Fix typo --- website/docs/guides/custom-protocol-schemes.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/guides/custom-protocol-schemes.mdx b/website/docs/guides/custom-protocol-schemes.mdx index a4f2ff9a0ae..c56634f0eb0 100644 --- a/website/docs/guides/custom-protocol-schemes.mdx +++ b/website/docs/guides/custom-protocol-schemes.mdx @@ -103,7 +103,7 @@ Currently, Wails doesn't support bundling for Linux. So, you need to create file 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` us important in Exec). Example: +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