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

Merge pull request #4098 from popaprozac/notifications_darwin

[v3] Notifications API
This commit is contained in:
Lea Anthony 2025-04-13 13:01:04 +10:00 committed by GitHub
commit afb4bd933d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 5182 additions and 0 deletions

View File

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

View File

@ -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
<Tabs>
<TabItem label="macOS" icon="fa-brands:apple">
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
</TabItem>
<TabItem label="Windows" icon="fa-brands:windows">
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`
</TabItem>
<TabItem label="Linux" icon="fa-brands:linux">
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
</TabItem>
</Tabs>
## 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
}
```

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

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

View File

@ -0,0 +1,32 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>My Product</string>
<key>CFBundleExecutable</key>
<string>Notifications Demo</string>
<key>CFBundleIdentifier</key>
<string>com.wails.notifications-demo</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleGetInfoString</key>
<string>This is a comment</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
<string>10.15.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>© now, My Company</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,27 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>My Product</string>
<key>CFBundleExecutable</key>
<string>Notifications Demo</string>
<key>CFBundleIdentifier</key>
<string>com.wails.notifications-demo</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleGetInfoString</key>
<string>This is a comment</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
<string>10.15.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>© now, My Company</string>
</dict>
</plist>

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
#!/bin/bash

View File

@ -0,0 +1 @@
#!/bin/bash

View File

@ -0,0 +1 @@
#!/bin/bash

View File

@ -0,0 +1 @@
#!/bin/bash

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.notifications" version="0.1.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View File

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

View File

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

View File

@ -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<NotificationAction> = {}) {
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<NotificationAction>);
}
}
/**
* 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<NotificationCategory> = {}) {
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<NotificationCategory>);
}
}
/**
* 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<NotificationOptions> = {}) {
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<NotificationOptions>);
}
}
// Private type creation functions
const $$createType0 = NotificationAction.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $Create.Map($Create.Any, $Create.Any);

View File

@ -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<boolean> {
return $Call.ByID(2789931702);
}
export function RegisterNotificationCategory(category: $models.NotificationCategory): $CancellablePromise<void> {
return $Call.ByID(2679064664, category);
}
export function RemoveAllDeliveredNotifications(): $CancellablePromise<void> {
return $Call.ByID(384520397);
}
export function RemoveAllPendingNotifications(): $CancellablePromise<void> {
return $Call.ByID(1423986276);
}
export function RemoveDeliveredNotification(identifier: string): $CancellablePromise<void> {
return $Call.ByID(149440045, identifier);
}
export function RemoveNotification(identifier: string): $CancellablePromise<void> {
return $Call.ByID(3702062929, identifier);
}
export function RemoveNotificationCategory(categoryID: string): $CancellablePromise<void> {
return $Call.ByID(229511469, categoryID);
}
export function RemovePendingNotification(identifier: string): $CancellablePromise<void> {
return $Call.ByID(3872412470, identifier);
}
/**
* Public methods that delegate to the implementation.
*/
export function RequestNotificationAuthorization(): $CancellablePromise<boolean> {
return $Call.ByID(729898933);
}
export function SendNotification(options: $models.NotificationOptions): $CancellablePromise<void> {
return $Call.ByID(2246903123, options);
}
export function SendNotificationWithActions(options: $models.NotificationOptions): $CancellablePromise<void> {
return $Call.ByID(1615199806, options);
}

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/wails.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="/style.css"/>
<title>Wails App</title>
</head>
<body>
<div class="container">
<div>
<a data-wml-openURL="https://wails.io">
<img src="/wails.png" class="logo" alt="Wails logo"/>
</a>
<a data-wml-openURL="https://www.typescriptlang.org/">
<img src="/typescript.svg" class="logo vanilla" alt="Typescript logo"/>
</a>
</div>
<h1>Wails + Typescript + Desktop Notifications</h1>
<h3>Send notifications 👇</h3>
<div class="controls">
<button class="btn" id="basic">Basic</button>
<button class="btn" id="complex">Complex</button>
</div>
<div class="footer" id="response"></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -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 = `
<h5>Notification Response</h5>
<table>
<thead>
${Object.keys(base).map(key => `<th>${key}</th>`).join("")}
</thead>
<tbody>
${Object.values(base).map(value => `<td>${value}</td>`).join("")}
</tbody>
</table>
<h5>Notification Metadata</h5>
<table>
<thead>
${Object.keys(userInfo).map(key => `<th>${key}</th>`).join("")}
</thead>
<tbody>
${Object.values(userInfo).map(value => `<td>${value}</td>`).join("")}
</tbody>
</table>
`;
const footer = document.querySelector("#response");
if (footer) footer.innerHTML = table;
});
window.onbeforeunload = () => unlisten();

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,21 @@
//go:build darwin
#ifndef NOTIFICATIONS_DARWIN_H
#define NOTIFICATIONS_DARWIN_H
#import <Foundation/Foundation.h>
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 */

View File

@ -0,0 +1,377 @@
#import "notifications_darwin.h"
#include <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
#import <UserNotifications/UserNotifications.h>
#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 <UNUserNotificationCenterDelegate>
@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<UNNotificationCategory *> *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<UNNotificationCategory *> *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]];
}

View File

@ -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 := &notificationData{
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 := &notificationData{
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)
}
}
}

View File

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

View File

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