diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx
index b40a4aecde7..957b3a8536d 100644
--- a/docs/src/content/docs/changelog.mdx
+++ b/docs/src/content/docs/changelog.mdx
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New `wails3 generate webview2bootstrapper` command by [@leaanthony](https://github.com/leaanthony)
- Added `init()` method in runtime to allow manual initialisation of the runtime by [@leaanthony](https://github.com/leaanthony)
- Added `WindowDidMoveDebounceMS` option to Window's WindowOptions by [@leaanthony](https://github.com/leaanthony)
+- Added Single Instance feature by [@leaanthony](https://github.com/leaanthony). Based on the [v2 PR](https://github.com/wailsapp/wails/pull/2951) by @APshenkin.
### Fixed
diff --git a/docs/src/content/docs/guides/cli.mdx b/docs/src/content/docs/guides/cli.mdx
index c0b82f4c25f..bc7b2845c95 100644
--- a/docs/src/content/docs/guides/cli.mdx
+++ b/docs/src/content/docs/guides/cli.mdx
@@ -5,8 +5,6 @@ sidebar:
order: 1
---
-import { Tabs, TabItem } from "@astrojs/starlight/components";
-
The Wails CLI provides a comprehensive set of commands to help you develop, build, and maintain your Wails applications.
## Core Commands
@@ -133,21 +131,21 @@ wails3 generate bindings [flags] [patterns...]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-f` | Additional Go build flags | |
-| `-d` | Output directory | `frontend/bindings` |
-| `-models` | Models filename | `models` |
-| `-internal` | Internal filename | `internal` |
-| `-index` | Index filename | `index` |
-| `-ts` | Generate TypeScript | `false` |
-| `-i` | Use TS interfaces | `false` |
-| `-b` | Use bundled runtime | `false` |
-| `-names` | Use names instead of IDs | `false` |
-| `-noindex` | Skip index files | `false` |
-| `-dry` | Dry run | `false` |
-| `-silent` | Silent mode | `false` |
-| `-v` | Debug output | `false` |
+| Flag | Description | Default |
+|-------------|---------------------------|---------------------|
+| `-f` | Additional Go build flags | |
+| `-d` | Output directory | `frontend/bindings` |
+| `-models` | Models filename | `models` |
+| `-internal` | Internal filename | `internal` |
+| `-index` | Index filename | `index` |
+| `-ts` | Generate TypeScript | `false` |
+| `-i` | Use TS interfaces | `false` |
+| `-b` | Use bundled runtime | `false` |
+| `-names` | Use names instead of IDs | `false` |
+| `-noindex` | Skip index files | `false` |
+| `-dry` | Dry run | `false` |
+| `-silent` | Silent mode | `false` |
+| `-v` | Debug output | `false` |
### `generate build-assets`
Generates build assets for your application.
@@ -157,18 +155,18 @@ wails3 generate build-assets [flags]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-name` | Project name | |
-| `-dir` | Output directory | `build` |
-| `-silent` | Suppress output | `false` |
-| `-company` | Company name | |
-| `-productname` | Product name | |
-| `-description` | Product description | |
-| `-version` | Product version | |
-| `-identifier` | Product identifier | `com.wails.[name]` |
-| `-copyright` | Copyright notice | |
-| `-comments` | File comments | |
+| Flag | Description | Default |
+|----------------|---------------------|--------------------|
+| `-name` | Project name | |
+| `-dir` | Output directory | `build` |
+| `-silent` | Suppress output | `false` |
+| `-company` | Company name | |
+| `-productname` | Product name | |
+| `-description` | Product description | |
+| `-version` | Product version | |
+| `-identifier` | Product identifier | `com.wails.[name]` |
+| `-copyright` | Copyright notice | |
+| `-comments` | File comments | |
### `generate icons`
Generates application icons.
@@ -178,13 +176,13 @@ wails3 generate icons [flags]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-input` | Input PNG file | Required |
-| `-windows` | Windows output filename | |
-| `-mac` | macOS output filename | |
-| `-sizes` | Icon sizes (comma-separated) | `256,128,64,48,32,16` |
-| `-example` | Generate example icon | `false` |
+| Flag | Description | Default |
+|------------|------------------------------|-----------------------|
+| `-input` | Input PNG file | Required |
+| `-windows` | Windows output filename | |
+| `-mac` | macOS output filename | |
+| `-sizes` | Icon sizes (comma-separated) | `256,128,64,48,32,16` |
+| `-example` | Generate example icon | `false` |
### `generate syso`
Generates Windows .syso file.
@@ -194,13 +192,13 @@ wails3 generate syso [flags]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-manifest` | Path to manifest file | Required |
-| `-icon` | Path to icon file | Required |
-| `-info` | Path to version info file | |
-| `-arch` | Target architecture | Current GOARCH |
-| `-out` | Output filename | `rsrc_windows_[arch].syso` |
+| Flag | Description | Default |
+|-------------|---------------------------|----------------------------|
+| `-manifest` | Path to manifest file | Required |
+| `-icon` | Path to icon file | Required |
+| `-info` | Path to version info file | |
+| `-arch` | Target architecture | Current GOARCH |
+| `-out` | Output filename | `rsrc_windows_[arch].syso` |
### `generate .desktop`
Generates a Linux .desktop file.
@@ -210,20 +208,20 @@ wails3 generate .desktop [flags]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-name` | Application name | Required |
-| `-exec` | Executable path | Required |
-| `-icon` | Icon path | |
-| `-categories` | Application categories | `Utility` |
-| `-comment` | Application comment | |
-| `-terminal` | Run in terminal | `false` |
-| `-keywords` | Search keywords | |
-| `-version` | Application version | |
-| `-genericname` | Generic name | |
-| `-startupnotify` | Show startup notification | `false` |
-| `-mimetype` | Supported MIME types | |
-| `-output` | Output filename | `[name].desktop` |
+| Flag | Description | Default |
+|------------------|---------------------------|------------------|
+| `-name` | Application name | Required |
+| `-exec` | Executable path | Required |
+| `-icon` | Icon path | |
+| `-categories` | Application categories | `Utility` |
+| `-comment` | Application comment | |
+| `-terminal` | Run in terminal | `false` |
+| `-keywords` | Search keywords | |
+| `-version` | Application version | |
+| `-genericname` | Generic name | |
+| `-startupnotify` | Show startup notification | `false` |
+| `-mimetype` | Supported MIME types | |
+| `-output` | Output filename | `[name].desktop` |
### `generate runtime`
Generates the pre-built version of the runtime.
@@ -247,13 +245,13 @@ wails3 generate appimage [flags]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-binary` | Path to binary | Required |
-| `-icon` | Path to icon file | Required |
-| `-desktop` | Path to .desktop file | Required |
-| `-builddir` | Build directory | Temp directory |
-| `-output` | Output directory | `.` |
+| Flag | Description | Default |
+|-------------|-----------------------|----------------|
+| `-binary` | Path to binary | Required |
+| `-icon` | Path to icon file | Required |
+| `-desktop` | Path to .desktop file | Required |
+| `-builddir` | Build directory | Temp directory |
+| `-output` | Output directory | `.` |
Base command: `wails3 service`
@@ -269,18 +267,18 @@ wails3 service init [flags]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-n` | Service name | `example_service` |
+| Flag | Description | Default |
+|------|---------------------|-------------------|
+| `-n` | Service name | `example_service` |
| `-d` | Service description | `Example service` |
-| `-p` | Package name | |
-| `-o` | Output directory | `.` |
-| `-q` | Suppress output | `false` |
-| `-a` | Author name | |
-| `-v` | Version | |
-| `-w` | Website URL | |
-| `-r` | Repository URL | |
-| `-l` | License | |
+| `-p` | Package name | |
+| `-o` | Output directory | `.` |
+| `-q` | Suppress output | `false` |
+| `-a` | Author name | |
+| `-v` | Version | |
+| `-w` | Website URL | |
+| `-r` | Repository URL | |
+| `-l` | License | |
Base command: `wails3 tool`
@@ -296,9 +294,9 @@ wails3 tool checkport [flags]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-port` | Port to check | `9245` |
+| Flag | Description | Default |
+|---------|---------------|-------------|
+| `-port` | Port to check | `9245` |
| `-host` | Host to check | `localhost` |
### `tool watcher`
@@ -309,11 +307,11 @@ wails3 tool watcher [flags]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-config` | Config file path | `./build/config.yml` |
-| `-ignore` | Patterns to ignore | |
-| `-include` | Patterns to include | |
+| Flag | Description | Default |
+|------------|---------------------|----------------------|
+| `-config` | Config file path | `./build/config.yml` |
+| `-ignore` | Patterns to ignore | |
+| `-include` | Patterns to include | |
### `tool cp`
Copies files.
@@ -337,20 +335,20 @@ wails3 tool package [flags]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-format` | Package format (deb, rpm, archlinux) | `deb` |
-| `-name` | Executable name | Required |
-| `-config` | Config file path | Required |
-| `-out` | Output directory | `.` |
+| Flag | Description | Default |
+|-----------|--------------------------------------|----------|
+| `-format` | Package format (deb, rpm, archlinux) | `deb` |
+| `-name` | Executable name | Required |
+| `-config` | Config file path | Required |
+| `-out` | Output directory | `.` |
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-format` | Package format (deb, rpm, archlinux) | `deb` |
-| `-name` | Executable name | `myapp` |
-| `-config` | Config file path | |
-| `-out` | Output directory | `.` |
+| Flag | Description | Default |
+|-----------|--------------------------------------|---------|
+| `-format` | Package format (deb, rpm, archlinux) | `deb` |
+| `-name` | Executable name | `myapp` |
+| `-config` | Config file path | |
+| `-out` | Output directory | `.` |
Base command: `wails3 update`
@@ -366,18 +364,18 @@ wails3 update build-assets [flags]
```
#### Flags
-| Flag | Description | Default |
-|------|-------------|---------|
-| `-config` | Config file path | |
-| `-dir` | Output directory | `build` |
-| `-silent` | Suppress output | `false` |
-| `-company` | Company name | |
-| `-productname` | Product name | |
-| `-description` | Product description | |
-| `-version` | Product version | |
-| `-identifier` | Product identifier | |
-| `-copyright` | Copyright notice | |
-| `-comments` | File comments | |
+| Flag | Description | Default |
+|----------------|---------------------|---------|
+| `-config` | Config file path | |
+| `-dir` | Output directory | `build` |
+| `-silent` | Suppress output | `false` |
+| `-company` | Company name | |
+| `-productname` | Product name | |
+| `-description` | Product description | |
+| `-version` | Product version | |
+| `-identifier` | Product identifier | |
+| `-copyright` | Copyright notice | |
+| `-comments` | File comments | |
Base command: `wails3`
diff --git a/docs/src/content/docs/guides/single-instance.mdx b/docs/src/content/docs/guides/single-instance.mdx
new file mode 100644
index 00000000000..25b7fac47ce
--- /dev/null
+++ b/docs/src/content/docs/guides/single-instance.mdx
@@ -0,0 +1,119 @@
+---
+title: Single Instance
+description: Limiting your app to a single running instance
+sidebar:
+ order: 40
+---
+
+import { Tabs, TabItem } from "@astrojs/starlight/components";
+
+Single instance locking is a mechanism that prevents 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.
+
+
+## Usage
+
+To enable single instance functionality in your app, provide a `SingleInstanceOptions` struct when creating your application:
+
+```go
+app := application.New(application.Options{
+ // ... other options ...
+ SingleInstance: &application.SingleInstanceOptions{
+ UniqueID: "com.myapp.unique-id",
+ OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
+ log.Printf("Second instance launched with args: %v", data.Args)
+ log.Printf("Working directory: %s", data.WorkingDir)
+ log.Printf("Additional data: %v", data.AdditionalData)
+ },
+ // Optional: Pass additional data to second instance
+ AdditionalData: map[string]string{
+ "launchtime": time.Now().String(),
+ },
+ },
+})
+```
+
+The `SingleInstanceOptions` struct has the following fields:
+
+- `UniqueID`: A unique identifier for your application. This should be a unique string, typically in reverse domain notation (e.g., "com.company.appname").
+- `EncryptionKey`: Optional 32-byte array for encrypting data passed between instances using AES-256-GCM. If provided as a non-zero array, all communication between instances will be encrypted.
+- `OnSecondInstanceLaunch`: A callback function that is called when a second instance of your app is launched. The callback receives a `SecondInstanceData` struct containing:
+ - `Args`: The command line arguments passed to the second instance
+ - `WorkingDir`: The working directory of the second instance
+ - `AdditionalData`: Any additional data passed from the second instance (if provided)
+- `AdditionalData`: Optional map of string key-value pairs that will be passed to the first instance when subsequent instances are launched
+
+:::danger[Warning]
+The Single Instance feature implements an optional encryption protocol using AES-256-GCM. Without encryption enabled,
+data passed between instances is not secure. When using the single instance feature without encryption,
+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.
+:::
+
+### Secure Communication
+
+To enable secure communication between instances, provide a 32-byte encryption key. This key must be the same for all instances of your application:
+
+```go
+// Define your encryption key (must be exactly 32 bytes)
+var encryptionKey = [32]byte{
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+}
+
+// Use the key in SingleInstanceOptions
+SingleInstance: &application.SingleInstanceOptions{
+ UniqueID: "com.myapp.unique-id",
+ // Enable encryption for instance communication
+ EncryptionKey: encryptionKey,
+ // ... other options ...
+}
+```
+
+:::tip[Security Best Practices]
+- Use a unique key for your application
+- Store the key securely if loading it from configuration
+- Do not use the example key shown above - create your own!
+:::
+
+### Window Management
+
+When handling second instance launches, you'll often want to bring your application window to the front. You can do this using the window's `Focus()` method. If your window is minimized, you may need to restore it first:
+
+```go
+
+ var mainWindow *application.WebviewWindow
+
+ SingleInstance: &application.SingleInstanceOptions{
+ // Other options...
+ OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
+ // Focus the window if needed
+ if mainWindow != nil {
+ mainWindow.Restore()
+ mainWindow.Focus()
+ }
+ },
+ }
+```
+
+## How it works
+
+
+
+
+ Single instance lock 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)
+
+
+
+
+ Single instance lock 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)
+
+
+
+
+ Single instance lock 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/)
+
+
+
diff --git a/v3/examples/single-instance/README.md b/v3/examples/single-instance/README.md
new file mode 100644
index 00000000000..5982cb90935
--- /dev/null
+++ b/v3/examples/single-instance/README.md
@@ -0,0 +1,62 @@
+# Single Instance Example
+
+This example demonstrates the single instance functionality in Wails v3. It shows how to:
+
+1. Ensure only one instance of your application can run at a time
+2. Notify the first instance when a second instance is launched
+3. Pass data between instances
+4. Handle command line arguments and working directory information from second instances
+
+## Running the Example
+
+1. Build and run the application:
+ ```bash
+ go build
+ ./single-instance
+ ```
+
+2. Try launching a second instance of the application. You'll notice:
+ - The second instance will exit immediately
+ - The first instance will receive and display:
+ - Command line arguments from the second instance
+ - Working directory of the second instance
+ - Additional data passed from the second instance
+
+3. Check the application logs to see the information received from second instances.
+
+## Features Demonstrated
+
+- Setting up single instance lock with a unique identifier
+- Handling second instance launches through callbacks
+- Passing custom data between instances
+- Displaying instance information in a web UI
+- Cross-platform support (Windows, macOS, Linux)
+
+## Code Overview
+
+The example consists of:
+
+- `main.go`: The main application code demonstrating single instance setup
+- A simple web UI showing current instance information
+- Callback handling for second instance launches
+
+## Implementation Details
+
+The application uses the Wails v3 single instance feature:
+
+```go
+app := application.New(&application.Options{
+ SingleInstance: &application.SingleInstanceOptions{
+ UniqueID: "com.wails.example.single-instance",
+ OnSecondInstance: func(data application.SecondInstanceData) {
+ // Handle second instance launch
+ },
+ AdditionalData: map[string]string{
+ },
+ },
+})
+```
+
+The implementation uses platform-specific mechanisms:
+- Windows: Named mutex and window messages
+- Unix (Linux/macOS): File locking with flock and signals
diff --git a/v3/examples/single-instance/assets/index.html b/v3/examples/single-instance/assets/index.html
new file mode 100644
index 00000000000..330b062ecd4
--- /dev/null
+++ b/v3/examples/single-instance/assets/index.html
@@ -0,0 +1,141 @@
+
+
+
+
+
+ Single Instance Demo
+
+
+
+
+
+
Single Instance Demo
+
+
+
Current Instance Information
+
Loading...
+
+
+
+
Instructions
+
Try launching another instance of this application. The first instance will:
+
+ - Receive notification of the second instance launch
+ - Get the command line arguments of the second instance
+ - Get the working directory of the second instance
+ - Receive any additional data passed from the second instance
+
+
Check the application logs to see the information received from second instances.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v3/examples/single-instance/main.go b/v3/examples/single-instance/main.go
new file mode 100644
index 00000000000..e0658ae8f45
--- /dev/null
+++ b/v3/examples/single-instance/main.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "embed"
+ "log"
+ "log/slog"
+ "os"
+ "time"
+
+ "github.com/wailsapp/wails/v3/pkg/application"
+)
+
+//go:embed assets/index.html
+var assets embed.FS
+
+type App struct{}
+
+func (a *App) GetCurrentInstanceInfo() map[string]interface{} {
+ return map[string]interface{}{
+ "args": os.Args,
+ "workingDir": getCurrentWorkingDir(),
+ }
+}
+
+var encryptionKey = [32]byte{
+ 0x1e, 0x1f, 0x1c, 0x1d, 0x1a, 0x1b, 0x18, 0x19,
+ 0x16, 0x17, 0x14, 0x15, 0x12, 0x13, 0x10, 0x11,
+ 0x0e, 0x0f, 0x0c, 0x0d, 0x0a, 0x0b, 0x08, 0x09,
+ 0x06, 0x07, 0x04, 0x05, 0x02, 0x03, 0x00, 0x01,
+}
+
+func main() {
+
+ var window *application.WebviewWindow
+ app := application.New(application.Options{
+ Name: "Single Instance Example",
+ LogLevel: slog.LevelDebug,
+ Description: "An example of single instance functionality in Wails v3",
+ Services: []application.Service{
+ application.NewService(&App{}),
+ },
+ SingleInstance: &application.SingleInstanceOptions{
+ UniqueID: "com.wails.example.single-instance",
+ EncryptionKey: encryptionKey,
+ OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
+ if window != nil {
+ window.EmitEvent("secondInstanceLaunched", data)
+ window.Restore()
+ window.Focus()
+ }
+ log.Printf("Second instance launched with args: %v\n", data.Args)
+ log.Printf("Working directory: %s\n", data.WorkingDir)
+ if data.AdditionalData != nil {
+ log.Printf("Additional data: %v\n", data.AdditionalData)
+ }
+ },
+ AdditionalData: map[string]string{
+ "launchtime": time.Now().Local().String(),
+ },
+ },
+ Assets: application.AssetOptions{
+ Handler: application.BundledAssetFileServer(assets),
+ },
+ })
+
+ window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
+ Title: "Single Instance Demo",
+ Width: 800,
+ Height: 700,
+ URL: "/",
+ })
+
+ app.Run()
+}
+
+func getCurrentWorkingDir() string {
+ dir, err := os.Getwd()
+ if err != nil {
+ return ""
+ }
+ return dir
+}
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/package.json b/v3/internal/runtime/desktop/@wailsio/runtime/package.json
index 4c909126bd2..28325672e57 100644
--- a/v3/internal/runtime/desktop/@wailsio/runtime/package.json
+++ b/v3/internal/runtime/desktop/@wailsio/runtime/package.json
@@ -1,7 +1,7 @@
{
"name": "@wailsio/runtime",
"type": "module",
- "version": "3.0.0-alpha.35",
+ "version": "3.0.0-alpha.36",
"description": "Wails Runtime",
"types": "types/index.d.ts",
"exports": {
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.js
index eea9aed8b7a..a663c60ed08 100644
--- a/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.js
+++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.js
@@ -174,6 +174,8 @@ export const EventTypes = {
WindowFileDraggingEntered: "mac:WindowFileDraggingEntered",
WindowFileDraggingPerformed: "mac:WindowFileDraggingPerformed",
WindowFileDraggingExited: "mac:WindowFileDraggingExited",
+ WindowShow: "mac:WindowShow",
+ WindowHide: "mac:WindowHide",
},
Linux: {
SystemThemeChanged: "linux:SystemThemeChanged",
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/event_types.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/event_types.d.ts
index 9c90df3d4ea..fbae10e1048 100644
--- a/v3/internal/runtime/desktop/@wailsio/runtime/types/event_types.d.ts
+++ b/v3/internal/runtime/desktop/@wailsio/runtime/types/event_types.d.ts
@@ -174,6 +174,8 @@ export declare const EventTypes: {
WindowFileDraggingEntered: string,
WindowFileDraggingPerformed: string,
WindowFileDraggingExited: string,
+ WindowShow: string,
+ WindowHide: string,
},
Linux: {
SystemThemeChanged: string,
diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go
index a68c589f7e2..7b5298fe7e8 100644
--- a/v3/pkg/application/application.go
+++ b/v3/pkg/application/application.go
@@ -4,6 +4,7 @@ import (
"context"
"embed"
"encoding/json"
+ "errors"
"fmt"
"io"
"log"
@@ -174,6 +175,23 @@ func New(appOptions Options) *App {
result.OnShutdown(appOptions.OnShutdown)
}
+ // Initialize single instance manager if enabled
+ if appOptions.SingleInstance != nil {
+ manager, err := newSingleInstanceManager(result, appOptions.SingleInstance)
+ if err != nil {
+ if errors.Is(err, alreadyRunningError) && manager != nil {
+ err = manager.notifyFirstInstance()
+ if err != nil {
+ globalApplication.error("Failed to notify first instance: " + err.Error())
+ }
+ os.Exit(appOptions.SingleInstance.ExitCode)
+ }
+ result.handleFatalError(fmt.Errorf("failed to initialize single instance manager: %w", err))
+ } else {
+ result.singleInstanceManager = manager
+ }
+ }
+
return result
}
@@ -357,6 +375,9 @@ type App struct {
// Wails ApplicationEvent Listener related
wailsEventListenerLock sync.Mutex
wailsEventListeners []WailsEventListener
+
+ // singleInstanceManager handles single instance functionality
+ singleInstanceManager *singleInstanceManager
}
func (a *App) handleWarning(msg string) {
@@ -792,6 +813,10 @@ func (a *App) cleanup() {
a.systemTrays = nil
a.systemTraysLock.Unlock()
})
+ // Cleanup single instance manager
+ if a.singleInstanceManager != nil {
+ a.singleInstanceManager.cleanup()
+ }
}
func (a *App) Quit() {
diff --git a/v3/pkg/application/application_darwin.go b/v3/pkg/application/application_darwin.go
index ecdfa5f3062..4a88b9a25cf 100644
--- a/v3/pkg/application/application_darwin.go
+++ b/v3/pkg/application/application_darwin.go
@@ -15,6 +15,7 @@ package application
extern void registerListener(unsigned int event);
#import
+#import
static AppDelegate *appDelegate = nil;
@@ -157,6 +158,13 @@ static const char* serializationNSDictionary(void *dict) {
return nil;
}
+
+static void startSingleInstanceListener(const char *uniqueID) {
+ // Convert to NSString
+ NSString *uid = [NSString stringWithUTF8String:uniqueID];
+ [[NSDistributedNotificationCenter defaultCenter] addObserver:appDelegate
+ selector:@selector(handleSecondInstanceNotification:) name:uid object:nil];
+}
*/
import "C"
import (
@@ -221,6 +229,11 @@ func (m *macosApp) setApplicationMenu(menu *Menu) {
}
func (m *macosApp) run() error {
+ if m.parent.options.SingleInstance != nil {
+ cUniqueID := C.CString(m.parent.options.SingleInstance.UniqueID)
+ defer C.free(unsafe.Pointer(cUniqueID))
+ C.startSingleInstanceListener(cUniqueID)
+ }
// Add a hook to the ApplicationDidFinishLaunching event
m.parent.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(*ApplicationEvent) {
C.setApplicationShouldTerminateAfterLastWindowClosed(C.bool(m.parent.options.Mac.ApplicationShouldTerminateAfterLastWindowClosed))
diff --git a/v3/pkg/application/application_darwin_delegate.m b/v3/pkg/application/application_darwin_delegate.m
index 82a91df2f29..0aeaaa4210c 100644
--- a/v3/pkg/application/application_darwin_delegate.m
+++ b/v3/pkg/application/application_darwin_delegate.m
@@ -4,6 +4,7 @@
extern bool hasListeners(unsigned int);
extern bool shouldQuitApplication();
extern void cleanup();
+extern void handleSecondInstanceData(char * message);
@implementation AppDelegate
- (void)dealloc
{
@@ -47,6 +48,15 @@ - (BOOL)applicationShouldHandleReopen:(NSNotification *)notification
return TRUE;
}
+- (void)handleSecondInstanceNotification:(NSNotification *)note;
+{
+ if (note.userInfo[@"message"] != nil) {
+ NSString *message = note.userInfo[@"message"];
+ const char* utf8Message = message.UTF8String;
+ handleSecondInstanceData((char*)utf8Message);
+ }
+}
+
// GENERATED EVENTS START
- (void)applicationDidBecomeActive:(NSNotification *)notification {
if( hasListeners(EventApplicationDidBecomeActive) ) {
diff --git a/v3/pkg/application/application_options.go b/v3/pkg/application/application_options.go
index 171b7e8a543..25328c9c6f5 100644
--- a/v3/pkg/application/application_options.go
+++ b/v3/pkg/application/application_options.go
@@ -117,6 +117,9 @@ type Options struct {
// The '.' is required
FileAssociations []string
+ // SingleInstance options for single instance functionality
+ SingleInstance *SingleInstanceOptions
+
// This blank field ensures types from other packages
// are never convertible to Options.
// This property, in turn, improves the accuracy of the binding generator.
diff --git a/v3/pkg/application/dialogs.go b/v3/pkg/application/dialogs.go
index 5fc908bc5fe..1e52225ce52 100644
--- a/v3/pkg/application/dialogs.go
+++ b/v3/pkg/application/dialogs.go
@@ -1,7 +1,6 @@
package application
import (
- "fmt"
"strings"
"sync"
)
@@ -294,9 +293,7 @@ func (d *OpenFileDialogStruct) PromptForMultipleSelection() ([]string, error) {
selections, err := InvokeSyncWithResultAndError(d.impl.show)
var result []string
- fmt.Println("Waiting for results:")
for filename := range selections {
- fmt.Println(filename)
result = append(result, filename)
}
diff --git a/v3/pkg/application/single_instance.go b/v3/pkg/application/single_instance.go
new file mode 100644
index 00000000000..43270ede16e
--- /dev/null
+++ b/v3/pkg/application/single_instance.go
@@ -0,0 +1,214 @@
+package application
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "os"
+ "path/filepath"
+ "sync"
+)
+
+var alreadyRunningError = errors.New("application is already running")
+var secondInstanceBuffer = make(chan string, 1)
+var once sync.Once
+
+// SecondInstanceData contains information about the second instance launch
+type SecondInstanceData struct {
+ Args []string `json:"args"`
+ WorkingDir string `json:"workingDir"`
+ AdditionalData map[string]string `json:"additionalData,omitempty"`
+}
+
+// SingleInstanceOptions defines options for single instance functionality
+type SingleInstanceOptions struct {
+ // UniqueID is used to identify the application instance
+ // This should be unique per application, e.g. "com.myapp.myapplication"
+ UniqueID string
+
+ // OnSecondInstanceLaunch is called when a second instance of the application is launched
+ // The callback receives data about the second instance launch
+ OnSecondInstanceLaunch func(data SecondInstanceData)
+
+ // AdditionalData allows passing custom data from second instance to first
+ AdditionalData map[string]string
+
+ // ExitCode is the exit code to use when the second instance exits
+ ExitCode int
+
+ // EncryptionKey is a 32-byte key used for encrypting instance communication
+ // If not provided (zero array), data will be sent unencrypted
+ EncryptionKey [32]byte
+}
+
+// platformLock is the interface that platform-specific lock implementations must implement
+type platformLock interface {
+ // acquire attempts to acquire the lock
+ acquire(uniqueID string) error
+ // release releases the lock and cleans up resources
+ release()
+ // notify sends data to the first instance
+ notify(data string) error
+}
+
+// singleInstanceManager handles the single instance functionality
+type singleInstanceManager struct {
+ options *SingleInstanceOptions
+ lock platformLock
+ app *App
+}
+
+func newSingleInstanceManager(app *App, options *SingleInstanceOptions) (*singleInstanceManager, error) {
+ if options == nil {
+ return nil, nil
+ }
+
+ manager := &singleInstanceManager{
+ options: options,
+ app: app,
+ }
+
+ // Launch second instance data listener
+ once.Do(func() {
+ go func() {
+ for encryptedData := range secondInstanceBuffer {
+ var secondInstanceData SecondInstanceData
+ var jsonData []byte
+ var err error
+
+ // Check if encryption key is non-zero
+ var zeroKey [32]byte
+ if options.EncryptionKey != zeroKey {
+ // Try to decrypt the data
+ jsonData, err = decrypt(options.EncryptionKey, encryptedData)
+ if err != nil {
+ continue // Skip invalid data
+ }
+ } else {
+ jsonData = []byte(encryptedData)
+ }
+
+ if err := json.Unmarshal(jsonData, &secondInstanceData); err == nil && manager.options.OnSecondInstanceLaunch != nil {
+ manager.options.OnSecondInstanceLaunch(secondInstanceData)
+ }
+ }
+ }()
+ })
+
+ // Create platform-specific lock
+ lock, err := newPlatformLock(manager)
+ if err != nil {
+ return nil, err
+ }
+
+ manager.lock = lock
+
+ // Try to acquire the lock
+ err = lock.acquire(options.UniqueID)
+ if err != nil {
+ return manager, err
+ }
+
+ return manager, nil
+}
+
+func (m *singleInstanceManager) cleanup() {
+ if m == nil || m.lock == nil {
+ return
+ }
+ m.lock.release()
+}
+
+// encrypt encrypts data using AES-256-GCM
+func encrypt(key [32]byte, plaintext []byte) (string, error) {
+ block, err := aes.NewCipher(key[:])
+ if err != nil {
+ return "", err
+ }
+
+ nonce := make([]byte, 12)
+ if _, err := rand.Read(nonce); err != nil {
+ return "", err
+ }
+
+ aesgcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", err
+ }
+
+ ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
+ encrypted := append(nonce, ciphertext...)
+ return base64.StdEncoding.EncodeToString(encrypted), nil
+}
+
+// decrypt decrypts data using AES-256-GCM
+func decrypt(key [32]byte, encrypted string) ([]byte, error) {
+ data, err := base64.StdEncoding.DecodeString(encrypted)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(data) < 12 {
+ return nil, errors.New("invalid encrypted data")
+ }
+
+ block, err := aes.NewCipher(key[:])
+ if err != nil {
+ return nil, err
+ }
+
+ aesgcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, err
+ }
+
+ nonce := data[:12]
+ ciphertext := data[12:]
+
+ return aesgcm.Open(nil, nonce, ciphertext, nil)
+}
+
+// notifyFirstInstance sends data to the first instance of the application
+func (m *singleInstanceManager) notifyFirstInstance() error {
+ data := SecondInstanceData{
+ Args: os.Args,
+ WorkingDir: getCurrentWorkingDir(),
+ AdditionalData: m.options.AdditionalData,
+ }
+
+ serialized, err := json.Marshal(data)
+ if err != nil {
+ return err
+ }
+
+ // Check if encryption key is non-zero
+ var zeroKey [32]byte
+ if m.options.EncryptionKey != zeroKey {
+ encrypted, err := encrypt(m.options.EncryptionKey, serialized)
+ if err != nil {
+ return err
+ }
+ return m.lock.notify(encrypted)
+ }
+
+ return m.lock.notify(string(serialized))
+}
+
+func getCurrentWorkingDir() string {
+ dir, err := os.Getwd()
+ if err != nil {
+ return ""
+ }
+ return dir
+}
+
+// getLockPath returns the path to the lock file for Unix systems
+func getLockPath(uniqueID string) string {
+ // Use system temp directory
+ tmpDir := os.TempDir()
+ lockFileName := uniqueID + ".lock"
+ return filepath.Join(tmpDir, lockFileName)
+}
diff --git a/v3/pkg/application/single_instance_darwin.go b/v3/pkg/application/single_instance_darwin.go
new file mode 100644
index 00000000000..4101294d81a
--- /dev/null
+++ b/v3/pkg/application/single_instance_darwin.go
@@ -0,0 +1,96 @@
+//go:build darwin
+
+package application
+
+/*
+#cgo CFLAGS: -x objective-c
+#cgo LDFLAGS: -framework Foundation -framework Cocoa
+
+#include
+#import
+#import
+
+static void SendDataToFirstInstance(char *singleInstanceUniqueId, char* message) {
+ [[NSDistributedNotificationCenter defaultCenter]
+ postNotificationName:[NSString stringWithUTF8String:singleInstanceUniqueId]
+ object:nil
+ userInfo:@{@"message": [NSString stringWithUTF8String:message]}
+ deliverImmediately:YES];
+}
+
+*/
+import "C"
+import (
+ "os"
+ "syscall"
+ "unsafe"
+)
+
+type darwinLock struct {
+ file *os.File
+ uniqueID string
+ manager *singleInstanceManager
+}
+
+func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
+ return &darwinLock{
+ manager: manager,
+ }, nil
+}
+
+func (l *darwinLock) acquire(uniqueID string) error {
+ l.uniqueID = uniqueID
+ lockFilePath := os.TempDir()
+ lockFileName := uniqueID + ".lock"
+ var err error
+ l.file, err = createLockFile(lockFilePath + "/" + lockFileName)
+ if err != nil {
+ return alreadyRunningError
+ }
+ return nil
+}
+
+func (l *darwinLock) release() {
+ if l.file != nil {
+ syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN)
+ l.file.Close()
+ os.Remove(l.file.Name())
+ l.file = nil
+ }
+}
+
+func (l *darwinLock) notify(data string) error {
+ singleInstanceUniqueId := C.CString(l.uniqueID)
+ defer C.free(unsafe.Pointer(singleInstanceUniqueId))
+ cData := C.CString(data)
+ defer C.free(unsafe.Pointer(cData))
+
+ C.SendDataToFirstInstance(singleInstanceUniqueId, cData)
+
+ os.Exit(l.manager.options.ExitCode)
+ return nil
+}
+
+// CreateLockFile tries to create a file with given name and acquire an
+// exclusive lock on it. If the file already exists AND is still locked, it will
+// fail.
+func createLockFile(filename string) (*os.File, error) {
+ file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0600)
+ if err != nil {
+ return nil, err
+ }
+
+ err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
+ if err != nil {
+ file.Close()
+ return nil, err
+ }
+
+ return file, nil
+}
+
+//export handleSecondInstanceData
+func handleSecondInstanceData(secondInstanceMessage *C.char) {
+ message := C.GoString(secondInstanceMessage)
+ secondInstanceBuffer <- message
+}
diff --git a/v3/pkg/application/single_instance_linux.go b/v3/pkg/application/single_instance_linux.go
new file mode 100644
index 00000000000..c2a306eeb68
--- /dev/null
+++ b/v3/pkg/application/single_instance_linux.go
@@ -0,0 +1,100 @@
+//go:build linux
+
+package application
+
+import (
+ "fmt"
+ "github.com/godbus/dbus/v5"
+ "os"
+ "strings"
+ "sync"
+ "syscall"
+)
+
+type dbusHandler func(string)
+
+var setup sync.Once
+
+func (f dbusHandler) SendMessage(message string) *dbus.Error {
+ f(message)
+ return nil
+}
+
+type linuxLock struct {
+ file *os.File
+ uniqueID string
+ dbusPath string
+ dbusName string
+ manager *singleInstanceManager
+}
+
+func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
+ return &linuxLock{
+ manager: manager,
+ }, nil
+}
+
+func (l *linuxLock) acquire(uniqueID string) error {
+ if uniqueID == "" {
+ return fmt.Errorf("UniqueID is required for single instance lock")
+ }
+
+ id := "wails_app_" + strings.ReplaceAll(strings.ReplaceAll(uniqueID, "-", "_"), ".", "_")
+
+ l.dbusName = "org." + id + ".SingleInstance"
+ l.dbusPath = "/org/" + id + "/SingleInstance"
+
+ conn, err := dbus.ConnectSessionBus()
+ // 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 err
+ }
+
+ setup.Do(func() {
+ f := dbusHandler(func(message string) {
+ secondInstanceBuffer <- message
+ })
+
+ err := conn.Export(f, dbus.ObjectPath(l.dbusPath), l.dbusName)
+ if err != nil {
+ globalApplication.error(err.Error())
+ }
+ })
+
+ reply, err := conn.RequestName(l.dbusName, dbus.NameFlagDoNotQueue)
+ if err != nil {
+ return err
+ }
+
+ // if name already taken, try to send args to existing instance, if no success just launch new instance
+ if reply == dbus.RequestNameReplyExists {
+ return alreadyRunningError
+ }
+ return nil
+}
+
+func (l *linuxLock) release() {
+ if l.file != nil {
+ syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN)
+ l.file.Close()
+ os.Remove(l.file.Name())
+ l.file = nil
+ }
+}
+
+func (l *linuxLock) notify(data string) error {
+ conn, err := dbus.ConnectSessionBus()
+ // 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 err
+ }
+
+ err = conn.Object(l.dbusName, dbus.ObjectPath(l.dbusPath)).Call(l.dbusName+".SendMessage", 0, data).Store()
+ if err != nil {
+ return err
+ }
+ os.Exit(l.manager.options.ExitCode)
+ return nil
+}
diff --git a/v3/pkg/application/single_instance_windows.go b/v3/pkg/application/single_instance_windows.go
new file mode 100644
index 00000000000..9f92b4eb7a7
--- /dev/null
+++ b/v3/pkg/application/single_instance_windows.go
@@ -0,0 +1,129 @@
+//go:build windows
+
+package application
+
+import (
+ "errors"
+ "fmt"
+ "github.com/wailsapp/wails/v3/pkg/w32"
+ "golang.org/x/sys/windows"
+ "syscall"
+ "unsafe"
+)
+
+var (
+ user32 = syscall.NewLazyDLL("user32.dll")
+)
+
+type windowsLock struct {
+ handle syscall.Handle
+ uniqueID string
+ msgString string
+ hwnd w32.HWND
+ manager *singleInstanceManager
+ className string
+ windowName string
+}
+
+func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
+ return &windowsLock{
+ manager: manager,
+ }, nil
+}
+
+func (l *windowsLock) acquire(uniqueID string) error {
+ if uniqueID == "" {
+ return fmt.Errorf("UniqueID is required for single instance lock")
+ }
+
+ l.uniqueID = uniqueID
+ id := "wails-app-" + uniqueID
+ l.className = id + "-sic"
+ l.windowName = id + "-siw"
+ mutexName := id + "-sim"
+
+ _, err := windows.CreateMutex(nil, false, windows.StringToUTF16Ptr(mutexName))
+ if err != nil {
+ // Find the window
+ return alreadyRunningError
+ } else {
+ l.hwnd = createEventTargetWindow(l.className, l.windowName)
+ }
+
+ return nil
+}
+
+func (l *windowsLock) release() {
+ if l.handle != 0 {
+ syscall.CloseHandle(l.handle)
+ l.handle = 0
+ }
+ if l.hwnd != 0 {
+ w32.DestroyWindow(l.hwnd)
+ l.hwnd = 0
+ }
+}
+
+func (l *windowsLock) notify(data string) error {
+
+ // app is already running
+ hwnd := w32.FindWindowW(windows.StringToUTF16Ptr(l.className), windows.StringToUTF16Ptr(l.windowName))
+
+ if hwnd == 0 {
+ return errors.New("unable to notify other instance")
+ }
+
+ w32.SendMessageToWindow(hwnd, data)
+
+ return nil
+}
+
+func createEventTargetWindow(className string, windowName string) w32.HWND {
+ 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 = w32.MustStringToUTF16Ptr(className)
+ class.IconSm = 0
+
+ w32.RegisterClassEx(&class)
+
+ // Create hidden message-only window
+ hwnd := w32.CreateWindowEx(
+ 0,
+ w32.MustStringToUTF16Ptr(className),
+ w32.MustStringToUTF16Ptr(windowName),
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ w32.HWND_MESSAGE,
+ 0,
+ w32.GetModuleHandle(""),
+ nil,
+ )
+
+ return hwnd
+}
+
+func wndProc(hwnd w32.HWND, msg uint32, wparam w32.WPARAM, lparam w32.LPARAM) w32.LRESULT {
+ if msg == w32.WM_COPYDATA {
+ ldata := (*w32.COPYDATASTRUCT)(unsafe.Pointer(lparam))
+
+ if ldata.DwData == w32.WMCOPYDATA_SINGLE_INSTANCE_DATA {
+ serialized := windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ldata.LpData)))
+ secondInstanceBuffer <- serialized
+ }
+ return w32.LRESULT(0)
+ }
+
+ return w32.DefWindowProc(hwnd, msg, wparam, lparam)
+}
diff --git a/v3/pkg/application/webview_window_darwin.m b/v3/pkg/application/webview_window_darwin.m
index 25684066046..a7b9f5010a9 100644
--- a/v3/pkg/application/webview_window_darwin.m
+++ b/v3/pkg/application/webview_window_darwin.m
@@ -195,38 +195,43 @@ - (void) dealloc {
}
- (void)windowDidZoom:(NSNotification *)notification {
NSWindow *window = notification.object;
+ WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[window delegate];
if ([window isZoomed]) {
if (hasListeners(EventWindowMaximise)) {
- processWindowEvent(self.windowId, EventWindowMaximise);
+ processWindowEvent(delegate.windowId, EventWindowMaximise);
}
} else {
if (hasListeners(EventWindowUnMaximise)) {
- processWindowEvent(self.windowId, EventWindowUnMaximise);
+ processWindowEvent(delegate.windowId, EventWindowUnMaximise);
}
}
}
- (void)performZoomIn:(id)sender {
[super zoom:sender];
if (hasListeners(EventWindowZoomIn)) {
- processWindowEvent(self.windowId, EventWindowZoomIn);
+ WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
+ processWindowEvent(delegate.windowId, EventWindowZoomIn);
}
}
- (void)performZoomOut:(id)sender {
[super zoom:sender];
if (hasListeners(EventWindowZoomOut)) {
- processWindowEvent(self.windowId, EventWindowZoomOut);
+ WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
+ processWindowEvent(delegate.windowId, EventWindowZoomOut);
}
}
- (void)performZoomReset:(id)sender {
[self setFrame:[self frameRectForContentRect:[[self screen] visibleFrame]] display:YES];
if (hasListeners(EventWindowZoomReset)) {
- processWindowEvent(self.windowId, EventWindowZoomReset);
+ WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
+ processWindowEvent(delegate.windowId, EventWindowZoomReset);
}
}
@end
@implementation WebviewWindowDelegate
- (BOOL)windowShouldClose:(NSWindow *)sender {
- processWindowEvent(self.windowId, EventWindowShouldClose);
+ WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
+ processWindowEvent(delegate.windowId, EventWindowShouldClose);
return false;
}
- (void) dealloc {
@@ -730,6 +735,18 @@ - (void)windowFileDraggingExited:(NSNotification *)notification {
}
}
+- (void)windowShow:(NSNotification *)notification {
+ if( hasListeners(EventWindowShow) ) {
+ processWindowEvent(self.windowId, EventWindowShow);
+ }
+}
+
+- (void)windowHide:(NSNotification *)notification {
+ if( hasListeners(EventWindowHide) ) {
+ processWindowEvent(self.windowId, EventWindowHide);
+ }
+}
+
- (void)webView:(WKWebView *)webview didStartProvisionalNavigation:(WKNavigation *)navigation {
if( hasListeners(EventWebViewDidStartProvisionalNavigation) ) {
processWindowEvent(self.windowId, EventWebViewDidStartProvisionalNavigation);
diff --git a/v3/pkg/events/events.go b/v3/pkg/events/events.go
index dc34cb3f6bc..56529ae9d2e 100644
--- a/v3/pkg/events/events.go
+++ b/v3/pkg/events/events.go
@@ -34,30 +34,30 @@ type commonEvents struct {
func newCommonEvents() commonEvents {
return commonEvents{
- ApplicationStarted: 1203,
- WindowMaximise: 1204,
- WindowUnMaximise: 1205,
- WindowFullscreen: 1206,
- WindowUnFullscreen: 1207,
- WindowRestore: 1208,
- WindowMinimise: 1209,
- WindowUnMinimise: 1210,
- WindowClosing: 1211,
- WindowZoom: 1212,
- WindowZoomIn: 1213,
- WindowZoomOut: 1214,
- WindowZoomReset: 1215,
- WindowFocus: 1216,
- WindowLostFocus: 1217,
- WindowShow: 1218,
- WindowHide: 1219,
- WindowDPIChanged: 1220,
- WindowFilesDropped: 1221,
- WindowRuntimeReady: 1222,
- ThemeChanged: 1223,
- WindowDidMove: 1224,
- WindowDidResize: 1225,
- ApplicationOpenedWithFile: 1226,
+ ApplicationStarted: 1205,
+ WindowMaximise: 1206,
+ WindowUnMaximise: 1207,
+ WindowFullscreen: 1208,
+ WindowUnFullscreen: 1209,
+ WindowRestore: 1210,
+ WindowMinimise: 1211,
+ WindowUnMinimise: 1212,
+ WindowClosing: 1213,
+ WindowZoom: 1214,
+ WindowZoomIn: 1215,
+ WindowZoomOut: 1216,
+ WindowZoomReset: 1217,
+ WindowFocus: 1218,
+ WindowLostFocus: 1219,
+ WindowShow: 1220,
+ WindowHide: 1221,
+ WindowDPIChanged: 1222,
+ WindowFilesDropped: 1223,
+ WindowRuntimeReady: 1224,
+ ThemeChanged: 1225,
+ WindowDidMove: 1226,
+ WindowDidResize: 1227,
+ ApplicationOpenedWithFile: 1228,
}
}
@@ -218,6 +218,8 @@ type macEvents struct {
WindowFileDraggingEntered WindowEventType
WindowFileDraggingPerformed WindowEventType
WindowFileDraggingExited WindowEventType
+ WindowShow WindowEventType
+ WindowHide WindowEventType
}
func newMacEvents() macEvents {
@@ -350,6 +352,8 @@ func newMacEvents() macEvents {
WindowFileDraggingEntered: 1157,
WindowFileDraggingPerformed: 1158,
WindowFileDraggingExited: 1159,
+ WindowShow: 1160,
+ WindowHide: 1161,
}
}
@@ -403,49 +407,49 @@ type windowsEvents struct {
func newWindowsEvents() windowsEvents {
return windowsEvents{
- SystemThemeChanged: 1160,
- APMPowerStatusChange: 1161,
- APMSuspend: 1162,
- APMResumeAutomatic: 1163,
- APMResumeSuspend: 1164,
- APMPowerSettingChange: 1165,
- ApplicationStarted: 1166,
- WebViewNavigationCompleted: 1167,
- WindowInactive: 1168,
- WindowActive: 1169,
- WindowClickActive: 1170,
- WindowMaximise: 1171,
- WindowUnMaximise: 1172,
- WindowFullscreen: 1173,
- WindowUnFullscreen: 1174,
- WindowRestore: 1175,
- WindowMinimise: 1176,
- WindowUnMinimise: 1177,
- WindowClosing: 1178,
- WindowSetFocus: 1179,
- WindowKillFocus: 1180,
- WindowDragDrop: 1181,
- WindowDragEnter: 1182,
- WindowDragLeave: 1183,
- WindowDragOver: 1184,
- WindowDidMove: 1185,
- WindowDidResize: 1186,
- WindowShow: 1187,
- WindowHide: 1188,
- WindowStartMove: 1189,
- WindowEndMove: 1190,
- WindowStartResize: 1191,
- WindowEndResize: 1192,
- WindowKeyDown: 1193,
- WindowKeyUp: 1194,
- WindowZOrderChanged: 1195,
- WindowPaint: 1196,
- WindowBackgroundErase: 1197,
- WindowNonClientHit: 1198,
- WindowNonClientMouseDown: 1199,
- WindowNonClientMouseUp: 1200,
- WindowNonClientMouseMove: 1201,
- WindowNonClientMouseLeave: 1202,
+ SystemThemeChanged: 1162,
+ APMPowerStatusChange: 1163,
+ APMSuspend: 1164,
+ APMResumeAutomatic: 1165,
+ APMResumeSuspend: 1166,
+ APMPowerSettingChange: 1167,
+ ApplicationStarted: 1168,
+ WebViewNavigationCompleted: 1169,
+ WindowInactive: 1170,
+ WindowActive: 1171,
+ WindowClickActive: 1172,
+ WindowMaximise: 1173,
+ WindowUnMaximise: 1174,
+ WindowFullscreen: 1175,
+ WindowUnFullscreen: 1176,
+ WindowRestore: 1177,
+ WindowMinimise: 1178,
+ WindowUnMinimise: 1179,
+ WindowClosing: 1180,
+ WindowSetFocus: 1181,
+ WindowKillFocus: 1182,
+ WindowDragDrop: 1183,
+ WindowDragEnter: 1184,
+ WindowDragLeave: 1185,
+ WindowDragOver: 1186,
+ WindowDidMove: 1187,
+ WindowDidResize: 1188,
+ WindowShow: 1189,
+ WindowHide: 1190,
+ WindowStartMove: 1191,
+ WindowEndMove: 1192,
+ WindowStartResize: 1193,
+ WindowEndResize: 1194,
+ WindowKeyDown: 1195,
+ WindowKeyUp: 1196,
+ WindowZOrderChanged: 1197,
+ WindowPaint: 1198,
+ WindowBackgroundErase: 1199,
+ WindowNonClientHit: 1200,
+ WindowNonClientMouseDown: 1201,
+ WindowNonClientMouseUp: 1202,
+ WindowNonClientMouseMove: 1203,
+ WindowNonClientMouseLeave: 1204,
}
}
@@ -590,71 +594,73 @@ var eventToJS = map[uint]string{
1157: "mac:WindowFileDraggingEntered",
1158: "mac:WindowFileDraggingPerformed",
1159: "mac:WindowFileDraggingExited",
- 1160: "windows:SystemThemeChanged",
- 1161: "windows:APMPowerStatusChange",
- 1162: "windows:APMSuspend",
- 1163: "windows:APMResumeAutomatic",
- 1164: "windows:APMResumeSuspend",
- 1165: "windows:APMPowerSettingChange",
- 1166: "windows:ApplicationStarted",
- 1167: "windows:WebViewNavigationCompleted",
- 1168: "windows:WindowInactive",
- 1169: "windows:WindowActive",
- 1170: "windows:WindowClickActive",
- 1171: "windows:WindowMaximise",
- 1172: "windows:WindowUnMaximise",
- 1173: "windows:WindowFullscreen",
- 1174: "windows:WindowUnFullscreen",
- 1175: "windows:WindowRestore",
- 1176: "windows:WindowMinimise",
- 1177: "windows:WindowUnMinimise",
- 1178: "windows:WindowClosing",
- 1179: "windows:WindowSetFocus",
- 1180: "windows:WindowKillFocus",
- 1181: "windows:WindowDragDrop",
- 1182: "windows:WindowDragEnter",
- 1183: "windows:WindowDragLeave",
- 1184: "windows:WindowDragOver",
- 1185: "windows:WindowDidMove",
- 1186: "windows:WindowDidResize",
- 1187: "windows:WindowShow",
- 1188: "windows:WindowHide",
- 1189: "windows:WindowStartMove",
- 1190: "windows:WindowEndMove",
- 1191: "windows:WindowStartResize",
- 1192: "windows:WindowEndResize",
- 1193: "windows:WindowKeyDown",
- 1194: "windows:WindowKeyUp",
- 1195: "windows:WindowZOrderChanged",
- 1196: "windows:WindowPaint",
- 1197: "windows:WindowBackgroundErase",
- 1198: "windows:WindowNonClientHit",
- 1199: "windows:WindowNonClientMouseDown",
- 1200: "windows:WindowNonClientMouseUp",
- 1201: "windows:WindowNonClientMouseMove",
- 1202: "windows:WindowNonClientMouseLeave",
- 1203: "common:ApplicationStarted",
- 1204: "common:WindowMaximise",
- 1205: "common:WindowUnMaximise",
- 1206: "common:WindowFullscreen",
- 1207: "common:WindowUnFullscreen",
- 1208: "common:WindowRestore",
- 1209: "common:WindowMinimise",
- 1210: "common:WindowUnMinimise",
- 1211: "common:WindowClosing",
- 1212: "common:WindowZoom",
- 1213: "common:WindowZoomIn",
- 1214: "common:WindowZoomOut",
- 1215: "common:WindowZoomReset",
- 1216: "common:WindowFocus",
- 1217: "common:WindowLostFocus",
- 1218: "common:WindowShow",
- 1219: "common:WindowHide",
- 1220: "common:WindowDPIChanged",
- 1221: "common:WindowFilesDropped",
- 1222: "common:WindowRuntimeReady",
- 1223: "common:ThemeChanged",
- 1224: "common:WindowDidMove",
- 1225: "common:WindowDidResize",
- 1226: "common:ApplicationOpenedWithFile",
+ 1160: "mac:WindowShow",
+ 1161: "mac:WindowHide",
+ 1162: "windows:SystemThemeChanged",
+ 1163: "windows:APMPowerStatusChange",
+ 1164: "windows:APMSuspend",
+ 1165: "windows:APMResumeAutomatic",
+ 1166: "windows:APMResumeSuspend",
+ 1167: "windows:APMPowerSettingChange",
+ 1168: "windows:ApplicationStarted",
+ 1169: "windows:WebViewNavigationCompleted",
+ 1170: "windows:WindowInactive",
+ 1171: "windows:WindowActive",
+ 1172: "windows:WindowClickActive",
+ 1173: "windows:WindowMaximise",
+ 1174: "windows:WindowUnMaximise",
+ 1175: "windows:WindowFullscreen",
+ 1176: "windows:WindowUnFullscreen",
+ 1177: "windows:WindowRestore",
+ 1178: "windows:WindowMinimise",
+ 1179: "windows:WindowUnMinimise",
+ 1180: "windows:WindowClosing",
+ 1181: "windows:WindowSetFocus",
+ 1182: "windows:WindowKillFocus",
+ 1183: "windows:WindowDragDrop",
+ 1184: "windows:WindowDragEnter",
+ 1185: "windows:WindowDragLeave",
+ 1186: "windows:WindowDragOver",
+ 1187: "windows:WindowDidMove",
+ 1188: "windows:WindowDidResize",
+ 1189: "windows:WindowShow",
+ 1190: "windows:WindowHide",
+ 1191: "windows:WindowStartMove",
+ 1192: "windows:WindowEndMove",
+ 1193: "windows:WindowStartResize",
+ 1194: "windows:WindowEndResize",
+ 1195: "windows:WindowKeyDown",
+ 1196: "windows:WindowKeyUp",
+ 1197: "windows:WindowZOrderChanged",
+ 1198: "windows:WindowPaint",
+ 1199: "windows:WindowBackgroundErase",
+ 1200: "windows:WindowNonClientHit",
+ 1201: "windows:WindowNonClientMouseDown",
+ 1202: "windows:WindowNonClientMouseUp",
+ 1203: "windows:WindowNonClientMouseMove",
+ 1204: "windows:WindowNonClientMouseLeave",
+ 1205: "common:ApplicationStarted",
+ 1206: "common:WindowMaximise",
+ 1207: "common:WindowUnMaximise",
+ 1208: "common:WindowFullscreen",
+ 1209: "common:WindowUnFullscreen",
+ 1210: "common:WindowRestore",
+ 1211: "common:WindowMinimise",
+ 1212: "common:WindowUnMinimise",
+ 1213: "common:WindowClosing",
+ 1214: "common:WindowZoom",
+ 1215: "common:WindowZoomIn",
+ 1216: "common:WindowZoomOut",
+ 1217: "common:WindowZoomReset",
+ 1218: "common:WindowFocus",
+ 1219: "common:WindowLostFocus",
+ 1220: "common:WindowShow",
+ 1221: "common:WindowHide",
+ 1222: "common:WindowDPIChanged",
+ 1223: "common:WindowFilesDropped",
+ 1224: "common:WindowRuntimeReady",
+ 1225: "common:ThemeChanged",
+ 1226: "common:WindowDidMove",
+ 1227: "common:WindowDidResize",
+ 1228: "common:ApplicationOpenedWithFile",
}
diff --git a/v3/pkg/events/events.txt b/v3/pkg/events/events.txt
index 02193852e9f..5703c13d0c3 100644
--- a/v3/pkg/events/events.txt
+++ b/v3/pkg/events/events.txt
@@ -134,6 +134,8 @@ mac:WebViewDidCommitNavigation
mac:WindowFileDraggingEntered
mac:WindowFileDraggingPerformed
mac:WindowFileDraggingExited
+mac:WindowShow
+mac:WindowHide
windows:SystemThemeChanged
windows:APMPowerStatusChange
windows:APMSuspend
diff --git a/v3/pkg/events/events_darwin.h b/v3/pkg/events/events_darwin.h
index 4e551ea0e61..6a6cefc4c7a 100644
--- a/v3/pkg/events/events_darwin.h
+++ b/v3/pkg/events/events_darwin.h
@@ -134,8 +134,10 @@ extern void processWindowEvent(unsigned int, unsigned int);
#define EventWindowFileDraggingEntered 1157
#define EventWindowFileDraggingPerformed 1158
#define EventWindowFileDraggingExited 1159
+#define EventWindowShow 1160
+#define EventWindowHide 1161
-#define MAX_EVENTS 1160
+#define MAX_EVENTS 1162
#endif
\ No newline at end of file
diff --git a/v3/pkg/w32/window.go b/v3/pkg/w32/window.go
index eb75e3ef100..542971db7d9 100644
--- a/v3/pkg/w32/window.go
+++ b/v3/pkg/w32/window.go
@@ -25,8 +25,20 @@ var (
user32 = syscall.NewLazyDLL("user32.dll")
getSystemMenu = user32.NewProc("GetSystemMenu")
enableMenuItem = user32.NewProc("EnableMenuItem")
+ findWindow = user32.NewProc("FindWindowW")
+ sendMessage = user32.NewProc("SendMessageW")
)
+const (
+ WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542
+)
+
+type COPYDATASTRUCT struct {
+ DwData uintptr
+ CbData uint32
+ LpData uintptr
+}
+
var Fatal func(error)
const (
@@ -305,3 +317,31 @@ func EnableCloseButton(hwnd HWND) error {
return nil
}
+
+func FindWindowW(className, windowName *uint16) HWND {
+ ret, _, _ := findWindow.Call(
+ uintptr(unsafe.Pointer(className)),
+ uintptr(unsafe.Pointer(windowName)),
+ )
+ return HWND(ret)
+}
+
+func SendMessageToWindow(hwnd HWND, msg string) {
+ // Convert data to UTF16 string
+ dataUTF16 := MustStringToUTF16(msg)
+
+ // Prepare COPYDATASTRUCT
+ cds := COPYDATASTRUCT{
+ DwData: WMCOPYDATA_SINGLE_INSTANCE_DATA,
+ CbData: uint32((len(dataUTF16) * 2) + 1), // +1 for null terminator
+ LpData: uintptr(unsafe.Pointer(&dataUTF16[0])),
+ }
+
+ // Send message to first instance
+ _, _, _ = procSendMessage.Call(
+ hwnd,
+ WM_COPYDATA,
+ 0,
+ uintptr(unsafe.Pointer(&cds)),
+ )
+}