diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx
index 6b9c5f152..681e3715f 100644
--- a/docs/src/content/docs/changelog.mdx
+++ b/docs/src/content/docs/changelog.mdx
@@ -76,6 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add cancellation support for query methods on `sqlite` service by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
- Add prepared statement support to `sqlite` service with JS bindings by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
- Add `SetMenu()` on window to allow for setting a menu on a window by [@leaanthony](https://github.com/leaanthony)
+- Add Notification support by [@popaprozac] in [#4098](https://github.com/wailsapp/wails/pull/4098)
- Add File Association support for mac by [@wimaha](https://github.com/wimaha) in [#4177](https://github.com/wailsapp/wails/pull/4177)
### Fixed
diff --git a/docs/src/content/docs/learn/notifications.mdx b/docs/src/content/docs/learn/notifications.mdx
new file mode 100644
index 000000000..5a59f5e2a
--- /dev/null
+++ b/docs/src/content/docs/learn/notifications.mdx
@@ -0,0 +1,304 @@
+---
+title: Notifications
+---
+
+import { Tabs, TabItem } from "@astrojs/starlight/components";
+
+## Introduction
+
+Wails provides a comprehensive cross-platform notification system for desktop applications. This service allows you to display native system notifications, with support for interactive elements like action buttons and text input fields.
+
+## Basic Usage
+
+### Creating the Service
+
+First, initialize the notifications service:
+
+```go
+import "github.com/wailsapp/wails/v3/pkg/application"
+import "github.com/wailsapp/wails/v3/services/notifications"
+
+// Create a new notification service
+notifier := notifications.New()
+
+//Register the service with the application
+app := application.New(application.Options{
+ Services: []application.Service{
+ application.NewService(notifier),
+ },
+})
+```
+
+## Notification Authorization
+
+Notifications on macOS require user authorization. Request and check authorization:
+
+```go
+authorized, err := notifier.CheckNotificationAuthorization()
+if err != nil {
+ // Handle authorization error
+}
+if authorized {
+ // Send notifications
+} else {
+ // Request authorization
+ authorized, err = notifier.RequestNotificationAuthorization()
+}
+```
+On Windows and Linux this always returns `true`.
+
+## Notification Types
+
+### Basic Notifications
+
+Send a basic notification with a unique id, title, optional subtitle (macOS and Linux), and body text to users:
+
+```go
+notifier.SendNotification(notifications.NotificationOptions{
+ ID: "unique-id",
+ Title: "New Calendar Invite",
+ Subtitle: "From: Jane Doe", // Optional
+ Body: "Tap to view the event",
+})
+
+```
+
+### Interactive Notifications
+Send a notification with action buttons and text inputs. These notifications require a notification category to be resgistered first:
+
+```go
+// Define a unique category id
+categoryID := "unique-category-id"
+
+// Define a category with actions
+category := notifications.NotificationCategory{
+ ID: categoryID,
+ Actions: []notifications.NotificationAction{
+ {
+ ID: "OPEN",
+ Title: "Open",
+ },
+ {
+ ID: "ARCHIVE",
+ Title: "Archive",
+ Destructive: true, /* macOS-specific */
+ },
+ },
+ HasReplyField: true,
+ ReplyPlaceholder: "message...",
+ ReplyButtonTitle: "Reply",
+}
+
+// Register the category
+notifier.RegisterNotificationCategory(category)
+
+// Send an interactive notification with the actions registered in the provided category
+notifier.SendNotificationWithActions(notifications.NotificationOptions{
+ ID: "unique-id",
+ Title: "New Message",
+ Subtitle: "From: Jane Doe",
+ Body: "Are you able to make it?",
+ CategoryID: categoryID,
+})
+```
+
+## Notification Responses
+
+Process user interactions with notifications:
+
+```go
+notifier.OnNotificationResponse(func(result notifications.NotificationResult) {
+ response := result.Response
+ fmt.Printf("Notification %s was actioned with: %s\n", response.ID, response.ActionIdentifier)
+
+ if response.ActionIdentifier == "TEXT_REPLY" {
+ fmt.Printf("User replied: %s\n", response.UserText)
+ }
+
+ if data, ok := response.UserInfo["sender"].(string); ok {
+ fmt.Printf("Original sender: %s\n", data)
+ }
+
+ // Emit an event to the frontend
+ app.EmitEvent("notification", result.Response)
+})
+```
+
+## Notification Customisation
+
+### Custom Metadata
+
+Basic and interactive notifications can include custom data:
+
+```go
+notifier.SendNotification(notifications.NotificationOptions{
+ ID: "unique-id",
+ Title: "New Calendar Invite",
+ Subtitle: "From: Jane Doe", // Optional
+ Body: "Tap to view the event",
+ Data: map[string]interface{}{
+ "sender": "jane.doe@example.com",
+ "timestamp": "2025-03-10T15:30:00Z",
+ }
+})
+
+```
+
+## Platform Considerations
+
+
+
+
+ On macOS, notifications:
+
+ - Require user authorization
+ - Require the app to be notorized for distribution
+ - Use system-standard notification appearances
+ - Support `subtitle`
+ - Support user text input
+ - Support the `Destructive` action option
+ - Automatically handle dark/light mode
+
+
+
+
+
+ On Windows, notifications:
+
+ - Use Windows system toast styles
+ - Adapt to Windows theme settings
+ - Support user text input
+ - Support high DPI displays
+ - Do not support `subtitle`
+
+
+
+
+
+ On Linux, dialog behaviour depends on the desktop environment:
+
+ - Use native notifications when available
+ - Follow desktop environment theme
+ - Position according to desktop environment rules
+ - Support `subtitle`
+ - Do not support user text input
+
+
+
+
+## Best Practices
+
+1. Check and request for authorization:
+ - macOS requires user authorization
+
+2. Provide clear and concise notifications:
+ - Use descriptive titles, subtitles, text, and action titles
+
+3. Handle dialog responses appropriately:
+ - Check for errors in notification responses
+ - Provide feedback for user actions
+
+4. Consider platform conventions:
+ - Follow platform-specific notification patterns
+ - Respect system settings
+
+## Examples
+
+Explore this example:
+
+- [Notifications](/examples/notifications)
+
+## API Reference
+
+### Service Management
+| Method | Description |
+|--------------------------------------------|-------------------------------------------------------|
+| `New()` | Creates a new notifications service |
+
+### Notification Authorization
+| Method | Description |
+|----------------------------------------------|------------------------------------------------------------|
+| `RequestNotificationAuthorization()` | Requests permission to display notifications (macOS) |
+| `CheckNotificationAuthorization()` | Checks current notification authorization status (macOS) |
+
+### Sending Notifications
+| Method | Description |
+|------------------------------------------------------------|---------------------------------------------------|
+| `SendNotification(options NotificationOptions)` | Sends a basic notification |
+| `SendNotificationWithActions(options NotificationOptions)` | Sends an interactive notification with actions |
+
+### Notification Categories
+| Method | Description |
+|---------------------------------------------------------------|---------------------------------------------------|
+| `RegisterNotificationCategory(category NotificationCategory)` | Registers a reusable notification category |
+| `RemoveNotificationCategory(categoryID string)` | Removes a previously registered category |
+
+### Managing Notifications
+| Method | Description |
+|-------------------------------------------------|---------------------------------------------------------------------|
+| `RemoveAllPendingNotifications()` | Removes all pending notifications (macOS and Linux only) |
+| `RemovePendingNotification(identifier string)` | Removes a specific pending notification (macOS and Linux only) |
+| `RemoveAllDeliveredNotifications()` | Removes all delivered notifications (macOS and Linux only) |
+| `RemoveDeliveredNotification(identifier string)`| Removes a specific delivered notification (macOS and Linux only) |
+| `RemoveNotification(identifier string)` | Removes a notification (Linux-specific) |
+
+### Event Handling
+| Method | Description |
+|--------------------------------------------------------------------|-------------------------------------------------|
+| `OnNotificationResponse(callback func(result NotificationResult))` | Registers a callback for notification responses |
+
+### Structs and Types
+
+#### NotificationOptions
+```go
+type NotificationOptions struct {
+ ID string // Unique identifier for the notification
+ Title string // Main notification title
+ Subtitle string // Subtitle text (macOS and Linux only)
+ Body string // Main notification content
+ CategoryID string // Category identifier for interactive notifications
+ Data map[string]interface{} // Custom data to associate with the notification
+}
+```
+
+#### NotificationCategory
+```go
+type NotificationCategory struct {
+ ID string // Unique identifier for the category
+ Actions []NotificationAction // Button actions for the notification
+ HasReplyField bool // Whether to include a text input field
+ ReplyPlaceholder string // Placeholder text for the input field
+ ReplyButtonTitle string // Text for the reply button
+}
+```
+
+#### NotificationAction
+```go
+type NotificationAction struct {
+ ID string // Unique identifier for the action
+ Title string // Button text
+ Destructive bool // Whether the action is destructive (macOS-specific)
+}
+```
+
+#### NotificationResponse
+```go
+type NotificationResponse struct {
+ ID string // Notification identifier
+ ActionIdentifier string // Action that was triggered
+ CategoryID string // Category of the notification
+ Title string // Title of the notification
+ Subtitle string // Subtitle of the notification
+ Body string // Body text of the notification
+ UserText string // Text entered by the user
+ UserInfo map[string]interface{} // Custom data from the notification
+}
+```
+
+#### NotificationResult
+```go
+type NotificationResult struct {
+ Response NotificationResponse // Response data
+ Error error // Any error that occurred
+}
+```
\ No newline at end of file
diff --git a/v3/examples/notifications/README.md b/v3/examples/notifications/README.md
new file mode 100644
index 000000000..ad12c3f40
--- /dev/null
+++ b/v3/examples/notifications/README.md
@@ -0,0 +1,59 @@
+# Welcome to Your New Wails3 Project!
+
+Congratulations on generating your Wails3 application! This README will guide you through the next steps to get your project up and running.
+
+## Getting Started
+
+1. Navigate to your project directory in the terminal.
+
+2. To run your application in development mode, use the following command:
+
+ ```
+ wails3 dev
+ ```
+
+ This will start your application and enable hot-reloading for both frontend and backend changes.
+
+3. To build your application for production, use:
+
+ ```
+ wails3 build
+ ```
+
+ This will create a production-ready executable in the `build` directory.
+
+## Exploring Wails3 Features
+
+Now that you have your project set up, it's time to explore the features that Wails3 offers:
+
+1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications.
+
+2. **Run an example**: To run any of the examples, navigate to the example's directory and use:
+
+ ```
+ go run .
+ ```
+
+ Note: Some examples may be under development during the alpha phase.
+
+3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3.wails.io/) for in-depth guides and API references.
+
+4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions).
+
+## Project Structure
+
+Take a moment to familiarize yourself with your project structure:
+
+- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript)
+- `main.go`: The entry point of your Go backend
+- `app.go`: Define your application structure and methods here
+- `wails.json`: Configuration file for your Wails project
+
+## Next Steps
+
+1. Modify the frontend in the `frontend/` directory to create your desired UI.
+2. Add backend functionality in `main.go`.
+3. Use `wails3 dev` to see your changes in real-time.
+4. When ready, build your application with `wails3 build`.
+
+Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community.
diff --git a/v3/examples/notifications/Taskfile.yml b/v3/examples/notifications/Taskfile.yml
new file mode 100644
index 000000000..1455cd70c
--- /dev/null
+++ b/v3/examples/notifications/Taskfile.yml
@@ -0,0 +1,34 @@
+version: '3'
+
+includes:
+ common: ./build/Taskfile.yml
+ windows: ./build/windows/Taskfile.yml
+ darwin: ./build/darwin/Taskfile.yml
+ linux: ./build/linux/Taskfile.yml
+
+vars:
+ APP_NAME: "Notifications\\ Demo"
+ BIN_DIR: "bin"
+ VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
+
+tasks:
+ build:
+ summary: Builds the application
+ cmds:
+ - task: "{{OS}}:build"
+
+ package:
+ summary: Packages a production build of the application
+ cmds:
+ - task: "{{OS}}:package"
+
+ run:
+ summary: Runs the application
+ cmds:
+ - task: "{{OS}}:run"
+
+ dev:
+ summary: Runs the application in development mode
+ cmds:
+ - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
+
diff --git a/v3/examples/notifications/build/Taskfile.yml b/v3/examples/notifications/build/Taskfile.yml
new file mode 100644
index 000000000..5f3517efc
--- /dev/null
+++ b/v3/examples/notifications/build/Taskfile.yml
@@ -0,0 +1,86 @@
+version: '3'
+
+tasks:
+ go:mod:tidy:
+ summary: Runs `go mod tidy`
+ internal: true
+ cmds:
+ - go mod tidy
+
+ install:frontend:deps:
+ summary: Install frontend dependencies
+ dir: frontend
+ sources:
+ - package.json
+ - package-lock.json
+ generates:
+ - node_modules/*
+ preconditions:
+ - sh: npm version
+ msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
+ cmds:
+ - npm install
+
+ build:frontend:
+ label: build:frontend (PRODUCTION={{.PRODUCTION}})
+ summary: Build the frontend project
+ dir: frontend
+ sources:
+ - "**/*"
+ generates:
+ - dist/**/*
+ deps:
+ - task: install:frontend:deps
+ - task: generate:bindings
+ vars:
+ BUILD_FLAGS:
+ ref: .BUILD_FLAGS
+ cmds:
+ - npm run {{.BUILD_COMMAND}} -q
+ env:
+ PRODUCTION: '{{.PRODUCTION | default "false"}}'
+ vars:
+ BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build:dev{{end}}'
+
+
+ generate:bindings:
+ label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}})
+ summary: Generates bindings for the frontend
+ deps:
+ - task: go:mod:tidy
+ sources:
+ - "**/*.[jt]s"
+ - exclude: frontend/**/*
+ - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
+ - "**/*.go"
+ - go.mod
+ - go.sum
+ generates:
+ - frontend/bindings/**/*
+ cmds:
+ - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true -ts
+
+ generate:icons:
+ summary: Generates Windows `.ico` and Mac `.icns` files from an image
+ dir: build
+ sources:
+ - "appicon.png"
+ generates:
+ - "darwin/icons.icns"
+ - "windows/icon.ico"
+ cmds:
+ - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico
+
+ dev:frontend:
+ summary: Runs the frontend in development mode
+ dir: frontend
+ deps:
+ - task: install:frontend:deps
+ cmds:
+ - npm run dev -- --port {{.VITE_PORT}} --strictPort
+
+ update:build-assets:
+ summary: Updates the build assets
+ dir: build
+ cmds:
+ - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
diff --git a/v3/examples/notifications/build/appicon.png b/v3/examples/notifications/build/appicon.png
new file mode 100644
index 000000000..63617fe4f
Binary files /dev/null and b/v3/examples/notifications/build/appicon.png differ
diff --git a/v3/examples/notifications/build/config.yml b/v3/examples/notifications/build/config.yml
new file mode 100644
index 000000000..bc09a6d28
--- /dev/null
+++ b/v3/examples/notifications/build/config.yml
@@ -0,0 +1,62 @@
+# This file contains the configuration for this project.
+# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets.
+# Note that this will overwrite any changes you have made to the assets.
+version: '3'
+
+# This information is used to generate the build assets.
+info:
+ companyName: "My Company" # The name of the company
+ productName: "My Product" # The name of the application
+ productIdentifier: "com.mycompany.myproduct" # The unique product identifier
+ description: "A program that does X" # The application description
+ copyright: "(c) 2025, My Company" # Copyright text
+ comments: "Some Product Comments" # Comments
+ version: "v0.0.1" # The application version
+
+# Dev mode configuration
+dev_mode:
+ root_path: .
+ log_level: warn
+ debounce: 1000
+ ignore:
+ dir:
+ - .git
+ - node_modules
+ - frontend
+ - bin
+ file:
+ - .DS_Store
+ - .gitignore
+ - .gitkeep
+ watched_extension:
+ - "*.go"
+ git_ignore: true
+ executes:
+ - cmd: wails3 task common:install:frontend:deps
+ type: once
+ - cmd: wails3 task common:dev:frontend
+ type: background
+ - cmd: go mod tidy
+ type: blocking
+ - cmd: wails3 task build
+ type: blocking
+ - cmd: wails3 task run
+ type: primary
+
+# File Associations
+# More information at: https://v3.wails.io/noit/done/yet
+fileAssociations:
+# - ext: wails
+# name: Wails
+# description: Wails Application File
+# iconName: wailsFileIcon
+# role: Editor
+# - ext: jpg
+# name: JPEG
+# description: Image File
+# iconName: jpegFileIcon
+# role: Editor
+
+# Other data
+other:
+ - name: My Other Data
\ No newline at end of file
diff --git a/v3/examples/notifications/build/darwin/Info.dev.plist b/v3/examples/notifications/build/darwin/Info.dev.plist
new file mode 100644
index 000000000..3a5b9735f
--- /dev/null
+++ b/v3/examples/notifications/build/darwin/Info.dev.plist
@@ -0,0 +1,32 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ My Product
+ CFBundleExecutable
+ Notifications Demo
+ CFBundleIdentifier
+ com.wails.notifications-demo
+ CFBundleVersion
+ 0.1.0
+ CFBundleGetInfoString
+ This is a comment
+ CFBundleShortVersionString
+ 0.1.0
+ CFBundleIconFile
+ icons
+ LSMinimumSystemVersion
+ 10.15.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ © now, My Company
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
+
+
\ No newline at end of file
diff --git a/v3/examples/notifications/build/darwin/Info.plist b/v3/examples/notifications/build/darwin/Info.plist
new file mode 100644
index 000000000..464270019
--- /dev/null
+++ b/v3/examples/notifications/build/darwin/Info.plist
@@ -0,0 +1,27 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ My Product
+ CFBundleExecutable
+ Notifications Demo
+ CFBundleIdentifier
+ com.wails.notifications-demo
+ CFBundleVersion
+ 0.1.0
+ CFBundleGetInfoString
+ This is a comment
+ CFBundleShortVersionString
+ 0.1.0
+ CFBundleIconFile
+ icons
+ LSMinimumSystemVersion
+ 10.15.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ © now, My Company
+
+
\ No newline at end of file
diff --git a/v3/examples/notifications/build/darwin/Taskfile.yml b/v3/examples/notifications/build/darwin/Taskfile.yml
new file mode 100644
index 000000000..3b6a9dc99
--- /dev/null
+++ b/v3/examples/notifications/build/darwin/Taskfile.yml
@@ -0,0 +1,80 @@
+version: '3'
+
+includes:
+ common: ../Taskfile.yml
+
+tasks:
+ build:
+ summary: Creates a production build of the application
+ deps:
+ - task: common:go:mod:tidy
+ - task: common:build:frontend
+ vars:
+ BUILD_FLAGS:
+ ref: .BUILD_FLAGS
+ PRODUCTION:
+ ref: .PRODUCTION
+ - task: common:generate:icons
+ cmds:
+ - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
+ vars:
+ BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
+ env:
+ GOOS: darwin
+ CGO_ENABLED: 1
+ GOARCH: '{{.ARCH | default ARCH}}'
+ CGO_CFLAGS: "-mmacosx-version-min=10.15"
+ CGO_LDFLAGS: "-mmacosx-version-min=10.15"
+ MACOSX_DEPLOYMENT_TARGET: "10.15"
+ PRODUCTION: '{{.PRODUCTION | default "false"}}'
+
+ build:universal:
+ summary: Builds darwin universal binary (arm64 + amd64)
+ deps:
+ - task: build
+ vars:
+ ARCH: amd64
+ OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64"
+ - task: build
+ vars:
+ ARCH: arm64
+ OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
+ cmds:
+ - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
+ - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
+
+ package:
+ summary: Packages a production build of the application into a `.app` bundle
+ deps:
+ - task: build
+ vars:
+ PRODUCTION: "true"
+ cmds:
+ - task: create:app:bundle
+
+ package:universal:
+ summary: Packages darwin universal binary (arm64 + amd64)
+ deps:
+ - task: build:universal
+ cmds:
+ - task: create:app:bundle
+
+
+ create:app:bundle:
+ summary: Creates an `.app` bundle
+ cmds:
+ - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
+ - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
+ - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
+ - cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
+
+ run:
+ cmds:
+ - mkdir -p {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
+ - cp build/darwin/icons.icns {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/Resources
+ - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/MacOS
+ - cp build/darwin/Info.dev.plist {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/Info.plist
+ - codesign --force --deep --sign - {{.BIN_DIR}}/dev/{{.APP_NAME}}.app
+ - '{{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/MacOS/{{.APP_NAME}}'
diff --git a/v3/examples/notifications/build/darwin/icons.icns b/v3/examples/notifications/build/darwin/icons.icns
new file mode 100644
index 000000000..1b5bd4c86
Binary files /dev/null and b/v3/examples/notifications/build/darwin/icons.icns differ
diff --git a/v3/examples/notifications/build/linux/Taskfile.yml b/v3/examples/notifications/build/linux/Taskfile.yml
new file mode 100644
index 000000000..560cc9c92
--- /dev/null
+++ b/v3/examples/notifications/build/linux/Taskfile.yml
@@ -0,0 +1,119 @@
+version: '3'
+
+includes:
+ common: ../Taskfile.yml
+
+tasks:
+ build:
+ summary: Builds the application for Linux
+ deps:
+ - task: common:go:mod:tidy
+ - task: common:build:frontend
+ vars:
+ BUILD_FLAGS:
+ ref: .BUILD_FLAGS
+ PRODUCTION:
+ ref: .PRODUCTION
+ - task: common:generate:icons
+ cmds:
+ - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
+ vars:
+ BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
+ env:
+ GOOS: linux
+ CGO_ENABLED: 1
+ GOARCH: '{{.ARCH | default ARCH}}'
+ PRODUCTION: '{{.PRODUCTION | default "false"}}'
+
+ package:
+ summary: Packages a production build of the application for Linux
+ deps:
+ - task: build
+ vars:
+ PRODUCTION: "true"
+ cmds:
+ - task: create:appimage
+ - task: create:deb
+ - task: create:rpm
+ - task: create:aur
+
+ create:appimage:
+ summary: Creates an AppImage
+ dir: build/linux/appimage
+ deps:
+ - task: build
+ vars:
+ PRODUCTION: "true"
+ - task: generate:dotdesktop
+ cmds:
+ - cp {{.APP_BINARY}} {{.APP_NAME}}
+ - cp ../../appicon.png appicon.png
+ - wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
+ vars:
+ APP_NAME: '{{.APP_NAME}}'
+ APP_BINARY: '../../../bin/{{.APP_NAME}}'
+ ICON: '../../appicon.png'
+ DESKTOP_FILE: '../{{.APP_NAME}}.desktop'
+ OUTPUT_DIR: '../../../bin'
+
+ create:deb:
+ summary: Creates a deb package
+ deps:
+ - task: build
+ vars:
+ PRODUCTION: "true"
+ cmds:
+ - task: generate:dotdesktop
+ - task: generate:deb
+
+ create:rpm:
+ summary: Creates a rpm package
+ deps:
+ - task: build
+ vars:
+ PRODUCTION: "true"
+ cmds:
+ - task: generate:dotdesktop
+ - task: generate:rpm
+
+ create:aur:
+ summary: Creates a arch linux packager package
+ deps:
+ - task: build
+ vars:
+ PRODUCTION: "true"
+ cmds:
+ - task: generate:dotdesktop
+ - task: generate:aur
+
+ generate:deb:
+ summary: Creates a deb package
+ cmds:
+ - wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
+
+ generate:rpm:
+ summary: Creates a rpm package
+ cmds:
+ - wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
+
+ generate:aur:
+ summary: Creates a arch linux packager package
+ cmds:
+ - wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
+
+ generate:dotdesktop:
+ summary: Generates a `.desktop` file
+ dir: build
+ cmds:
+ - mkdir -p {{.ROOT_DIR}}/build/linux/appimage
+ - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
+ vars:
+ APP_NAME: '{{.APP_NAME}}'
+ EXEC: '{{.APP_NAME}}'
+ ICON: 'appicon'
+ CATEGORIES: 'Development;'
+ OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
+
+ run:
+ cmds:
+ - '{{.BIN_DIR}}/{{.APP_NAME}}'
diff --git a/v3/examples/notifications/build/linux/appimage/build.sh b/v3/examples/notifications/build/linux/appimage/build.sh
new file mode 100644
index 000000000..85901c34e
--- /dev/null
+++ b/v3/examples/notifications/build/linux/appimage/build.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# Copyright (c) 2018-Present Lea Anthony
+# SPDX-License-Identifier: MIT
+
+# Fail script on any error
+set -euxo pipefail
+
+# Define variables
+APP_DIR="${APP_NAME}.AppDir"
+
+# Create AppDir structure
+mkdir -p "${APP_DIR}/usr/bin"
+cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/"
+cp "${ICON_PATH}" "${APP_DIR}/"
+cp "${DESKTOP_FILE}" "${APP_DIR}/"
+
+if [[ $(uname -m) == *x86_64* ]]; then
+ # Download linuxdeploy and make it executable
+ wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
+ chmod +x linuxdeploy-x86_64.AppImage
+
+ # Run linuxdeploy to bundle the application
+ ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
+else
+ # Download linuxdeploy and make it executable (arm64)
+ wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage
+ chmod +x linuxdeploy-aarch64.AppImage
+
+ # Run linuxdeploy to bundle the application (arm64)
+ ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage
+fi
+
+# Rename the generated AppImage
+mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"
+
diff --git a/v3/examples/notifications/build/linux/nfpm/nfpm.yaml b/v3/examples/notifications/build/linux/nfpm/nfpm.yaml
new file mode 100644
index 000000000..c2cb7cd81
--- /dev/null
+++ b/v3/examples/notifications/build/linux/nfpm/nfpm.yaml
@@ -0,0 +1,50 @@
+# Feel free to remove those if you don't want/need to use them.
+# Make sure to check the documentation at https://nfpm.goreleaser.com
+#
+# The lines below are called `modelines`. See `:help modeline`
+
+name: "notifications"
+arch: ${GOARCH}
+platform: "linux"
+version: "0.1.0"
+section: "default"
+priority: "extra"
+maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
+description: "My Product Description"
+vendor: "My Company"
+homepage: "https://wails.io"
+license: "MIT"
+release: "1"
+
+contents:
+ - src: "./bin/notifications"
+ dst: "/usr/local/bin/notifications"
+ - src: "./build/appicon.png"
+ dst: "/usr/share/icons/hicolor/128x128/apps/notifications.png"
+ - src: "./build/linux/notifications.desktop"
+ dst: "/usr/share/applications/notifications.desktop"
+
+depends:
+ - gtk3
+ - libwebkit2gtk
+
+# replaces:
+# - foobar
+# provides:
+# - bar
+# depends:
+# - gtk3
+# - libwebkit2gtk
+# recommends:
+# - whatever
+# suggests:
+# - something-else
+# conflicts:
+# - not-foo
+# - not-bar
+# changelog: "changelog.yaml"
+# scripts:
+# preinstall: ./build/linux/nfpm/scripts/preinstall.sh
+# postinstall: ./build/linux/nfpm/scripts/postinstall.sh
+# preremove: ./build/linux/nfpm/scripts/preremove.sh
+# postremove: ./build/linux/nfpm/scripts/postremove.sh
diff --git a/v3/examples/notifications/build/linux/nfpm/scripts/postinstall.sh b/v3/examples/notifications/build/linux/nfpm/scripts/postinstall.sh
new file mode 100644
index 000000000..a9bf588e2
--- /dev/null
+++ b/v3/examples/notifications/build/linux/nfpm/scripts/postinstall.sh
@@ -0,0 +1 @@
+#!/bin/bash
diff --git a/v3/examples/notifications/build/linux/nfpm/scripts/postremove.sh b/v3/examples/notifications/build/linux/nfpm/scripts/postremove.sh
new file mode 100644
index 000000000..a9bf588e2
--- /dev/null
+++ b/v3/examples/notifications/build/linux/nfpm/scripts/postremove.sh
@@ -0,0 +1 @@
+#!/bin/bash
diff --git a/v3/examples/notifications/build/linux/nfpm/scripts/preinstall.sh b/v3/examples/notifications/build/linux/nfpm/scripts/preinstall.sh
new file mode 100644
index 000000000..a9bf588e2
--- /dev/null
+++ b/v3/examples/notifications/build/linux/nfpm/scripts/preinstall.sh
@@ -0,0 +1 @@
+#!/bin/bash
diff --git a/v3/examples/notifications/build/linux/nfpm/scripts/preremove.sh b/v3/examples/notifications/build/linux/nfpm/scripts/preremove.sh
new file mode 100644
index 000000000..a9bf588e2
--- /dev/null
+++ b/v3/examples/notifications/build/linux/nfpm/scripts/preremove.sh
@@ -0,0 +1 @@
+#!/bin/bash
diff --git a/v3/examples/notifications/build/windows/Taskfile.yml b/v3/examples/notifications/build/windows/Taskfile.yml
new file mode 100644
index 000000000..be6e4125e
--- /dev/null
+++ b/v3/examples/notifications/build/windows/Taskfile.yml
@@ -0,0 +1,63 @@
+version: '3'
+
+includes:
+ common: ../Taskfile.yml
+
+tasks:
+ build:
+ summary: Builds the application for Windows
+ deps:
+ - task: common:go:mod:tidy
+ - task: common:build:frontend
+ vars:
+ BUILD_FLAGS:
+ ref: .BUILD_FLAGS
+ PRODUCTION:
+ ref: .PRODUCTION
+ - task: common:generate:icons
+ cmds:
+ - task: generate:syso
+ - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
+ - cmd: powershell Remove-item *.syso
+ platforms: [windows]
+ - cmd: rm -f *.syso
+ platforms: [linux, darwin]
+ vars:
+ BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
+ env:
+ GOOS: windows
+ CGO_ENABLED: 0
+ GOARCH: '{{.ARCH | default ARCH}}'
+ PRODUCTION: '{{.PRODUCTION | default "false"}}'
+
+ package:
+ summary: Packages a production build of the application into a `.exe` bundle
+ cmds:
+ - task: create:nsis:installer
+
+ generate:syso:
+ summary: Generates Windows `.syso` file
+ dir: build
+ cmds:
+ - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
+ vars:
+ ARCH: '{{.ARCH | default ARCH}}'
+
+ create:nsis:installer:
+ summary: Creates an NSIS installer
+ dir: build/windows/nsis
+ deps:
+ - task: build
+ vars:
+ PRODUCTION: "true"
+ cmds:
+ # Create the Microsoft WebView2 bootstrapper if it doesn't exist
+ - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
+ - makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi
+ vars:
+ ARCH: '{{.ARCH | default ARCH}}'
+ ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
+
+ run:
+ cmds:
+ - '{{.BIN_DIR}}\\{{.APP_NAME}}.exe'
diff --git a/v3/examples/notifications/build/windows/icon.ico b/v3/examples/notifications/build/windows/icon.ico
new file mode 100644
index 000000000..bfa0690b7
Binary files /dev/null and b/v3/examples/notifications/build/windows/icon.ico differ
diff --git a/v3/examples/notifications/build/windows/info.json b/v3/examples/notifications/build/windows/info.json
new file mode 100644
index 000000000..850b2b5b0
--- /dev/null
+++ b/v3/examples/notifications/build/windows/info.json
@@ -0,0 +1,15 @@
+{
+ "fixed": {
+ "file_version": "0.1.0"
+ },
+ "info": {
+ "0000": {
+ "ProductVersion": "0.1.0",
+ "CompanyName": "My Company",
+ "FileDescription": "My Product Description",
+ "LegalCopyright": "© now, My Company",
+ "ProductName": "My Product",
+ "Comments": "This is a comment"
+ }
+ }
+}
\ No newline at end of file
diff --git a/v3/examples/notifications/build/windows/nsis/project.nsi b/v3/examples/notifications/build/windows/nsis/project.nsi
new file mode 100644
index 000000000..4cb18e04f
--- /dev/null
+++ b/v3/examples/notifications/build/windows/nsis/project.nsi
@@ -0,0 +1,112 @@
+Unicode true
+
+####
+## Please note: Template replacements don't work in this file. They are provided with default defines like
+## mentioned underneath.
+## If the keyword is not defined, "wails_tools.nsh" will populate them.
+## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually
+## from outside of Wails for debugging and development of the installer.
+##
+## For development first make a wails nsis build to populate the "wails_tools.nsh":
+## > wails build --target windows/amd64 --nsis
+## Then you can call makensis on this file with specifying the path to your binary:
+## For a AMD64 only installer:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
+## For a ARM64 only installer:
+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
+## For a installer with both architectures:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
+####
+## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
+####
+## !define INFO_PROJECTNAME "my-project" # Default "notifications"
+## !define INFO_COMPANYNAME "My Company" # Default "My Company"
+## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product"
+## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0"
+## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© now, My Company"
+###
+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+####
+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
+####
+## Include the wails tools
+####
+!include "wails_tools.nsh"
+
+# The version information for this two must consist of 4 parts
+VIProductVersion "${INFO_PRODUCTVERSION}.0"
+VIFileVersion "${INFO_PRODUCTVERSION}.0"
+
+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
+
+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
+ManifestDPIAware true
+
+!include "MUI.nsh"
+
+!define MUI_ICON "..\icon.ico"
+!define MUI_UNICON "..\icon.ico"
+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
+
+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
+!insertmacro MUI_PAGE_INSTFILES # Installing page.
+!insertmacro MUI_PAGE_FINISH # Finished installation page.
+
+!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page
+
+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
+
+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
+#!uninstfinalize 'signtool --file "%1"'
+#!finalize 'signtool --file "%1"'
+
+Name "${INFO_PRODUCTNAME}"
+OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
+InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
+ShowInstDetails show # This will always show the installation details.
+
+Function .onInit
+ !insertmacro wails.checkArchitecture
+FunctionEnd
+
+Section
+ !insertmacro wails.setShellContext
+
+ !insertmacro wails.webview2runtime
+
+ SetOutPath $INSTDIR
+
+ !insertmacro wails.files
+
+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+
+ !insertmacro wails.associateFiles
+
+ !insertmacro wails.writeUninstaller
+SectionEnd
+
+Section "uninstall"
+ !insertmacro wails.setShellContext
+
+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
+
+ RMDir /r $INSTDIR
+
+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
+
+ !insertmacro wails.unassociateFiles
+
+ !insertmacro wails.deleteUninstaller
+SectionEnd
diff --git a/v3/examples/notifications/build/windows/nsis/wails_tools.nsh b/v3/examples/notifications/build/windows/nsis/wails_tools.nsh
new file mode 100644
index 000000000..c47c784a4
--- /dev/null
+++ b/v3/examples/notifications/build/windows/nsis/wails_tools.nsh
@@ -0,0 +1,212 @@
+# DO NOT EDIT - Generated automatically by `wails build`
+
+!include "x64.nsh"
+!include "WinVer.nsh"
+!include "FileFunc.nsh"
+
+!ifndef INFO_PROJECTNAME
+ !define INFO_PROJECTNAME "notifications"
+!endif
+!ifndef INFO_COMPANYNAME
+ !define INFO_COMPANYNAME "My Company"
+!endif
+!ifndef INFO_PRODUCTNAME
+ !define INFO_PRODUCTNAME "My Product"
+!endif
+!ifndef INFO_PRODUCTVERSION
+ !define INFO_PRODUCTVERSION "0.1.0"
+!endif
+!ifndef INFO_COPYRIGHT
+ !define INFO_COPYRIGHT "© now, My Company"
+!endif
+!ifndef PRODUCT_EXECUTABLE
+ !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
+!endif
+!ifndef UNINST_KEY_NAME
+ !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+!endif
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
+
+!ifndef REQUEST_EXECUTION_LEVEL
+ !define REQUEST_EXECUTION_LEVEL "admin"
+!endif
+
+RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
+
+!ifdef ARG_WAILS_AMD64_BINARY
+ !define SUPPORTS_AMD64
+!endif
+
+!ifdef ARG_WAILS_ARM64_BINARY
+ !define SUPPORTS_ARM64
+!endif
+
+!ifdef SUPPORTS_AMD64
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "amd64_arm64"
+ !else
+ !define ARCH "amd64"
+ !endif
+!else
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "arm64"
+ !else
+ !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
+ !endif
+!endif
+
+!macro wails.checkArchitecture
+ !ifndef WAILS_WIN10_REQUIRED
+ !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
+ !endif
+
+ !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
+ !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
+ !endif
+
+ ${If} ${AtLeastWin10}
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ IfSilent silentArch notSilentArch
+ silentArch:
+ SetErrorLevel 65
+ Abort
+ notSilentArch:
+ MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
+ Quit
+ ${else}
+ IfSilent silentWin notSilentWin
+ silentWin:
+ SetErrorLevel 64
+ Abort
+ notSilentWin:
+ MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
+ Quit
+ ${EndIf}
+
+ ok:
+!macroend
+
+!macro wails.files
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
+ ${EndIf}
+ !endif
+!macroend
+
+!macro wails.writeUninstaller
+ WriteUninstaller "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+ WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
+
+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
+ IntFmt $0 "0x%08X" $0
+ WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
+!macroend
+
+!macro wails.deleteUninstaller
+ Delete "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ DeleteRegKey HKLM "${UNINST_KEY}"
+!macroend
+
+!macro wails.setShellContext
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
+ SetShellVarContext all
+ ${else}
+ SetShellVarContext current
+ ${EndIf}
+!macroend
+
+# Install webview2 by launching the bootstrapper
+# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
+!macro wails.webview2runtime
+ !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
+ !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
+ !endif
+
+ SetRegView 64
+ # If the admin key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
+ # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+ ${EndIf}
+
+ SetDetailsPrint both
+ DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
+ SetDetailsPrint listonly
+
+ InitPluginsDir
+ CreateDirectory "$pluginsdir\webview2bootstrapper"
+ SetOutPath "$pluginsdir\webview2bootstrapper"
+ File "MicrosoftEdgeWebview2Setup.exe"
+ ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
+
+ SetDetailsPrint both
+ ok:
+!macroend
+
+# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
+!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
+!macroend
+
+!macro APP_UNASSOCIATE EXT FILECLASS
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
+
+ DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
+!macroend
+
+!macro wails.associateFiles
+ ; Create file associations
+
+!macroend
+
+!macro wails.unassociateFiles
+ ; Delete app associations
+
+!macroend
\ No newline at end of file
diff --git a/v3/examples/notifications/build/windows/wails.exe.manifest b/v3/examples/notifications/build/windows/wails.exe.manifest
new file mode 100644
index 000000000..0299e62ca
--- /dev/null
+++ b/v3/examples/notifications/build/windows/wails.exe.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
\ No newline at end of file
diff --git a/v3/examples/notifications/frontend/Inter Font License.txt b/v3/examples/notifications/frontend/Inter Font License.txt
new file mode 100644
index 000000000..b525cbf3a
--- /dev/null
+++ b/v3/examples/notifications/frontend/Inter Font License.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/index.ts b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/index.ts
new file mode 100644
index 000000000..bbdce6579
--- /dev/null
+++ b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/index.ts
@@ -0,0 +1,13 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+import * as Service from "./service.js";
+export {
+ Service
+};
+
+export {
+ NotificationAction,
+ NotificationCategory,
+ NotificationOptions
+} from "./models.js";
diff --git a/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/models.ts b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/models.ts
new file mode 100644
index 000000000..d7f48edfe
--- /dev/null
+++ b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/models.ts
@@ -0,0 +1,107 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore: Unused imports
+import { Create as $Create } from "@wailsio/runtime";
+
+/**
+ * NotificationAction represents an action button for a notification.
+ */
+export class NotificationAction {
+ "id"?: string;
+ "title"?: string;
+
+ /**
+ * (macOS-specific)
+ */
+ "destructive"?: boolean;
+
+ /** Creates a new NotificationAction instance. */
+ constructor($$source: Partial = {}) {
+
+ Object.assign(this, $$source);
+ }
+
+ /**
+ * Creates a new NotificationAction instance from a string or object.
+ */
+ static createFrom($$source: any = {}): NotificationAction {
+ let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
+ return new NotificationAction($$parsedSource as Partial);
+ }
+}
+
+/**
+ * NotificationCategory groups actions for notifications.
+ */
+export class NotificationCategory {
+ "id"?: string;
+ "actions"?: NotificationAction[];
+ "hasReplyField"?: boolean;
+ "replyPlaceholder"?: string;
+ "replyButtonTitle"?: string;
+
+ /** Creates a new NotificationCategory instance. */
+ constructor($$source: Partial = {}) {
+
+ Object.assign(this, $$source);
+ }
+
+ /**
+ * Creates a new NotificationCategory instance from a string or object.
+ */
+ static createFrom($$source: any = {}): NotificationCategory {
+ const $$createField1_0 = $$createType1;
+ let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
+ if ("actions" in $$parsedSource) {
+ $$parsedSource["actions"] = $$createField1_0($$parsedSource["actions"]);
+ }
+ return new NotificationCategory($$parsedSource as Partial);
+ }
+}
+
+/**
+ * NotificationOptions contains configuration for a notification
+ */
+export class NotificationOptions {
+ "id": string;
+ "title": string;
+
+ /**
+ * (macOS and Linux only)
+ */
+ "subtitle"?: string;
+ "body"?: string;
+ "categoryId"?: string;
+ "data"?: { [_: string]: any };
+
+ /** Creates a new NotificationOptions instance. */
+ constructor($$source: Partial = {}) {
+ if (!("id" in $$source)) {
+ this["id"] = "";
+ }
+ if (!("title" in $$source)) {
+ this["title"] = "";
+ }
+
+ Object.assign(this, $$source);
+ }
+
+ /**
+ * Creates a new NotificationOptions instance from a string or object.
+ */
+ static createFrom($$source: any = {}): NotificationOptions {
+ const $$createField5_0 = $$createType2;
+ let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
+ if ("data" in $$parsedSource) {
+ $$parsedSource["data"] = $$createField5_0($$parsedSource["data"]);
+ }
+ return new NotificationOptions($$parsedSource as Partial);
+ }
+}
+
+// Private type creation functions
+const $$createType0 = NotificationAction.createFrom;
+const $$createType1 = $Create.Array($$createType0);
+const $$createType2 = $Create.Map($Create.Any, $Create.Any);
diff --git a/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/service.ts b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/service.ts
new file mode 100644
index 000000000..28f1cb3b2
--- /dev/null
+++ b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/service.ts
@@ -0,0 +1,62 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+/**
+ * Service represents the notifications service
+ * @module
+ */
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore: Unused imports
+import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore: Unused imports
+import * as $models from "./models.js";
+
+export function CheckNotificationAuthorization(): $CancellablePromise {
+ return $Call.ByID(2789931702);
+}
+
+export function RegisterNotificationCategory(category: $models.NotificationCategory): $CancellablePromise {
+ return $Call.ByID(2679064664, category);
+}
+
+export function RemoveAllDeliveredNotifications(): $CancellablePromise {
+ return $Call.ByID(384520397);
+}
+
+export function RemoveAllPendingNotifications(): $CancellablePromise {
+ return $Call.ByID(1423986276);
+}
+
+export function RemoveDeliveredNotification(identifier: string): $CancellablePromise {
+ return $Call.ByID(149440045, identifier);
+}
+
+export function RemoveNotification(identifier: string): $CancellablePromise {
+ return $Call.ByID(3702062929, identifier);
+}
+
+export function RemoveNotificationCategory(categoryID: string): $CancellablePromise {
+ return $Call.ByID(229511469, categoryID);
+}
+
+export function RemovePendingNotification(identifier: string): $CancellablePromise {
+ return $Call.ByID(3872412470, identifier);
+}
+
+/**
+ * Public methods that delegate to the implementation.
+ */
+export function RequestNotificationAuthorization(): $CancellablePromise {
+ return $Call.ByID(729898933);
+}
+
+export function SendNotification(options: $models.NotificationOptions): $CancellablePromise {
+ return $Call.ByID(2246903123, options);
+}
+
+export function SendNotificationWithActions(options: $models.NotificationOptions): $CancellablePromise {
+ return $Call.ByID(1615199806, options);
+}
diff --git a/v3/examples/notifications/frontend/index.html b/v3/examples/notifications/frontend/index.html
new file mode 100644
index 000000000..ae067c8a2
--- /dev/null
+++ b/v3/examples/notifications/frontend/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ Wails App
+
+
+
+
+
Wails + Typescript + Desktop Notifications
+
Send notifications 👇
+
+
+
+
+
+
+
+
+
diff --git a/v3/examples/notifications/frontend/package-lock.json b/v3/examples/notifications/frontend/package-lock.json
new file mode 100644
index 000000000..cdd50c49d
--- /dev/null
+++ b/v3/examples/notifications/frontend/package-lock.json
@@ -0,0 +1,935 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "devDependencies": {
+ "@wailsio/runtime": "latest",
+ "typescript": "^4.9.3",
+ "vite": "^5.0.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz",
+ "integrity": "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz",
+ "integrity": "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.37.0.tgz",
+ "integrity": "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.37.0.tgz",
+ "integrity": "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.37.0.tgz",
+ "integrity": "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.37.0.tgz",
+ "integrity": "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.37.0.tgz",
+ "integrity": "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.37.0.tgz",
+ "integrity": "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.37.0.tgz",
+ "integrity": "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.37.0.tgz",
+ "integrity": "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.37.0.tgz",
+ "integrity": "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.37.0.tgz",
+ "integrity": "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.37.0.tgz",
+ "integrity": "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.37.0.tgz",
+ "integrity": "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.37.0.tgz",
+ "integrity": "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.37.0.tgz",
+ "integrity": "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.37.0.tgz",
+ "integrity": "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.37.0.tgz",
+ "integrity": "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.37.0.tgz",
+ "integrity": "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz",
+ "integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@wailsio/runtime": {
+ "version": "3.0.0-alpha.66",
+ "resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.66.tgz",
+ "integrity": "sha512-ENLu8rn1griL1gFHJqkq1i+BVxrrA0JPJHYneUJYuf/s54kjuQViW0RKDEe/WTDo56ABpfykrd/T8OYpPUyXUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
+ "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.37.0.tgz",
+ "integrity": "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.37.0",
+ "@rollup/rollup-android-arm64": "4.37.0",
+ "@rollup/rollup-darwin-arm64": "4.37.0",
+ "@rollup/rollup-darwin-x64": "4.37.0",
+ "@rollup/rollup-freebsd-arm64": "4.37.0",
+ "@rollup/rollup-freebsd-x64": "4.37.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.37.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.37.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.37.0",
+ "@rollup/rollup-linux-arm64-musl": "4.37.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.37.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.37.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.37.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.37.0",
+ "@rollup/rollup-linux-x64-gnu": "4.37.0",
+ "@rollup/rollup-linux-x64-musl": "4.37.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.37.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.37.0",
+ "@rollup/rollup-win32-x64-msvc": "4.37.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.15",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",
+ "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/v3/examples/notifications/frontend/package.json b/v3/examples/notifications/frontend/package.json
new file mode 100644
index 000000000..4d675f189
--- /dev/null
+++ b/v3/examples/notifications/frontend/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build:dev": "tsc && vite build --minify false --mode development",
+ "build": "tsc && vite build --mode production",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "typescript": "^4.9.3",
+ "vite": "^5.0.0",
+ "@wailsio/runtime": "latest"
+ }
+}
\ No newline at end of file
diff --git a/v3/examples/notifications/frontend/public/Inter-Medium.ttf b/v3/examples/notifications/frontend/public/Inter-Medium.ttf
new file mode 100644
index 000000000..a01f3777a
Binary files /dev/null and b/v3/examples/notifications/frontend/public/Inter-Medium.ttf differ
diff --git a/v3/examples/notifications/frontend/public/style.css b/v3/examples/notifications/frontend/public/style.css
new file mode 100644
index 000000000..074717bca
--- /dev/null
+++ b/v3/examples/notifications/frontend/public/style.css
@@ -0,0 +1,131 @@
+:root {
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
+ "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: rgba(27, 38, 54, 1);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+}
+
+* {
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+}
+
+@font-face {
+ font-family: "Inter";
+ font-style: normal;
+ font-weight: 400;
+ src: local(""),
+ url("./Inter-Medium.ttf") format("truetype");
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+
+a:hover {
+ color: #535bf2;
+}
+
+.controls {
+ display: flex;
+ gap: 1em;
+}
+
+button {
+ height: 30px;
+ line-height: 30px;
+ border-radius: 3px;
+ border: none;
+ margin: 0 0 0 20px;
+ padding: 0 8px;
+ cursor: pointer;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ place-content: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 3em;
+}
+
+h1, h3 {
+ line-height: 1.1;
+ text-align: center;
+}
+
+#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+}
+
+.logo:hover {
+ filter: drop-shadow(0 0 2em #e80000aa);
+}
+
+.logo.vanilla:hover {
+ filter: drop-shadow(0 0 2em #f7df1eaa);
+}
+
+.footer {
+ margin-top: 1rem;
+ align-content: center;
+ text-align: center;
+}
+
+.footer table {
+ font-size: 12px;
+ border-collapse: collapse;
+ margin: 0 auto;
+}
+
+.footer table, th, td {
+ border: 1px solid #ddd;
+ padding: 0.5em;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+
+ a:hover {
+ color: #747bff;
+ }
+
+ button {
+ background-color: #f9f9f9;
+ }
+}
\ No newline at end of file
diff --git a/v3/examples/notifications/frontend/public/typescript.svg b/v3/examples/notifications/frontend/public/typescript.svg
new file mode 100644
index 000000000..d91c910cc
--- /dev/null
+++ b/v3/examples/notifications/frontend/public/typescript.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/v3/examples/notifications/frontend/public/wails.png b/v3/examples/notifications/frontend/public/wails.png
new file mode 100644
index 000000000..8bdf42483
Binary files /dev/null and b/v3/examples/notifications/frontend/public/wails.png differ
diff --git a/v3/examples/notifications/frontend/src/main.ts b/v3/examples/notifications/frontend/src/main.ts
new file mode 100644
index 000000000..437fb8c94
--- /dev/null
+++ b/v3/examples/notifications/frontend/src/main.ts
@@ -0,0 +1,95 @@
+import { Events } from "@wailsio/runtime";
+import * as Notifications from "../bindings/github.com/wailsapp/wails/v3/pkg/services/notifications";
+
+document.querySelector("#basic")?.addEventListener("click", async () => {
+ try {
+ const authorized = await Notifications.Service.CheckNotificationAuthorization();
+ if (authorized) {
+ await Notifications.Service.SendNotification({
+ id: crypto.randomUUID(),
+ title: "Notification Title",
+ subtitle: "Subtitle on macOS and Linux",
+ body: "Body text of notification.",
+ data: {
+ "user-id": "user-123",
+ "message-id": "msg-123",
+ "timestamp": Date.now(),
+ },
+ });
+ } else {
+ console.warn("Notifications are not authorized.\n You can attempt to request again or let the user know in the UI.\n");
+ }
+ } catch (error) {
+ console.error(error);
+ }
+});
+document.querySelector("#complex")?.addEventListener("click", async () => {
+ try {
+ const authorized = await Notifications.Service.CheckNotificationAuthorization();
+ if (authorized) {
+ const CategoryID = "frontend-notification-id";
+
+ await Notifications.Service.RegisterNotificationCategory({
+ id: CategoryID,
+ actions: [
+ { id: "VIEW", title: "View" },
+ { id: "MARK_READ", title: "Mark as read" },
+ { id: "DELETE", title: "Delete", destructive: true },
+ ],
+ hasReplyField: true,
+ replyPlaceholder: "Message...",
+ replyButtonTitle: "Reply",
+ });
+
+ await Notifications.Service.SendNotificationWithActions({
+ id: crypto.randomUUID(),
+ title: "Notification Title",
+ subtitle: "Subtitle on macOS and Linux",
+ body: "Body text of notification.",
+ categoryId: CategoryID,
+ data: {
+ "user-id": "user-123",
+ "message-id": "msg-123",
+ "timestamp": Date.now(),
+ },
+ });
+ } else {
+ console.warn("Notifications are not authorized.\n You can attempt to request again or let the user know in the UI.\n");
+ }
+ } catch (error) {
+ console.error(error);
+ }
+});
+
+const unlisten = Events.On("notification:action", (response) => {
+ console.info(`Recieved a ${response.name} event`);
+ const { userInfo, ...base } = response.data[0];
+ console.info("Notification Response:");
+ console.table(base);
+ console.info("Notification Response Metadata:");
+ console.table(userInfo);
+ const table = `
+ Notification Response
+
+
+ ${Object.keys(base).map(key => `${key} | `).join("")}
+
+
+ ${Object.values(base).map(value => `${value} | `).join("")}
+
+
+ Notification Metadata
+
+
+ ${Object.keys(userInfo).map(key => `${key} | `).join("")}
+
+
+ ${Object.values(userInfo).map(value => `${value} | `).join("")}
+
+
+ `;
+ const footer = document.querySelector("#response");
+ if (footer) footer.innerHTML = table;
+});
+
+window.onbeforeunload = () => unlisten();
\ No newline at end of file
diff --git a/v3/examples/notifications/frontend/src/vite-env.d.ts b/v3/examples/notifications/frontend/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/v3/examples/notifications/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/v3/examples/notifications/frontend/tsconfig.json b/v3/examples/notifications/frontend/tsconfig.json
new file mode 100644
index 000000000..c267ecf24
--- /dev/null
+++ b/v3/examples/notifications/frontend/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "Node",
+ "strict": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "noEmit": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": false,
+ "noImplicitAny": false,
+ "noImplicitReturns": true,
+ "skipLibCheck": true
+ },
+ "include": ["src"]
+}
diff --git a/v3/examples/notifications/go.mod b/v3/examples/notifications/go.mod
new file mode 100644
index 000000000..39537e938
--- /dev/null
+++ b/v3/examples/notifications/go.mod
@@ -0,0 +1,53 @@
+module notifications
+
+go 1.24.0
+
+toolchain go1.24.1
+
+require github.com/wailsapp/wails/v3 v3.0.0-dev
+
+require (
+ dario.cat/mergo v1.0.1 // indirect
+ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/ProtonMail/go-crypto v1.1.6 // indirect
+ github.com/adrg/xdg v0.5.3 // indirect
+ github.com/bep/debounce v1.2.1 // indirect
+ github.com/cloudflare/circl v1.6.0 // indirect
+ github.com/cyphar/filepath-securejoin v0.4.1 // indirect
+ github.com/ebitengine/purego v0.8.2 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.6.2 // indirect
+ github.com/go-git/go-git/v5 v5.13.2 // indirect
+ github.com/go-ole/go-ole v1.3.0 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
+ github.com/kevinburke/ssh_config v1.2.0 // indirect
+ github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
+ github.com/leaanthony/u v1.1.1 // indirect
+ github.com/lmittmann/tint v1.0.7 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/pjbgf/sha1cd v0.3.2 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/samber/lo v1.49.1 // indirect
+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
+ github.com/skeema/knownhosts v1.3.1 // indirect
+ github.com/wailsapp/go-webview2 v1.0.21 // indirect
+ github.com/wailsapp/mimetype v1.4.1 // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ golang.org/x/crypto v0.36.0 // indirect
+ golang.org/x/net v0.37.0 // indirect
+ golang.org/x/sys v0.31.0 // indirect
+ golang.org/x/text v0.23.0 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
+)
+
+replace github.com/wailsapp/wails/v3 => ../wails/v3
diff --git a/v3/examples/notifications/main.go b/v3/examples/notifications/main.go
new file mode 100644
index 000000000..264e7273e
--- /dev/null
+++ b/v3/examples/notifications/main.go
@@ -0,0 +1,154 @@
+package main
+
+import (
+ "embed"
+ _ "embed"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/wailsapp/wails/v3/pkg/application"
+ "github.com/wailsapp/wails/v3/pkg/events"
+ "github.com/wailsapp/wails/v3/pkg/services/notifications"
+)
+
+// Wails uses Go's `embed` package to embed the frontend files into the binary.
+// Any files in the frontend/dist folder will be embedded into the binary and
+// made available to the frontend.
+// See https://pkg.go.dev/embed for more information.
+
+//go:embed all:frontend/dist
+var assets embed.FS
+
+// main function serves as the application's entry point. It initializes the application, creates a window,
+// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and
+// logs any error that might occur.
+func main() {
+ // Create a new Notification Service
+ ns := notifications.New()
+
+ // Create a new Wails application by providing the necessary options.
+ // Variables 'Name' and 'Description' are for application metadata.
+ // 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files.
+ // 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances.
+ // 'Mac' options tailor the application when running an macOS.
+ app := application.New(application.Options{
+ Name: "Notifications Demo",
+ Description: "A demo of using desktop notifications with Wails",
+ Services: []application.Service{
+ application.NewService(ns),
+ },
+ Assets: application.AssetOptions{
+ Handler: application.AssetFileServerFS(assets),
+ },
+ Mac: application.MacOptions{
+ ApplicationShouldTerminateAfterLastWindowClosed: true,
+ },
+ })
+
+ // Create a new window with the necessary options.
+ // 'Title' is the title of the window.
+ // 'Mac' options tailor the window when running on macOS.
+ // 'BackgroundColour' is the background colour of the window.
+ // 'URL' is the URL that will be loaded into the webview.
+ app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
+ Title: "Window 1",
+ Mac: application.MacWindow{
+ InvisibleTitleBarHeight: 50,
+ Backdrop: application.MacBackdropTranslucent,
+ TitleBar: application.MacTitleBarHiddenInset,
+ },
+ BackgroundColour: application.NewRGB(27, 38, 54),
+ URL: "/",
+ })
+
+ app.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
+ // Create a goroutine that spawns desktop notifications from Go
+ go func() {
+ var authorized bool
+ var err error
+ authorized, err = ns.CheckNotificationAuthorization()
+ if err != nil {
+ println(fmt.Errorf("checking app notification authorization failed: %s", err))
+ }
+
+ if !authorized {
+ authorized, err = ns.RequestNotificationAuthorization()
+ if err != nil {
+ println(fmt.Errorf("requesting app notification authorization failed: %s", err))
+ }
+ }
+
+ if authorized {
+ ns.OnNotificationResponse(func(result notifications.NotificationResult) {
+ if result.Error != nil {
+ println(fmt.Errorf("parsing notification result failed: %s", result.Error))
+ } else {
+ fmt.Printf("Response: %+v\n", result.Response)
+ println("Sending response to frontend...")
+ app.EmitEvent("notification:action", result.Response)
+ }
+ })
+
+ err = ns.SendNotification(notifications.NotificationOptions{
+ ID: "uuid-basic-1",
+ Title: "Notification Title",
+ Subtitle: "Subtitle on macOS and Linux",
+ Body: "Body text of notification.",
+ Data: map[string]interface{}{
+ "user-id": "user-123",
+ "message-id": "msg-123",
+ "timestamp": time.Now().Unix(),
+ },
+ })
+ if err != nil {
+ println(fmt.Errorf("sending basic notification failed: %s", err))
+ }
+
+ // Delay before sending next notification
+ time.Sleep(time.Second * 2)
+
+ const CategoryID = "backend-notification-id"
+
+ err = ns.RegisterNotificationCategory(notifications.NotificationCategory{
+ ID: CategoryID,
+ Actions: []notifications.NotificationAction{
+ {ID: "VIEW", Title: "View"},
+ {ID: "MARK_READ", Title: "Mark as read"},
+ {ID: "DELETE", Title: "Delete", Destructive: true},
+ },
+ HasReplyField: true,
+ ReplyPlaceholder: "Message...",
+ ReplyButtonTitle: "Reply",
+ })
+ if err != nil {
+ println(fmt.Errorf("creating notification category failed: %s", err))
+ }
+
+ err = ns.SendNotificationWithActions(notifications.NotificationOptions{
+ ID: "uuid-with-actions-1",
+ Title: "Actions Notification Title",
+ Subtitle: "Subtitle on macOS and Linux",
+ Body: "Body text of notification with actions.",
+ CategoryID: CategoryID,
+ Data: map[string]interface{}{
+ "user-id": "user-123",
+ "message-id": "msg-123",
+ "timestamp": time.Now().Unix(),
+ },
+ })
+ if err != nil {
+ println(fmt.Errorf("sending notification with actions failed: %s", err))
+ }
+ }
+ }()
+ })
+
+ // Run the application. This blocks until the application has been exited.
+ err := app.Run()
+
+ // If an error occurred while running the application, log it and exit.
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/v3/go.mod b/v3/go.mod
index 6f80abb19..947f0495f 100644
--- a/v3/go.mod
+++ b/v3/go.mod
@@ -3,6 +3,7 @@ module github.com/wailsapp/wails/v3
go 1.24.0
require (
+ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3
github.com/Masterminds/semver v1.5.0
github.com/adrg/xdg v0.5.3
github.com/atterpac/refresh v0.8.6
diff --git a/v3/go.sum b/v3/go.sum
index 07fc3d87a..2c07fe6c8 100644
--- a/v3/go.sum
+++ b/v3/go.sum
@@ -8,6 +8,8 @@ atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
+git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go
index 21c0792cc..b3a8f6f34 100644
--- a/v3/pkg/application/application.go
+++ b/v3/pkg/application/application.go
@@ -345,6 +345,10 @@ type App struct {
singleInstanceManager *singleInstanceManager
}
+func (a *App) Config() Options {
+ return a.options
+}
+
func (a *App) handleWarning(msg string) {
if a.options.WarningHandler != nil {
a.options.WarningHandler(msg)
diff --git a/v3/pkg/services/notifications/notifications.go b/v3/pkg/services/notifications/notifications.go
new file mode 100644
index 000000000..1a33d6d56
--- /dev/null
+++ b/v3/pkg/services/notifications/notifications.go
@@ -0,0 +1,216 @@
+// Package notifications provides cross-platform notification capabilities for desktop applications.
+// It supports macOS, Windows, and Linux with a consistent API while handling platform-specific
+// differences internally. Key features include:
+// - Basic notifications with title, subtitle, and body
+// - Interactive notifications with buttons and actions
+// - Notification categories for reusing configurations
+// - User feedback handling with a unified callback system
+//
+// Platform-specific notes:
+// - macOS: Requires a properly bundled and signed application
+// - Windows: Uses Windows Toast notifications
+// - Linux: Uses D-Bus and does not support text inputs
+package notifications
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ "github.com/wailsapp/wails/v3/pkg/application"
+)
+
+type platformNotifier interface {
+ // Lifecycle methods
+ Startup(ctx context.Context, options application.ServiceOptions) error
+ Shutdown() error
+
+ // Core notification methods
+ RequestNotificationAuthorization() (bool, error)
+ CheckNotificationAuthorization() (bool, error)
+ SendNotification(options NotificationOptions) error
+ SendNotificationWithActions(options NotificationOptions) error
+
+ // Category management
+ RegisterNotificationCategory(category NotificationCategory) error
+ RemoveNotificationCategory(categoryID string) error
+
+ // Notification management
+ RemoveAllPendingNotifications() error
+ RemovePendingNotification(identifier string) error
+ RemoveAllDeliveredNotifications() error
+ RemoveDeliveredNotification(identifier string) error
+ RemoveNotification(identifier string) error
+}
+
+// Service represents the notifications service
+type Service struct {
+ impl platformNotifier
+
+ // notificationResponseCallback is called when a notification result is received.
+ // Only one callback can be assigned at a time.
+ notificationResultCallback func(result NotificationResult)
+
+ callbackLock sync.RWMutex
+}
+
+var (
+ notificationServiceOnce sync.Once
+ NotificationService *Service
+ notificationServiceLock sync.RWMutex
+)
+
+// NotificationAction represents an action button for a notification.
+type NotificationAction struct {
+ ID string `json:"id,omitempty"`
+ Title string `json:"title,omitempty"`
+ Destructive bool `json:"destructive,omitempty"` // (macOS-specific)
+}
+
+// NotificationCategory groups actions for notifications.
+type NotificationCategory struct {
+ ID string `json:"id,omitempty"`
+ Actions []NotificationAction `json:"actions,omitempty"`
+ HasReplyField bool `json:"hasReplyField,omitempty"`
+ ReplyPlaceholder string `json:"replyPlaceholder,omitempty"`
+ ReplyButtonTitle string `json:"replyButtonTitle,omitempty"`
+}
+
+// NotificationOptions contains configuration for a notification
+type NotificationOptions struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ Subtitle string `json:"subtitle,omitempty"` // (macOS and Linux only)
+ Body string `json:"body,omitempty"`
+ CategoryID string `json:"categoryId,omitempty"`
+ Data map[string]interface{} `json:"data,omitempty"`
+}
+
+const DefaultActionIdentifier = "DEFAULT_ACTION"
+
+// NotificationResponse represents the response sent by interacting with a notification.
+type NotificationResponse struct {
+ ID string `json:"id,omitempty"`
+ ActionIdentifier string `json:"actionIdentifier,omitempty"`
+ CategoryID string `json:"categoryIdentifier,omitempty"`
+ Title string `json:"title,omitempty"`
+ Subtitle string `json:"subtitle,omitempty"` // (macOS and Linux only)
+ Body string `json:"body,omitempty"`
+ UserText string `json:"userText,omitempty"`
+ UserInfo map[string]interface{} `json:"userInfo,omitempty"`
+}
+
+// NotificationResult represents the result of a notification response,
+// returning the response or any errors that occurred.
+type NotificationResult struct {
+ Response NotificationResponse
+ Error error
+}
+
+// ServiceName returns the name of the service.
+func (ns *Service) ServiceName() string {
+ return "github.com/wailsapp/wails/v3/services/notifications"
+}
+
+// OnNotificationResponse registers a callback function that will be called when
+// a notification response is received from the user.
+//
+//wails:ignore
+func (ns *Service) OnNotificationResponse(callback func(result NotificationResult)) {
+ ns.callbackLock.Lock()
+ defer ns.callbackLock.Unlock()
+
+ ns.notificationResultCallback = callback
+}
+
+// handleNotificationResponse is an internal method to handle notification responses
+// and invoke the registered callback if one exists.
+func (ns *Service) handleNotificationResult(result NotificationResult) {
+ ns.callbackLock.RLock()
+ callback := ns.notificationResultCallback
+ ns.callbackLock.RUnlock()
+
+ if callback != nil {
+ callback(result)
+ }
+}
+
+// ServiceStartup is called when the service is loaded.
+func (ns *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
+ return ns.impl.Startup(ctx, options)
+}
+
+// ServiceShutdown is called when the service is unloaded.
+func (ns *Service) ServiceShutdown() error {
+ return ns.impl.Shutdown()
+}
+
+// Public methods that delegate to the implementation.
+func (ns *Service) RequestNotificationAuthorization() (bool, error) {
+ return ns.impl.RequestNotificationAuthorization()
+}
+
+func (ns *Service) CheckNotificationAuthorization() (bool, error) {
+ return ns.impl.CheckNotificationAuthorization()
+}
+
+func (ns *Service) SendNotification(options NotificationOptions) error {
+ if err := validateNotificationOptions(options); err != nil {
+ return err
+ }
+ return ns.impl.SendNotification(options)
+}
+
+func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
+ if err := validateNotificationOptions(options); err != nil {
+ return err
+ }
+ return ns.impl.SendNotificationWithActions(options)
+}
+
+func (ns *Service) RegisterNotificationCategory(category NotificationCategory) error {
+ return ns.impl.RegisterNotificationCategory(category)
+}
+
+func (ns *Service) RemoveNotificationCategory(categoryID string) error {
+ return ns.impl.RemoveNotificationCategory(categoryID)
+}
+
+func (ns *Service) RemoveAllPendingNotifications() error {
+ return ns.impl.RemoveAllPendingNotifications()
+}
+
+func (ns *Service) RemovePendingNotification(identifier string) error {
+ return ns.impl.RemovePendingNotification(identifier)
+}
+
+func (ns *Service) RemoveAllDeliveredNotifications() error {
+ return ns.impl.RemoveAllDeliveredNotifications()
+}
+
+func (ns *Service) RemoveDeliveredNotification(identifier string) error {
+ return ns.impl.RemoveDeliveredNotification(identifier)
+}
+
+func (ns *Service) RemoveNotification(identifier string) error {
+ return ns.impl.RemoveNotification(identifier)
+}
+
+func getNotificationService() *Service {
+ notificationServiceLock.RLock()
+ defer notificationServiceLock.RUnlock()
+ return NotificationService
+}
+
+// validateNotificationOptions validates an ID and Title are provided for notifications.
+func validateNotificationOptions(options NotificationOptions) error {
+ if options.ID == "" {
+ return fmt.Errorf("notification ID cannot be empty")
+ }
+
+ if options.Title == "" {
+ return fmt.Errorf("notification title cannot be empty")
+ }
+
+ return nil
+}
diff --git a/v3/pkg/services/notifications/notifications_darwin.go b/v3/pkg/services/notifications/notifications_darwin.go
new file mode 100644
index 000000000..2c8f33d15
--- /dev/null
+++ b/v3/pkg/services/notifications/notifications_darwin.go
@@ -0,0 +1,423 @@
+//go:build darwin
+
+package notifications
+
+/*
+#cgo CFLAGS:-x objective-c
+#cgo LDFLAGS: -framework Foundation -framework Cocoa
+
+#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
+#cgo LDFLAGS: -framework UserNotifications
+#endif
+
+#import "./notifications_darwin.h"
+*/
+import "C"
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sync"
+ "time"
+ "unsafe"
+
+ "github.com/wailsapp/wails/v3/pkg/application"
+)
+
+type darwinNotifier struct {
+ channels map[int]chan notificationChannel
+ channelsLock sync.Mutex
+ nextChannelID int
+}
+
+type notificationChannel struct {
+ Success bool
+ Error error
+}
+
+type ChannelHandler interface {
+ GetChannel(id int) (chan notificationChannel, bool)
+}
+
+const AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier"
+
+// Creates a new Notifications Service.
+// Your app must be packaged and signed for this feature to work.
+func New() *Service {
+ notificationServiceOnce.Do(func() {
+ impl := &darwinNotifier{
+ channels: make(map[int]chan notificationChannel),
+ nextChannelID: 0,
+ }
+
+ NotificationService = &Service{
+ impl: impl,
+ }
+ })
+
+ return NotificationService
+}
+
+func (dn *darwinNotifier) Startup(ctx context.Context, options application.ServiceOptions) error {
+ if !isNotificationAvailable() {
+ return fmt.Errorf("notifications are not available on this system")
+ }
+ if !checkBundleIdentifier() {
+ return fmt.Errorf("notifications require a valid bundle identifier")
+ }
+ return nil
+}
+
+func (dn *darwinNotifier) Shutdown() error {
+ return nil
+}
+
+// isNotificationAvailable checks if notifications are available on the system.
+func isNotificationAvailable() bool {
+ return bool(C.isNotificationAvailable())
+}
+
+func checkBundleIdentifier() bool {
+ return bool(C.checkBundleIdentifier())
+}
+
+// RequestNotificationAuthorization requests permission for notifications.
+// Default timeout is 3 minutes
+func (dn *darwinNotifier) RequestNotificationAuthorization() (bool, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
+ defer cancel()
+
+ id, resultCh := dn.registerChannel()
+
+ C.requestNotificationAuthorization(C.int(id))
+
+ select {
+ case result := <-resultCh:
+ return result.Success, result.Error
+ case <-ctx.Done():
+ dn.cleanupChannel(id)
+ return false, fmt.Errorf("notification authorization timed out after 3 minutes: %w", ctx.Err())
+ }
+}
+
+// CheckNotificationAuthorization checks current notification permission status.
+func (dn *darwinNotifier) CheckNotificationAuthorization() (bool, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer cancel()
+
+ id, resultCh := dn.registerChannel()
+
+ C.checkNotificationAuthorization(C.int(id))
+
+ select {
+ case result := <-resultCh:
+ return result.Success, result.Error
+ case <-ctx.Done():
+ dn.cleanupChannel(id)
+ return false, fmt.Errorf("notification authorization timed out after 15s: %w", ctx.Err())
+ }
+}
+
+// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
+func (dn *darwinNotifier) SendNotification(options NotificationOptions) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ cIdentifier := C.CString(options.ID)
+ cTitle := C.CString(options.Title)
+ cSubtitle := C.CString(options.Subtitle)
+ cBody := C.CString(options.Body)
+ defer C.free(unsafe.Pointer(cIdentifier))
+ defer C.free(unsafe.Pointer(cTitle))
+ defer C.free(unsafe.Pointer(cSubtitle))
+ defer C.free(unsafe.Pointer(cBody))
+
+ var cDataJSON *C.char
+ if options.Data != nil {
+ jsonData, err := json.Marshal(options.Data)
+ if err != nil {
+ return fmt.Errorf("failed to marshal notification data: %w", err)
+ }
+ cDataJSON = C.CString(string(jsonData))
+ defer C.free(unsafe.Pointer(cDataJSON))
+ }
+
+ id, resultCh := dn.registerChannel()
+ C.sendNotification(C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cDataJSON)
+
+ select {
+ case result := <-resultCh:
+ if !result.Success {
+ if result.Error != nil {
+ return result.Error
+ }
+ return fmt.Errorf("sending notification failed")
+ }
+ return nil
+ case <-ctx.Done():
+ dn.cleanupChannel(id)
+ return fmt.Errorf("sending notification timed out: %w", ctx.Err())
+ }
+}
+
+// SendNotificationWithActions sends a notification with additional actions and inputs.
+// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
+// If a NotificationCategory is not registered a basic notification will be sent.
+func (dn *darwinNotifier) SendNotificationWithActions(options NotificationOptions) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ cIdentifier := C.CString(options.ID)
+ cTitle := C.CString(options.Title)
+ cSubtitle := C.CString(options.Subtitle)
+ cBody := C.CString(options.Body)
+ cCategoryID := C.CString(options.CategoryID)
+ defer C.free(unsafe.Pointer(cIdentifier))
+ defer C.free(unsafe.Pointer(cTitle))
+ defer C.free(unsafe.Pointer(cSubtitle))
+ defer C.free(unsafe.Pointer(cBody))
+ defer C.free(unsafe.Pointer(cCategoryID))
+
+ var cDataJSON *C.char
+ if options.Data != nil {
+ jsonData, err := json.Marshal(options.Data)
+ if err != nil {
+ return fmt.Errorf("failed to marshal notification data: %w", err)
+ }
+ cDataJSON = C.CString(string(jsonData))
+ defer C.free(unsafe.Pointer(cDataJSON))
+ }
+
+ id, resultCh := dn.registerChannel()
+ C.sendNotificationWithActions(C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cCategoryID, cDataJSON)
+
+ select {
+ case result := <-resultCh:
+ if !result.Success {
+ if result.Error != nil {
+ return result.Error
+ }
+ return fmt.Errorf("sending notification failed")
+ }
+ return nil
+ case <-ctx.Done():
+ dn.cleanupChannel(id)
+ return fmt.Errorf("sending notification timed out: %w", ctx.Err())
+ }
+}
+
+// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
+// Registering a category with the same name as a previously registered NotificationCategory will override it.
+func (dn *darwinNotifier) RegisterNotificationCategory(category NotificationCategory) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ cCategoryID := C.CString(category.ID)
+ defer C.free(unsafe.Pointer(cCategoryID))
+
+ actionsJSON, err := json.Marshal(category.Actions)
+ if err != nil {
+ return fmt.Errorf("failed to marshal notification category: %w", err)
+ }
+ cActionsJSON := C.CString(string(actionsJSON))
+ defer C.free(unsafe.Pointer(cActionsJSON))
+
+ var cReplyPlaceholder, cReplyButtonTitle *C.char
+ if category.HasReplyField {
+ cReplyPlaceholder = C.CString(category.ReplyPlaceholder)
+ cReplyButtonTitle = C.CString(category.ReplyButtonTitle)
+ defer C.free(unsafe.Pointer(cReplyPlaceholder))
+ defer C.free(unsafe.Pointer(cReplyButtonTitle))
+ }
+
+ id, resultCh := dn.registerChannel()
+ C.registerNotificationCategory(C.int(id), cCategoryID, cActionsJSON, C.bool(category.HasReplyField),
+ cReplyPlaceholder, cReplyButtonTitle)
+
+ select {
+ case result := <-resultCh:
+ if !result.Success {
+ if result.Error != nil {
+ return result.Error
+ }
+ return fmt.Errorf("category registration failed")
+ }
+ return nil
+ case <-ctx.Done():
+ dn.cleanupChannel(id)
+ return fmt.Errorf("category registration timed out: %w", ctx.Err())
+ }
+}
+
+// RemoveNotificationCategory remove a previously registered NotificationCategory.
+func (dn *darwinNotifier) RemoveNotificationCategory(categoryId string) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ cCategoryID := C.CString(categoryId)
+ defer C.free(unsafe.Pointer(cCategoryID))
+
+ id, resultCh := dn.registerChannel()
+ C.removeNotificationCategory(C.int(id), cCategoryID)
+
+ select {
+ case result := <-resultCh:
+ if !result.Success {
+ if result.Error != nil {
+ return result.Error
+ }
+ return fmt.Errorf("category removal failed")
+ }
+ return nil
+ case <-ctx.Done():
+ dn.cleanupChannel(id)
+ return fmt.Errorf("category removal timed out: %w", ctx.Err())
+ }
+}
+
+// RemoveAllPendingNotifications removes all pending notifications.
+func (dn *darwinNotifier) RemoveAllPendingNotifications() error {
+ C.removeAllPendingNotifications()
+ return nil
+}
+
+// RemovePendingNotification removes a pending notification matching the unique identifier.
+func (dn *darwinNotifier) RemovePendingNotification(identifier string) error {
+ cIdentifier := C.CString(identifier)
+ defer C.free(unsafe.Pointer(cIdentifier))
+ C.removePendingNotification(cIdentifier)
+ return nil
+}
+
+// RemoveAllDeliveredNotifications removes all delivered notifications.
+func (dn *darwinNotifier) RemoveAllDeliveredNotifications() error {
+ C.removeAllDeliveredNotifications()
+ return nil
+}
+
+// RemoveDeliveredNotification removes a delivered notification matching the unique identifier.
+func (dn *darwinNotifier) RemoveDeliveredNotification(identifier string) error {
+ cIdentifier := C.CString(identifier)
+ defer C.free(unsafe.Pointer(cIdentifier))
+ C.removeDeliveredNotification(cIdentifier)
+ return nil
+}
+
+// RemoveNotification is a macOS stub that always returns nil.
+// Use one of the following instead:
+// RemoveAllPendingNotifications
+// RemovePendingNotification
+// RemoveAllDeliveredNotifications
+// RemoveDeliveredNotification
+// (Linux-specific)
+func (dn *darwinNotifier) RemoveNotification(identifier string) error {
+ return nil
+}
+
+//export captureResult
+func captureResult(channelID C.int, success C.bool, errorMsg *C.char) {
+ ns := getNotificationService()
+ if ns == nil {
+ return
+ }
+
+ handler, ok := ns.impl.(ChannelHandler)
+ if !ok {
+ return
+ }
+
+ resultCh, exists := handler.GetChannel(int(channelID))
+ if !exists {
+ return
+ }
+
+ var err error
+ if errorMsg != nil {
+ err = fmt.Errorf("%s", C.GoString(errorMsg))
+ }
+
+ resultCh <- notificationChannel{
+ Success: bool(success),
+ Error: err,
+ }
+
+ close(resultCh)
+}
+
+//export didReceiveNotificationResponse
+func didReceiveNotificationResponse(jsonPayload *C.char, err *C.char) {
+ result := NotificationResult{}
+
+ if err != nil {
+ errMsg := C.GoString(err)
+ result.Error = fmt.Errorf("notification response error: %s", errMsg)
+ if ns := getNotificationService(); ns != nil {
+ ns.handleNotificationResult(result)
+ }
+ return
+ }
+
+ if jsonPayload == nil {
+ result.Error = fmt.Errorf("received nil JSON payload in notification response")
+ if ns := getNotificationService(); ns != nil {
+ ns.handleNotificationResult(result)
+ }
+ return
+ }
+
+ payload := C.GoString(jsonPayload)
+
+ var response NotificationResponse
+ if err := json.Unmarshal([]byte(payload), &response); err != nil {
+ result.Error = fmt.Errorf("failed to unmarshal notification response: %w", err)
+ if ns := getNotificationService(); ns != nil {
+ ns.handleNotificationResult(result)
+ }
+ return
+ }
+
+ if response.ActionIdentifier == AppleDefaultActionIdentifier {
+ response.ActionIdentifier = DefaultActionIdentifier
+ }
+
+ result.Response = response
+ if ns := getNotificationService(); ns != nil {
+ ns.handleNotificationResult(result)
+ }
+}
+
+// Helper methods
+
+func (dn *darwinNotifier) registerChannel() (int, chan notificationChannel) {
+ dn.channelsLock.Lock()
+ defer dn.channelsLock.Unlock()
+
+ id := dn.nextChannelID
+ dn.nextChannelID++
+
+ resultCh := make(chan notificationChannel, 1)
+
+ dn.channels[id] = resultCh
+ return id, resultCh
+}
+
+func (dn *darwinNotifier) GetChannel(id int) (chan notificationChannel, bool) {
+ dn.channelsLock.Lock()
+ defer dn.channelsLock.Unlock()
+
+ ch, exists := dn.channels[id]
+ if exists {
+ delete(dn.channels, id)
+ }
+ return ch, exists
+}
+
+func (dn *darwinNotifier) cleanupChannel(id int) {
+ dn.channelsLock.Lock()
+ defer dn.channelsLock.Unlock()
+
+ if ch, exists := dn.channels[id]; exists {
+ delete(dn.channels, id)
+ close(ch)
+ }
+}
diff --git a/v3/pkg/services/notifications/notifications_darwin.h b/v3/pkg/services/notifications/notifications_darwin.h
new file mode 100644
index 000000000..7cd505240
--- /dev/null
+++ b/v3/pkg/services/notifications/notifications_darwin.h
@@ -0,0 +1,21 @@
+//go:build darwin
+
+#ifndef NOTIFICATIONS_DARWIN_H
+#define NOTIFICATIONS_DARWIN_H
+
+#import
+
+bool checkBundleIdentifier(void);
+bool isNotificationAvailable(void);
+void requestNotificationAuthorization(int channelID);
+void checkNotificationAuthorization(int channelID);
+void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json);
+void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json);
+void registerNotificationCategory(int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle);
+void removeNotificationCategory(int channelID, const char *categoryId);
+void removeAllPendingNotifications(void);
+void removePendingNotification(const char *identifier);
+void removeAllDeliveredNotifications(void);
+void removeDeliveredNotification(const char *identifier);
+
+#endif /* NOTIFICATIONS_DARWIN_H */
\ No newline at end of file
diff --git a/v3/pkg/services/notifications/notifications_darwin.m b/v3/pkg/services/notifications/notifications_darwin.m
new file mode 100644
index 000000000..6c2048b74
--- /dev/null
+++ b/v3/pkg/services/notifications/notifications_darwin.m
@@ -0,0 +1,377 @@
+#import "notifications_darwin.h"
+#include
+#import
+
+#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
+#import
+#endif
+
+bool isNotificationAvailable(void) {
+ if (@available(macOS 11.0, *)) {
+ return YES;
+ } else {
+ return NO;
+ }
+}
+
+bool checkBundleIdentifier(void) {
+ NSBundle *main = [NSBundle mainBundle];
+ if (main.bundleIdentifier == nil) {
+ return NO;
+ }
+ return YES;
+}
+
+extern void captureResult(int channelID, bool success, const char* error);
+extern void didReceiveNotificationResponse(const char *jsonPayload, const char* error);
+
+@interface NotificationsDelegate : NSObject
+@end
+
+@implementation NotificationsDelegate
+
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center
+ willPresentNotification:(UNNotification *)notification
+ withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
+ UNNotificationPresentationOptions options = 0;
+
+ if (@available(macOS 11.0, *)) {
+ // These options are only available in macOS 11.0+
+ options = UNNotificationPresentationOptionList |
+ UNNotificationPresentationOptionBanner |
+ UNNotificationPresentationOptionSound;
+ }
+
+ completionHandler(options);
+}
+
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center
+didReceiveNotificationResponse:(UNNotificationResponse *)response
+ withCompletionHandler:(void (^)(void))completionHandler {
+
+ NSMutableDictionary *payload = [NSMutableDictionary dictionary];
+
+ [payload setObject:response.notification.request.identifier forKey:@"id"];
+ [payload setObject:response.actionIdentifier forKey:@"actionIdentifier"];
+ [payload setObject:response.notification.request.content.title ?: @"" forKey:@"title"];
+ [payload setObject:response.notification.request.content.body ?: @"" forKey:@"body"];
+
+ if (response.notification.request.content.categoryIdentifier) {
+ [payload setObject:response.notification.request.content.categoryIdentifier forKey:@"categoryIdentifier"];
+ }
+
+ if (response.notification.request.content.subtitle) {
+ [payload setObject:response.notification.request.content.subtitle forKey:@"subtitle"];
+ }
+
+ if (response.notification.request.content.userInfo) {
+ [payload setObject:response.notification.request.content.userInfo forKey:@"userInfo"];
+ }
+
+ if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) {
+ UNTextInputNotificationResponse *textResponse = (UNTextInputNotificationResponse *)response;
+ [payload setObject:textResponse.userText forKey:@"userText"];
+ }
+
+ NSError *error = nil;
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
+ if (error) {
+ NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
+ didReceiveNotificationResponse(NULL, [errorMsg UTF8String]);
+ } else {
+ NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+ didReceiveNotificationResponse([jsonString UTF8String], NULL);
+ }
+
+ completionHandler();
+}
+
+@end
+
+static NotificationsDelegate *delegateInstance = nil;
+static dispatch_once_t onceToken;
+
+static BOOL ensureDelegateInitialized(void) {
+ __block BOOL success = YES;
+
+ dispatch_once(&onceToken, ^{
+ delegateInstance = [[NotificationsDelegate alloc] init];
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ center.delegate = delegateInstance;
+ });
+
+ if (!delegateInstance) {
+ success = NO;
+ }
+
+ return success;
+}
+
+void requestNotificationAuthorization(int channelID) {
+ if (!ensureDelegateInitialized()) {
+ NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ return;
+ }
+
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
+
+ [center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) {
+ if (error) {
+ NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ } else {
+ captureResult(channelID, granted, NULL);
+ }
+ }];
+}
+
+void checkNotificationAuthorization(int channelID) {
+ if (!ensureDelegateInitialized()) {
+ NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ return;
+ }
+
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) {
+ BOOL isAuthorized = (settings.authorizationStatus == UNAuthorizationStatusAuthorized);
+ captureResult(channelID, isAuthorized, NULL);
+ }];
+}
+
+// Helper function to create notification content
+UNMutableNotificationContent* createNotificationContent(const char *title, const char *subtitle,
+ const char *body, const char *data_json, NSError **contentError) {
+ NSString *nsTitle = [NSString stringWithUTF8String:title];
+ NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
+ NSString *nsBody = [NSString stringWithUTF8String:body];
+
+ UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
+ content.title = nsTitle;
+ if (![nsSubtitle isEqualToString:@""]) {
+ content.subtitle = nsSubtitle;
+ }
+ content.body = nsBody;
+ content.sound = [UNNotificationSound defaultSound];
+
+ // Parse JSON data if provided
+ if (data_json) {
+ NSString *dataJsonStr = [NSString stringWithUTF8String:data_json];
+ NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding];
+ NSError *error = nil;
+ NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
+ if (!error && parsedData) {
+ content.userInfo = parsedData;
+ } else if (error) {
+ *contentError = error;
+ }
+ }
+
+ return content;
+}
+
+void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) {
+ if (!ensureDelegateInitialized()) {
+ NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ return;
+ }
+
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+
+ NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
+
+ NSError *contentError = nil;
+ UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json, &contentError);
+
+ if (contentError) {
+ NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [contentError localizedDescription]];
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ return;
+ }
+
+ UNTimeIntervalNotificationTrigger *trigger = nil;
+
+ UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger];
+
+ [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
+ if (error) {
+ NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ } else {
+ captureResult(channelID, true, NULL);
+ }
+ }];
+}
+
+void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle,
+ const char *body, const char *categoryId, const char *data_json) {
+ if (!ensureDelegateInitialized()) {
+ NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ return;
+ }
+
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+
+ NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
+ NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
+
+ NSError *contentError = nil;
+ UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json, &contentError);
+
+ if (contentError) {
+ NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [contentError localizedDescription]];
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ return;
+ }
+
+ content.categoryIdentifier = nsCategoryId;
+
+ UNTimeIntervalNotificationTrigger *trigger = nil;
+
+ UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger];
+
+ [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
+ if (error) {
+ NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ } else {
+ captureResult(channelID, true, NULL);
+ }
+ }];
+}
+
+void registerNotificationCategory(int channelID, const char *categoryId, const char *actions_json, bool hasReplyField,
+ const char *replyPlaceholder, const char *replyButtonTitle) {
+ if (!ensureDelegateInitialized()) {
+ NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ return;
+ }
+
+ NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
+ NSString *actionsJsonStr = actions_json ? [NSString stringWithUTF8String:actions_json] : @"[]";
+
+ NSData *jsonData = [actionsJsonStr dataUsingEncoding:NSUTF8StringEncoding];
+ NSError *error = nil;
+ NSArray *actionsArray = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
+
+ if (error) {
+ NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ return;
+ }
+
+ NSMutableArray *actions = [NSMutableArray array];
+
+ for (NSDictionary *actionDict in actionsArray) {
+ NSString *actionId = actionDict[@"id"];
+ NSString *actionTitle = actionDict[@"title"];
+ BOOL destructive = [actionDict[@"destructive"] boolValue];
+
+ if (actionId && actionTitle) {
+ UNNotificationActionOptions options = UNNotificationActionOptionNone;
+ if (destructive) options |= UNNotificationActionOptionDestructive;
+
+ UNNotificationAction *action = [UNNotificationAction
+ actionWithIdentifier:actionId
+ title:actionTitle
+ options:options];
+ [actions addObject:action];
+ }
+ }
+
+ if (hasReplyField && replyPlaceholder && replyButtonTitle) {
+ NSString *placeholder = [NSString stringWithUTF8String:replyPlaceholder];
+ NSString *buttonTitle = [NSString stringWithUTF8String:replyButtonTitle];
+
+ UNTextInputNotificationAction *textAction =
+ [UNTextInputNotificationAction actionWithIdentifier:@"TEXT_REPLY"
+ title:buttonTitle
+ options:UNNotificationActionOptionNone
+ textInputButtonTitle:buttonTitle
+ textInputPlaceholder:placeholder];
+ [actions addObject:textAction];
+ }
+
+ UNNotificationCategory *newCategory = [UNNotificationCategory
+ categoryWithIdentifier:nsCategoryId
+ actions:actions
+ intentIdentifiers:@[]
+ options:UNNotificationCategoryOptionNone];
+
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ [center getNotificationCategoriesWithCompletionHandler:^(NSSet *categories) {
+ NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories];
+
+ // Remove existing category with same ID if it exists
+ UNNotificationCategory *existingCategory = nil;
+ for (UNNotificationCategory *category in updatedCategories) {
+ if ([category.identifier isEqualToString:nsCategoryId]) {
+ existingCategory = category;
+ break;
+ }
+ }
+ if (existingCategory) {
+ [updatedCategories removeObject:existingCategory];
+ }
+
+ // Add the new category
+ [updatedCategories addObject:newCategory];
+ [center setNotificationCategories:updatedCategories];
+
+ captureResult(channelID, true, NULL);
+ }];
+}
+
+void removeNotificationCategory(int channelID, const char *categoryId) {
+ NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+
+ [center getNotificationCategoriesWithCompletionHandler:^(NSSet *categories) {
+ NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories];
+
+ // Find and remove the category with matching identifier
+ UNNotificationCategory *categoryToRemove = nil;
+ for (UNNotificationCategory *category in updatedCategories) {
+ if ([category.identifier isEqualToString:nsCategoryId]) {
+ categoryToRemove = category;
+ break;
+ }
+ }
+
+ if (categoryToRemove) {
+ [updatedCategories removeObject:categoryToRemove];
+ [center setNotificationCategories:updatedCategories];
+ captureResult(channelID, true, NULL);
+ } else {
+ NSString *errorMsg = [NSString stringWithFormat:@"Category '%@' not found", nsCategoryId];
+ captureResult(channelID, false, [errorMsg UTF8String]);
+ }
+ }];
+}
+
+void removeAllPendingNotifications(void) {
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ [center removeAllPendingNotificationRequests];
+}
+
+void removePendingNotification(const char *identifier) {
+ NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ [center removePendingNotificationRequestsWithIdentifiers:@[nsIdentifier]];
+}
+
+void removeAllDeliveredNotifications(void) {
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ [center removeAllDeliveredNotifications];
+}
+
+void removeDeliveredNotification(const char *identifier) {
+ NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ [center removeDeliveredNotificationsWithIdentifiers:@[nsIdentifier]];
+}
\ No newline at end of file
diff --git a/v3/pkg/services/notifications/notifications_linux.go b/v3/pkg/services/notifications/notifications_linux.go
new file mode 100644
index 000000000..bdc8312ff
--- /dev/null
+++ b/v3/pkg/services/notifications/notifications_linux.go
@@ -0,0 +1,565 @@
+//go:build linux
+
+package notifications
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+
+ "github.com/godbus/dbus/v5"
+ "github.com/wailsapp/wails/v3/pkg/application"
+)
+
+type linuxNotifier struct {
+ conn *dbus.Conn
+ categories map[string]NotificationCategory
+ categoriesLock sync.RWMutex
+ notifications map[uint32]*notificationData
+ notificationsLock sync.RWMutex
+ appName string
+ cancel context.CancelFunc
+}
+
+type notificationData struct {
+ ID string
+ Title string
+ Subtitle string
+ Body string
+ CategoryID string
+ Data map[string]interface{}
+ DBusID uint32
+ ActionMap map[string]string
+}
+
+const (
+ dbusNotificationInterface = "org.freedesktop.Notifications"
+ dbusNotificationPath = "/org/freedesktop/Notifications"
+)
+
+// Creates a new Notifications Service.
+func New() *Service {
+ notificationServiceOnce.Do(func() {
+ impl := &linuxNotifier{
+ categories: make(map[string]NotificationCategory),
+ notifications: make(map[uint32]*notificationData),
+ }
+
+ NotificationService = &Service{
+ impl: impl,
+ }
+ })
+
+ return NotificationService
+}
+
+// Startup is called when the service is loaded.
+func (ln *linuxNotifier) Startup(ctx context.Context, options application.ServiceOptions) error {
+ ln.appName = application.Get().Config().Name
+
+ conn, err := dbus.ConnectSessionBus()
+ if err != nil {
+ return fmt.Errorf("failed to connect to session bus: %w", err)
+ }
+ ln.conn = conn
+
+ if err := ln.loadCategories(); err != nil {
+ fmt.Printf("Failed to load notification categories: %v\n", err)
+ }
+
+ var signalCtx context.Context
+ signalCtx, ln.cancel = context.WithCancel(context.Background())
+
+ if err := ln.setupSignalHandling(signalCtx); err != nil {
+ return fmt.Errorf("failed to set up notification signal handling: %w", err)
+ }
+
+ return nil
+}
+
+// Shutdown will save categories and close the D-Bus connection when the service unloads.
+func (ln *linuxNotifier) Shutdown() error {
+ if ln.cancel != nil {
+ ln.cancel()
+ }
+
+ if err := ln.saveCategories(); err != nil {
+ fmt.Printf("Failed to save notification categories: %v\n", err)
+ }
+
+ if ln.conn != nil {
+ return ln.conn.Close()
+ }
+ return nil
+}
+
+// RequestNotificationAuthorization is a Linux stub that always returns true, nil.
+// (authorization is macOS-specific)
+func (ln *linuxNotifier) RequestNotificationAuthorization() (bool, error) {
+ return true, nil
+}
+
+// CheckNotificationAuthorization is a Linux stub that always returns true.
+// (authorization is macOS-specific)
+func (ln *linuxNotifier) CheckNotificationAuthorization() (bool, error) {
+ return true, nil
+}
+
+// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
+func (ln *linuxNotifier) SendNotification(options NotificationOptions) error {
+ hints := map[string]dbus.Variant{}
+
+ body := options.Body
+ if options.Subtitle != "" {
+ body = options.Subtitle + "\n" + body
+ }
+
+ defaultActionID := "default"
+ actions := []string{defaultActionID, "Default"}
+
+ actionMap := map[string]string{
+ defaultActionID: DefaultActionIdentifier,
+ }
+
+ hints["x-notification-id"] = dbus.MakeVariant(options.ID)
+
+ if options.Data != nil {
+ userData, err := json.Marshal(options.Data)
+ if err == nil {
+ hints["x-user-data"] = dbus.MakeVariant(string(userData))
+ }
+ }
+
+ // Call the Notify method on the D-Bus interface
+ obj := ln.conn.Object(dbusNotificationInterface, dbusNotificationPath)
+ call := obj.Call(
+ dbusNotificationInterface+".Notify",
+ 0,
+ ln.appName,
+ uint32(0),
+ "", // Icon
+ options.Title,
+ body,
+ actions,
+ hints,
+ int32(-1),
+ )
+
+ if call.Err != nil {
+ return fmt.Errorf("failed to send notification: %w", call.Err)
+ }
+
+ var dbusID uint32
+ if err := call.Store(&dbusID); err != nil {
+ return fmt.Errorf("failed to store notification ID: %w", err)
+ }
+
+ notification := ¬ificationData{
+ ID: options.ID,
+ Title: options.Title,
+ Subtitle: options.Subtitle,
+ Body: options.Body,
+ Data: options.Data,
+ DBusID: dbusID,
+ ActionMap: actionMap,
+ }
+
+ ln.notificationsLock.Lock()
+ ln.notifications[dbusID] = notification
+ ln.notificationsLock.Unlock()
+
+ return nil
+}
+
+// SendNotificationWithActions sends a notification with additional actions.
+func (ln *linuxNotifier) SendNotificationWithActions(options NotificationOptions) error {
+ ln.categoriesLock.RLock()
+ category, exists := ln.categories[options.CategoryID]
+ ln.categoriesLock.RUnlock()
+
+ if options.CategoryID == "" || !exists {
+ // Fall back to basic notification
+ return ln.SendNotification(options)
+ }
+
+ body := options.Body
+ if options.Subtitle != "" {
+ body = options.Subtitle + "\n" + body
+ }
+
+ var actions []string
+ actionMap := make(map[string]string)
+
+ defaultActionID := "default"
+ actions = append(actions, defaultActionID, "Default")
+ actionMap[defaultActionID] = DefaultActionIdentifier
+
+ for _, action := range category.Actions {
+ actions = append(actions, action.ID, action.Title)
+ actionMap[action.ID] = action.ID
+ }
+
+ hints := map[string]dbus.Variant{}
+
+ hints["x-notification-id"] = dbus.MakeVariant(options.ID)
+
+ hints["x-category-id"] = dbus.MakeVariant(options.CategoryID)
+
+ if options.Data != nil {
+ userData, err := json.Marshal(options.Data)
+ if err == nil {
+ hints["x-user-data"] = dbus.MakeVariant(string(userData))
+ }
+ }
+
+ obj := ln.conn.Object(dbusNotificationInterface, dbusNotificationPath)
+ call := obj.Call(
+ dbusNotificationInterface+".Notify",
+ 0,
+ ln.appName,
+ uint32(0),
+ "", // Icon
+ options.Title,
+ body,
+ actions,
+ hints,
+ int32(-1),
+ )
+
+ if call.Err != nil {
+ return fmt.Errorf("failed to send notification: %w", call.Err)
+ }
+
+ var dbusID uint32
+ if err := call.Store(&dbusID); err != nil {
+ return fmt.Errorf("failed to store notification ID: %w", err)
+ }
+
+ notification := ¬ificationData{
+ ID: options.ID,
+ Title: options.Title,
+ Subtitle: options.Subtitle,
+ Body: options.Body,
+ CategoryID: options.CategoryID,
+ Data: options.Data,
+ DBusID: dbusID,
+ ActionMap: actionMap,
+ }
+
+ ln.notificationsLock.Lock()
+ ln.notifications[dbusID] = notification
+ ln.notificationsLock.Unlock()
+
+ return nil
+}
+
+// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
+func (ln *linuxNotifier) RegisterNotificationCategory(category NotificationCategory) error {
+ ln.categoriesLock.Lock()
+ ln.categories[category.ID] = category
+ ln.categoriesLock.Unlock()
+
+ if err := ln.saveCategories(); err != nil {
+ fmt.Printf("Failed to save notification categories: %v\n", err)
+ }
+
+ return nil
+}
+
+// RemoveNotificationCategory removes a previously registered NotificationCategory.
+func (ln *linuxNotifier) RemoveNotificationCategory(categoryId string) error {
+ ln.categoriesLock.Lock()
+ delete(ln.categories, categoryId)
+ ln.categoriesLock.Unlock()
+
+ if err := ln.saveCategories(); err != nil {
+ fmt.Printf("Failed to save notification categories: %v\n", err)
+ }
+
+ return nil
+}
+
+// RemoveAllPendingNotifications attempts to remove all active notifications.
+func (ln *linuxNotifier) RemoveAllPendingNotifications() error {
+ ln.notificationsLock.Lock()
+ dbusIDs := make([]uint32, 0, len(ln.notifications))
+ for id := range ln.notifications {
+ dbusIDs = append(dbusIDs, id)
+ }
+ ln.notificationsLock.Unlock()
+
+ for _, id := range dbusIDs {
+ ln.closeNotification(id)
+ }
+
+ return nil
+}
+
+// RemovePendingNotification removes a pending notification.
+func (ln *linuxNotifier) RemovePendingNotification(identifier string) error {
+ var dbusID uint32
+ found := false
+
+ ln.notificationsLock.Lock()
+ for id, notif := range ln.notifications {
+ if notif.ID == identifier {
+ dbusID = id
+ found = true
+ break
+ }
+ }
+ ln.notificationsLock.Unlock()
+
+ if !found {
+ return nil
+ }
+
+ return ln.closeNotification(dbusID)
+}
+
+// RemoveAllDeliveredNotifications functionally equivalent to RemoveAllPendingNotification on Linux.
+func (ln *linuxNotifier) RemoveAllDeliveredNotifications() error {
+ return ln.RemoveAllPendingNotifications()
+}
+
+// RemoveDeliveredNotification functionally equivalent RemovePendingNotification on Linux.
+func (ln *linuxNotifier) RemoveDeliveredNotification(identifier string) error {
+ return ln.RemovePendingNotification(identifier)
+}
+
+// RemoveNotification removes a notification by identifier.
+func (ln *linuxNotifier) RemoveNotification(identifier string) error {
+ return ln.RemovePendingNotification(identifier)
+}
+
+// Helper method to close a notification.
+func (ln *linuxNotifier) closeNotification(id uint32) error {
+ obj := ln.conn.Object(dbusNotificationInterface, dbusNotificationPath)
+ call := obj.Call(dbusNotificationInterface+".CloseNotification", 0, id)
+
+ if call.Err != nil {
+ return fmt.Errorf("failed to close notification: %w", call.Err)
+ }
+
+ return nil
+}
+
+func (ln *linuxNotifier) getConfigDir() (string, error) {
+ configDir, err := os.UserConfigDir()
+ if err != nil {
+ return "", fmt.Errorf("failed to get user config directory: %w", err)
+ }
+
+ appConfigDir := filepath.Join(configDir, ln.appName)
+ if err := os.MkdirAll(appConfigDir, 0755); err != nil {
+ return "", fmt.Errorf("failed to create app config directory: %w", err)
+ }
+
+ return appConfigDir, nil
+}
+
+// Save notification categories.
+func (ln *linuxNotifier) saveCategories() error {
+ configDir, err := ln.getConfigDir()
+ if err != nil {
+ return err
+ }
+
+ categoriesFile := filepath.Join(configDir, "notification-categories.json")
+
+ ln.categoriesLock.RLock()
+ categoriesData, err := json.MarshalIndent(ln.categories, "", " ")
+ ln.categoriesLock.RUnlock()
+
+ if err != nil {
+ return fmt.Errorf("failed to marshal notification categories: %w", err)
+ }
+
+ if err := os.WriteFile(categoriesFile, categoriesData, 0644); err != nil {
+ return fmt.Errorf("failed to write notification categories to disk: %w", err)
+ }
+
+ return nil
+}
+
+// Load notification categories.
+func (ln *linuxNotifier) loadCategories() error {
+ configDir, err := ln.getConfigDir()
+ if err != nil {
+ return err
+ }
+
+ categoriesFile := filepath.Join(configDir, "notification-categories.json")
+
+ if _, err := os.Stat(categoriesFile); os.IsNotExist(err) {
+ return nil
+ }
+
+ categoriesData, err := os.ReadFile(categoriesFile)
+ if err != nil {
+ return fmt.Errorf("failed to read notification categories from disk: %w", err)
+ }
+
+ categories := make(map[string]NotificationCategory)
+ if err := json.Unmarshal(categoriesData, &categories); err != nil {
+ return fmt.Errorf("failed to unmarshal notification categories: %w", err)
+ }
+
+ ln.categoriesLock.Lock()
+ ln.categories = categories
+ ln.categoriesLock.Unlock()
+
+ return nil
+}
+
+// Setup signal handling for notification actions.
+func (ln *linuxNotifier) setupSignalHandling(ctx context.Context) error {
+ if err := ln.conn.AddMatchSignal(
+ dbus.WithMatchInterface(dbusNotificationInterface),
+ dbus.WithMatchMember("ActionInvoked"),
+ ); err != nil {
+ return err
+ }
+
+ if err := ln.conn.AddMatchSignal(
+ dbus.WithMatchInterface(dbusNotificationInterface),
+ dbus.WithMatchMember("NotificationClosed"),
+ ); err != nil {
+ return err
+ }
+
+ c := make(chan *dbus.Signal, 10)
+ ln.conn.Signal(c)
+
+ go ln.handleSignals(ctx, c)
+
+ return nil
+}
+
+// Handle incoming D-Bus signals.
+func (ln *linuxNotifier) handleSignals(ctx context.Context, c chan *dbus.Signal) {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case signal, ok := <-c:
+ if !ok {
+ return
+ }
+
+ switch signal.Name {
+ case dbusNotificationInterface + ".ActionInvoked":
+ ln.handleActionInvoked(signal)
+ case dbusNotificationInterface + ".NotificationClosed":
+ ln.handleNotificationClosed(signal)
+ }
+ }
+ }
+}
+
+// Handle ActionInvoked signal.
+func (ln *linuxNotifier) handleActionInvoked(signal *dbus.Signal) {
+ if len(signal.Body) < 2 {
+ return
+ }
+
+ dbusID, ok := signal.Body[0].(uint32)
+ if !ok {
+ return
+ }
+
+ actionID, ok := signal.Body[1].(string)
+ if !ok {
+ return
+ }
+
+ ln.notificationsLock.Lock()
+ notification, exists := ln.notifications[dbusID]
+ if exists {
+ delete(ln.notifications, dbusID)
+ }
+ ln.notificationsLock.Unlock()
+
+ if !exists {
+ return
+ }
+
+ appActionID, ok := notification.ActionMap[actionID]
+ if !ok {
+ appActionID = actionID
+ }
+
+ response := NotificationResponse{
+ ID: notification.ID,
+ ActionIdentifier: appActionID,
+ Title: notification.Title,
+ Subtitle: notification.Subtitle,
+ Body: notification.Body,
+ CategoryID: notification.CategoryID,
+ UserInfo: notification.Data,
+ }
+
+ result := NotificationResult{
+ Response: response,
+ }
+
+ if ns := getNotificationService(); ns != nil {
+ ns.handleNotificationResult(result)
+ }
+}
+
+// Handle NotificationClosed signal.
+// Reason codes:
+// 1 - expired timeout
+// 2 - dismissed by user (click on X)
+// 3 - closed by CloseNotification call
+// 4 - undefined/reserved
+func (ln *linuxNotifier) handleNotificationClosed(signal *dbus.Signal) {
+ if len(signal.Body) < 2 {
+ return
+ }
+
+ dbusID, ok := signal.Body[0].(uint32)
+ if !ok {
+ return
+ }
+
+ reason, ok := signal.Body[1].(uint32)
+ if !ok {
+ reason = 0 // Unknown reason
+ }
+
+ ln.notificationsLock.Lock()
+ notification, exists := ln.notifications[dbusID]
+ if exists {
+ delete(ln.notifications, dbusID)
+ }
+ ln.notificationsLock.Unlock()
+
+ if !exists {
+ return
+ }
+
+ if reason == 2 {
+ response := NotificationResponse{
+ ID: notification.ID,
+ ActionIdentifier: DefaultActionIdentifier,
+ Title: notification.Title,
+ Subtitle: notification.Subtitle,
+ Body: notification.Body,
+ CategoryID: notification.CategoryID,
+ UserInfo: notification.Data,
+ }
+
+ result := NotificationResult{
+ Response: response,
+ }
+
+ if ns := getNotificationService(); ns != nil {
+ ns.handleNotificationResult(result)
+ }
+ }
+}
diff --git a/v3/pkg/services/notifications/notifications_windows.go b/v3/pkg/services/notifications/notifications_windows.go
new file mode 100644
index 000000000..b7a16decc
--- /dev/null
+++ b/v3/pkg/services/notifications/notifications_windows.go
@@ -0,0 +1,433 @@
+//go:build windows
+
+package notifications
+
+import (
+ "context"
+ _ "embed"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+
+ "git.sr.ht/~jackmordaunt/go-toast/v2"
+ "github.com/google/uuid"
+ "github.com/wailsapp/wails/v3/pkg/application"
+ "github.com/wailsapp/wails/v3/pkg/w32"
+ "golang.org/x/sys/windows/registry"
+)
+
+type windowsNotifier struct {
+ categories map[string]NotificationCategory
+ categoriesLock sync.RWMutex
+ appName string
+ appGUID string
+ iconPath string
+}
+
+const (
+ ToastRegistryPath = `Software\Classes\AppUserModelId\`
+ ToastRegistryGuidKey = "CustomActivator"
+ NotificationCategoriesRegistryPath = `SOFTWARE\%s\NotificationCategories`
+ NotificationCategoriesRegistryKey = "Categories"
+)
+
+// NotificationPayload combines the action ID and user data into a single structure
+type NotificationPayload struct {
+ Action string `json:"action"`
+ Options NotificationOptions `json:"payload,omitempty"`
+}
+
+// Creates a new Notifications Service.
+func New() *Service {
+ notificationServiceOnce.Do(func() {
+ impl := &windowsNotifier{
+ categories: make(map[string]NotificationCategory),
+ }
+
+ NotificationService = &Service{
+ impl: impl,
+ }
+ })
+
+ return NotificationService
+}
+
+// Startup is called when the service is loaded
+// Sets an activation callback to emit an event when notifications are interacted with.
+func (wn *windowsNotifier) Startup(ctx context.Context, options application.ServiceOptions) error {
+ wn.categoriesLock.Lock()
+ defer wn.categoriesLock.Unlock()
+
+ wn.appName = application.Get().Config().Name
+
+ guid, err := wn.getGUID()
+ if err != nil {
+ return err
+ }
+ wn.appGUID = guid
+
+ wn.iconPath = filepath.Join(os.TempDir(), wn.appName+wn.appGUID+".png")
+
+ toast.SetAppData(toast.AppData{
+ AppID: wn.appName,
+ GUID: guid,
+ IconPath: wn.iconPath,
+ })
+
+ toast.SetActivationCallback(func(args string, data []toast.UserData) {
+ result := NotificationResult{}
+
+ actionIdentifier, options, err := parseNotificationResponse(args)
+
+ if err != nil {
+ result.Error = err
+
+ if ns := getNotificationService(); ns != nil {
+ ns.handleNotificationResult(result)
+ }
+ return
+ }
+
+ // Subtitle is retained but was not shown with the notification
+ response := NotificationResponse{
+ ID: options.ID,
+ ActionIdentifier: actionIdentifier,
+ Title: options.Title,
+ Subtitle: options.Subtitle,
+ Body: options.Body,
+ CategoryID: options.CategoryID,
+ UserInfo: options.Data,
+ }
+
+ if userText, found := wn.getUserText(data); found {
+ response.UserText = userText
+ }
+
+ result.Response = response
+ if ns := getNotificationService(); ns != nil {
+ ns.handleNotificationResult(result)
+ }
+ })
+
+ return wn.loadCategoriesFromRegistry()
+}
+
+// Shutdown will attempt to save the categories to the registry when the service unloads
+func (wn *windowsNotifier) Shutdown() error {
+ wn.categoriesLock.Lock()
+ defer wn.categoriesLock.Unlock()
+
+ return wn.saveCategoriesToRegistry()
+}
+
+// RequestNotificationAuthorization is a Windows stub that always returns true, nil.
+// (user authorization is macOS-specific)
+func (wn *windowsNotifier) RequestNotificationAuthorization() (bool, error) {
+ return true, nil
+}
+
+// CheckNotificationAuthorization is a Windows stub that always returns true.
+// (user authorization is macOS-specific)
+func (wn *windowsNotifier) CheckNotificationAuthorization() (bool, error) {
+ return true, nil
+}
+
+// SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows.
+// (subtitle is only available on macOS and Linux)
+func (wn *windowsNotifier) SendNotification(options NotificationOptions) error {
+ if err := wn.saveIconToDir(); err != nil {
+ fmt.Printf("Error saving icon: %v\n", err)
+ }
+
+ n := toast.Notification{
+ Title: options.Title,
+ Body: options.Body,
+ ActivationArguments: DefaultActionIdentifier,
+ }
+
+ if options.Data != nil {
+ encodedPayload, err := wn.encodePayload(DefaultActionIdentifier, options)
+ if err != nil {
+ return fmt.Errorf("failed to encode notification payload: %w", err)
+ }
+ n.ActivationArguments = encodedPayload
+ }
+
+ return n.Push()
+}
+
+// SendNotificationWithActions sends a notification with additional actions and inputs.
+// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
+// If a NotificationCategory is not registered a basic notification will be sent.
+// (subtitle is only available on macOS and Linux)
+func (wn *windowsNotifier) SendNotificationWithActions(options NotificationOptions) error {
+ if err := wn.saveIconToDir(); err != nil {
+ fmt.Printf("Error saving icon: %v\n", err)
+ }
+
+ wn.categoriesLock.RLock()
+ nCategory, categoryExists := wn.categories[options.CategoryID]
+ wn.categoriesLock.RUnlock()
+
+ if options.CategoryID == "" || !categoryExists {
+ fmt.Printf("Category '%s' not found, sending basic notification without actions\n", options.CategoryID)
+ }
+
+ n := toast.Notification{
+ Title: options.Title,
+ Body: options.Body,
+ ActivationArguments: DefaultActionIdentifier,
+ }
+
+ for _, action := range nCategory.Actions {
+ n.Actions = append(n.Actions, toast.Action{
+ Content: action.Title,
+ Arguments: action.ID,
+ })
+ }
+
+ if nCategory.HasReplyField {
+ n.Inputs = append(n.Inputs, toast.Input{
+ ID: "userText",
+ Placeholder: nCategory.ReplyPlaceholder,
+ })
+
+ n.Actions = append(n.Actions, toast.Action{
+ Content: nCategory.ReplyButtonTitle,
+ Arguments: "TEXT_REPLY",
+ InputID: "userText",
+ })
+ }
+
+ if options.Data != nil {
+ encodedPayload, err := wn.encodePayload(n.ActivationArguments, options)
+ if err != nil {
+ return fmt.Errorf("failed to encode notification payload: %w", err)
+ }
+ n.ActivationArguments = encodedPayload
+
+ for index := range n.Actions {
+ encodedPayload, err := wn.encodePayload(n.Actions[index].Arguments, options)
+ if err != nil {
+ return fmt.Errorf("failed to encode notification payload: %w", err)
+ }
+ n.Actions[index].Arguments = encodedPayload
+ }
+ }
+
+ return n.Push()
+}
+
+// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
+// Registering a category with the same name as a previously registered NotificationCategory will override it.
+func (wn *windowsNotifier) RegisterNotificationCategory(category NotificationCategory) error {
+ wn.categoriesLock.Lock()
+ defer wn.categoriesLock.Unlock()
+
+ wn.categories[category.ID] = NotificationCategory{
+ ID: category.ID,
+ Actions: category.Actions,
+ HasReplyField: bool(category.HasReplyField),
+ ReplyPlaceholder: category.ReplyPlaceholder,
+ ReplyButtonTitle: category.ReplyButtonTitle,
+ }
+
+ return wn.saveCategoriesToRegistry()
+}
+
+// RemoveNotificationCategory removes a previously registered NotificationCategory.
+func (wn *windowsNotifier) RemoveNotificationCategory(categoryId string) error {
+ wn.categoriesLock.Lock()
+ defer wn.categoriesLock.Unlock()
+
+ delete(wn.categories, categoryId)
+
+ return wn.saveCategoriesToRegistry()
+}
+
+// RemoveAllPendingNotifications is a Windows stub that always returns nil.
+// (macOS and Linux only)
+func (wn *windowsNotifier) RemoveAllPendingNotifications() error {
+ return nil
+}
+
+// RemovePendingNotification is a Windows stub that always returns nil.
+// (macOS and Linux only)
+func (wn *windowsNotifier) RemovePendingNotification(_ string) error {
+ return nil
+}
+
+// RemoveAllDeliveredNotifications is a Windows stub that always returns nil.
+// (macOS and Linux only)
+func (wn *windowsNotifier) RemoveAllDeliveredNotifications() error {
+ return nil
+}
+
+// RemoveDeliveredNotification is a Windows stub that always returns nil.
+// (macOS and Linux only)
+func (wn *windowsNotifier) RemoveDeliveredNotification(_ string) error {
+ return nil
+}
+
+// RemoveNotification is a Windows stub that always returns nil.
+// (Linux-specific)
+func (wn *windowsNotifier) RemoveNotification(identifier string) error {
+ return nil
+}
+
+// encodePayload combines an action ID and user data into a single encoded string
+func (wn *windowsNotifier) encodePayload(actionID string, options NotificationOptions) (string, error) {
+ payload := NotificationPayload{
+ Action: actionID,
+ Options: options,
+ }
+
+ jsonData, err := json.Marshal(payload)
+ if err != nil {
+ return actionID, err
+ }
+
+ encodedPayload := base64.StdEncoding.EncodeToString(jsonData)
+ return encodedPayload, nil
+}
+
+// decodePayload extracts the action ID and user data from an encoded payload
+func decodePayload(encodedString string) (string, NotificationOptions, error) {
+ jsonData, err := base64.StdEncoding.DecodeString(encodedString)
+ if err != nil {
+ return encodedString, NotificationOptions{}, fmt.Errorf("failed to decode base64 payload: %w", err)
+ }
+
+ var payload NotificationPayload
+ if err := json.Unmarshal(jsonData, &payload); err != nil {
+ return encodedString, NotificationOptions{}, fmt.Errorf("failed to unmarshal notification payload: %w", err)
+ }
+
+ return payload.Action, payload.Options, nil
+}
+
+// parseNotificationResponse updated to use structured payload decoding
+func parseNotificationResponse(response string) (action string, options NotificationOptions, err error) {
+ actionID, options, err := decodePayload(response)
+
+ if err != nil {
+ fmt.Printf("Warning: Failed to decode notification response: %v\n", err)
+ return response, NotificationOptions{}, err
+ }
+
+ return actionID, options, nil
+}
+
+func (wn *windowsNotifier) saveIconToDir() error {
+ icon, err := application.NewIconFromResource(w32.GetModuleHandle(""), uint16(3))
+ if err != nil {
+ return fmt.Errorf("failed to retrieve application icon: %w", err)
+ }
+
+ return w32.SaveHIconAsPNG(icon, wn.iconPath)
+}
+
+func (wn *windowsNotifier) saveCategoriesToRegistry() error {
+ // We assume lock is held by caller
+
+ registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, wn.appName)
+
+ key, _, err := registry.CreateKey(
+ registry.CURRENT_USER,
+ registryPath,
+ registry.ALL_ACCESS,
+ )
+ if err != nil {
+ return err
+ }
+ defer key.Close()
+
+ data, err := json.Marshal(wn.categories)
+ if err != nil {
+ return err
+ }
+
+ return key.SetStringValue(NotificationCategoriesRegistryKey, string(data))
+}
+
+func (wn *windowsNotifier) loadCategoriesFromRegistry() error {
+ // We assume lock is held by caller
+
+ registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, wn.appName)
+
+ key, err := registry.OpenKey(
+ registry.CURRENT_USER,
+ registryPath,
+ registry.QUERY_VALUE,
+ )
+ if err != nil {
+ if err == registry.ErrNotExist {
+ // Not an error, no saved categories
+ return nil
+ }
+ return fmt.Errorf("failed to open registry key: %w", err)
+ }
+ defer key.Close()
+
+ data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey)
+ if err != nil {
+ if err == registry.ErrNotExist {
+ // No value yet, but key exists
+ return nil
+ }
+ return fmt.Errorf("failed to read categories from registry: %w", err)
+ }
+
+ categories := make(map[string]NotificationCategory)
+ if err := json.Unmarshal([]byte(data), &categories); err != nil {
+ return fmt.Errorf("failed to parse notification categories from registry: %w", err)
+ }
+
+ wn.categories = categories
+
+ return nil
+}
+
+func (wn *windowsNotifier) getUserText(data []toast.UserData) (string, bool) {
+ for _, d := range data {
+ if d.Key == "userText" {
+ return d.Value, true
+ }
+ }
+ return "", false
+}
+
+func (wn *windowsNotifier) getGUID() (string, error) {
+ keyPath := ToastRegistryPath + wn.appName
+
+ k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE)
+ if err == nil {
+ guid, _, err := k.GetStringValue(ToastRegistryGuidKey)
+ k.Close()
+ if err == nil && guid != "" {
+ return guid, nil
+ }
+ }
+
+ guid := wn.generateGUID()
+
+ k, _, err = registry.CreateKey(registry.CURRENT_USER, keyPath, registry.WRITE)
+ if err != nil {
+ return "", fmt.Errorf("failed to create registry key: %w", err)
+ }
+ defer k.Close()
+
+ if err := k.SetStringValue(ToastRegistryGuidKey, guid); err != nil {
+ return "", fmt.Errorf("failed to write GUID to registry: %w", err)
+ }
+
+ return guid, nil
+}
+
+func (wn *windowsNotifier) generateGUID() string {
+ guid := uuid.New()
+ return fmt.Sprintf("{%s}", guid.String())
+}
diff --git a/v3/pkg/w32/icon.go b/v3/pkg/w32/icon.go
index 009479323..97d4ad854 100644
--- a/v3/pkg/w32/icon.go
+++ b/v3/pkg/w32/icon.go
@@ -6,8 +6,11 @@ import (
"bytes"
"fmt"
"image"
+ "image/color"
"image/draw"
"image/png"
+ "os"
+ "syscall"
"unsafe"
)
@@ -90,6 +93,121 @@ func CreateLargeHIconFromImage(fileData []byte) (HICON, error) {
return HICON(icon), err
}
+type ICONINFO struct {
+ FIcon int32
+ XHotspot int32
+ YHotspot int32
+ HbmMask syscall.Handle
+ HbmColor syscall.Handle
+}
+
+func SaveHIconAsPNG(hIcon HICON, filePath string) error {
+ // Load necessary DLLs
+ user32 := syscall.NewLazyDLL("user32.dll")
+ gdi32 := syscall.NewLazyDLL("gdi32.dll")
+
+ // Get procedures
+ getIconInfo := user32.NewProc("GetIconInfo")
+ getObject := gdi32.NewProc("GetObjectW")
+ createCompatibleDC := gdi32.NewProc("CreateCompatibleDC")
+ selectObject := gdi32.NewProc("SelectObject")
+ getDIBits := gdi32.NewProc("GetDIBits")
+ deleteObject := gdi32.NewProc("DeleteObject")
+ deleteDC := gdi32.NewProc("DeleteDC")
+
+ // Get icon info
+ var iconInfo ICONINFO
+ ret, _, err := getIconInfo.Call(
+ uintptr(hIcon),
+ uintptr(unsafe.Pointer(&iconInfo)),
+ )
+ if ret == 0 {
+ return err
+ }
+ defer deleteObject.Call(uintptr(iconInfo.HbmMask))
+ defer deleteObject.Call(uintptr(iconInfo.HbmColor))
+
+ // Get bitmap info
+ var bmp BITMAP
+ ret, _, err = getObject.Call(
+ uintptr(iconInfo.HbmColor),
+ unsafe.Sizeof(bmp),
+ uintptr(unsafe.Pointer(&bmp)),
+ )
+ if ret == 0 {
+ return err
+ }
+
+ // Create DC
+ hdc, _, _ := createCompatibleDC.Call(0)
+ if hdc == 0 {
+ return syscall.EINVAL
+ }
+ defer deleteDC.Call(hdc)
+
+ // Select bitmap into DC
+ oldBitmap, _, _ := selectObject.Call(hdc, uintptr(iconInfo.HbmColor))
+ defer selectObject.Call(hdc, oldBitmap)
+
+ // Prepare bitmap info header
+ var bi BITMAPINFO
+ bi.BmiHeader.BiSize = uint32(unsafe.Sizeof(bi.BmiHeader))
+ bi.BmiHeader.BiWidth = bmp.BmWidth
+ bi.BmiHeader.BiHeight = bmp.BmHeight
+ bi.BmiHeader.BiPlanes = 1
+ bi.BmiHeader.BiBitCount = 32
+ bi.BmiHeader.BiCompression = BI_RGB
+
+ // Allocate memory for bitmap bits
+ width, height := int(bmp.BmWidth), int(bmp.BmHeight)
+ bufferSize := width * height * 4
+ bits := make([]byte, bufferSize)
+
+ // Get bitmap bits
+ ret, _, err = getDIBits.Call(
+ hdc,
+ uintptr(iconInfo.HbmColor),
+ 0,
+ uintptr(bmp.BmHeight),
+ uintptr(unsafe.Pointer(&bits[0])),
+ uintptr(unsafe.Pointer(&bi)),
+ DIB_RGB_COLORS,
+ )
+ if ret == 0 {
+ return err
+ }
+
+ // Create Go image
+ img := image.NewRGBA(image.Rect(0, 0, width, height))
+
+ // Convert DIB to RGBA
+ for y := 0; y < height; y++ {
+ for x := 0; x < width; x++ {
+ // DIB is bottom-up, so we need to invert Y
+ dibIndex := ((height-1-y)*width + x) * 4
+
+ // BGRA to RGBA
+ b := bits[dibIndex]
+ g := bits[dibIndex+1]
+ r := bits[dibIndex+2]
+ a := bits[dibIndex+3]
+
+ // Set pixel in the image
+ img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a})
+ }
+ }
+
+ // Create output file
+ outFile, err := os.Create(filePath)
+ if err != nil {
+ return err
+ }
+ defer outFile.Close()
+
+ // Encode and save the image
+ return png.Encode(outFile, img)
+}
+
func SetWindowIcon(hwnd HWND, icon HICON) {
SendMessage(hwnd, WM_SETICON, ICON_SMALL, uintptr(icon))
}