5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-04 00:29:36 +08:00

Single Instance feature.

Fix missing events on darwin.
This commit is contained in:
Lea Anthony 2024-12-30 21:02:43 +11:00
parent 7cee957161
commit 773dca77d4
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
23 changed files with 1313 additions and 252 deletions

View File

@ -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

View File

@ -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`

View File

@ -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
<Tabs syncKey="platform">
<TabItem label="Mac" icon="apple">
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)
</TabItem>
<TabItem label="Windows" icon="seti:windows">
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)
</TabItem>
<TabItem label="Linux" icon="linux">
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/)
</TabItem>
</Tabs>

View File

@ -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

View File

@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Single Instance Demo</title>
<script src="/wails/runtime.js" type="module"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #2c5282;
margin-bottom: 20px;
}
.info-box {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.info-box h2 {
margin-top: 0;
color: #4a5568;
font-size: 1.2em;
}
.info-item {
margin: 10px 0;
}
.info-label {
font-weight: bold;
color: #4a5568;
}
.args-list {
margin: 5px 0;
padding-left: 20px;
}
.args-list li {
word-break: break-all;
margin: 3px 0;
}
.instructions {
background-color: #ebf8ff;
border: 1px solid #bee3f8;
border-radius: 4px;
padding: 15px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Single Instance Demo</h1>
<div class="info-box">
<h2>Current Instance Information</h2>
<div id="instanceInfo">Loading...</div>
</div>
<div class="instructions">
<h2>Instructions</h2>
<p>Try launching another instance of this application. The first instance will:</p>
<ul>
<li>Receive notification of the second instance launch</li>
<li>Get the command line arguments of the second instance</li>
<li>Get the working directory of the second instance</li>
<li>Receive any additional data passed from the second instance</li>
</ul>
<p>Check the application logs to see the information received from second instances.</p>
</div>
</div>
<script>
let lastNotificationTime = "";
function init() {
updateInstanceInfo();
// Listen for second instance launch
wails.Events.On('secondInstanceLaunched', (info) => {
console.log('Second instance launched');
lastNotificationTime = new Date().toLocaleString();
debugger;
updateInstanceInfo(info.data[0]);
})
}
// Update instance information
async function updateInstanceInfo(info) {
if (!info) {
info = await wails.Call.ByName('main.App.GetCurrentInstanceInfo');
}
const infoDiv = document.getElementById('instanceInfo');
infoDiv.innerHTML = '';
if (lastNotificationTime) {
infoDiv.innerHTML += `<div class="info-item">
<span class="info-label">Second Instance Launch Time:</span> ${lastNotificationTime}
</div>`
}
infoDiv.innerHTML += `
<div class="info-item">
<span class="info-label">Arguments:</span>
<ul class="args-list">
${info.args.map(arg => `<li>${arg}</li>`).join('')}
</ul>
</div>
<div class="info-item">
<span class="info-label">Working Directory:</span> ${info.workingDir}
</div>
`;
if (info.additionalData) {
infoDiv.innerHTML += `
<div class="info-item">
<span class="info-label">Additional Data:</span>
<ul class="args-list">
${Object.entries(info.additionalData).map(([key, value]) => `<li>${key}: ${value}</li>`).join('')}
</ul>
</div>
`;
}
}
// Update info when page loads
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>

View File

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

View File

@ -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": {

View File

@ -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",

View File

@ -174,6 +174,8 @@ export declare const EventTypes: {
WindowFileDraggingEntered: string,
WindowFileDraggingPerformed: string,
WindowFileDraggingExited: string,
WindowShow: string,
WindowHide: string,
},
Linux: {
SystemThemeChanged: string,

View File

@ -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() {

View File

@ -15,6 +15,7 @@ package application
extern void registerListener(unsigned int event);
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
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))

View File

@ -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 @@ extern void cleanup();
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) ) {

View File

@ -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.

View File

@ -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)
}

View File

@ -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)
}

View File

@ -0,0 +1,96 @@
//go:build darwin
package application
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa
#include <stdlib.h>
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
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
}

View File

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

View File

@ -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)
}

View File

@ -195,38 +195,43 @@ extern bool hasListeners(unsigned int);
}
- (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 @@ extern bool hasListeners(unsigned int);
}
}
- (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);

View File

@ -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",
}

View File

@ -134,6 +134,8 @@ mac:WebViewDidCommitNavigation
mac:WindowFileDraggingEntered
mac:WindowFileDraggingPerformed
mac:WindowFileDraggingExited
mac:WindowShow
mac:WindowHide
windows:SystemThemeChanged
windows:APMPowerStatusChange
windows:APMSuspend

View File

@ -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

View File

@ -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)),
)
}