Compare commits

...

24 commits

Author SHA1 Message Date
Zach Botterman
51d30325fc
[v2] Notifications API (#4256)
* init v2

* implement macOS and Windows

* minor cleanup

* fix segfault

* linux

* 🐰

* remove windows init

* formatting

* fix win icon

* clean

* clean

* codesign full path

* fix en/decoding and notification types

* changelog & docs

* fix options and channel fix

* update docs

* correct docs

* Update website/docs/reference/runtime/notification.mdx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* work through rabbit suggestions

* nil checks, cleanups, docs

* locks

* Update v2/internal/frontend/desktop/windows/notifications.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* update js runtime

* update docs

* second pass of comments

* coherent JSON key, icon improv

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-15 02:38:32 +00:00
github-actions[bot]
f8595e3052
chore: update sponsors.svg (#5032)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-03-15 13:07:12 +11:00
github-actions[bot]
4d0abeb37c
chore: update sponsors.svg (#5025)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-02-28 14:09:40 +11:00
github-actions[bot]
033650d792
chore: update sponsors.svg (#5015)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-02-26 21:01:45 +11:00
github-actions[bot]
4c49f27edf
chore: update sponsors.svg (#5000)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-02-22 15:27:44 +11:00
github-actions[bot]
c84578721c
chore: update sponsors.svg (#4999)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-02-18 21:18:16 +11:00
github-actions[bot]
354fee648e
chore: update sponsors.svg (#4997)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-02-17 22:39:19 +11:00
github-actions[bot]
da3ce17161
chore: update sponsors.svg (#4993)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-02-14 18:43:34 +11:00
Lea Anthony
bbd1b33122
Add Claude Code GitHub Workflow (#4988)
* "Claude PR Assistant workflow"

* "Claude Code Review workflow"
2026-02-14 00:55:40 +11:00
github-actions[bot]
ae40ca4ac1
chore: update sponsors.svg (#4980)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-02-11 21:50:29 +11:00
github-actions[bot]
093aa2d663
chore: update sponsors.svg (#4978)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-02-09 20:56:49 +11:00
Jay Pipes
e906751c89
update github.com/jaypipes/ghw dependency (#4970)
There's been a ton of improvements in the `ghw` library since the
v0.13.0 release, including the update of certain transitive dependencies
around Windows and Darwin support libraries.

This patch simply brings in those improvements. The `v0.21.3` release of
`ghw` is fully backwards-compatible with `v0.13.0`.

Signed-off-by: Jay Pipes <jaypipes@gmail.com>
2026-02-09 07:49:23 +11:00
Lea Anthony
718fd92f85
fix(v2): prevent wails init in non-empty directory with -d flag (#4955)
* fix(v2): prevent wails init in non-empty directory with -d flag

When using -d to specify a target directory, wails init now checks if
the directory is non-empty and errors if so. This prevents accidental
data loss (e.g., overwriting .git directories).

Fixes #4940

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(v2): add tests for init non-empty directory check

Add tests to verify:
- Install fails when target directory is non-empty
- Install succeeds when target directory is empty

Also update changelog with the fix.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Apply suggestions from code review

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-04 21:23:07 +11:00
Lea Anthony
01b661f6a5
feat(v2): add runtime.ResetSignalHandlers() for Linux panic recovery (#4921)
* feat(v2): add runtime.ResetSignalHandlers() for Linux panic recovery

Add a new runtime function that allows users to reset signal handlers
before code that might panic from nil pointer dereferences.

On Linux, WebKit installs signal handlers without the SA_ONSTACK flag,
which prevents Go from properly recovering from panics caused by
SIGSEGV and other signals. This function adds SA_ONSTACK to the
relevant signal handlers (SIGSEGV, SIGBUS, SIGFPE, SIGABRT).

Usage:
```go
go func() {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("Recovered: %v", err)
        }
    }()
    runtime.ResetSignalHandlers()
    // Code that might panic...
}()
```

The function is a no-op on macOS and Windows.

Fixes #3965

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(v2): add panic-recovery-test example

Add an example that demonstrates the Linux signal handler issue (#3965)
and verifies the fix using runtime.ResetSignalHandlers().

The example includes:
- A Greet function that triggers a nil pointer dereference after a delay
- Auto-call from frontend after 5 seconds
- README with reproduction steps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 18:55:57 +11:00
github-actions[bot]
896344eb66
chore: update sponsors.svg (#4942)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-02-02 18:40:10 +11:00
github-actions[bot]
8fd0340404
chore: update sponsors.svg (#4911)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-01-31 22:32:06 +11:00
Lea Anthony
bc4ee373b5 ci: add cross-compilation tests to Docker image workflow
- Add test-cross-compile job that tests CGO builds for all 6 platform/arch combos
- Add test-non-cgo job for pure Go cross-compilation verification
- Add test-summary job with GitHub Actions summary output
- Add skip_tests input for manual workflow dispatch
- Verify Linux binaries link to required GTK/WebKit libraries
- Verify binary format matches expected architecture

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 08:34:46 +11:00
Lea Anthony
5d39f1aa9a ci: fix cross-compiler workflow for multi-arch builds
- Fix Dockerfile path (was pointing to non-existent setupwizard/docker)
- Add multi-platform build (linux/amd64 + linux/arm64)
- Add QEMU for ARM64 cross-compilation
- Add automatic trigger on Dockerfile.cross changes to v3-alpha
- Update SDK version default to 14.5

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 06:32:08 +11:00
github-actions[bot]
a27995940a
chore: update sponsors.svg (#4907)
Co-authored-by: leaanthony <1943904+leaanthony@users.noreply.github.com>
2026-01-26 22:16:26 +11:00
NarciLee
4e8b705cda
fix: add missing EventsOffAll export to runtime templates (#4883)
* fix: add missing EventsOffAll export to runtime templates

The EventsOffAll function was implemented in the internal runtime
(desktop/events.js) and exported in the wrapper runtime files, but was
missing from the public runtime templates that are generated in user
projects. This caused a type mismatch where EventsOffAll was declared
in the TypeScript definition but the implementation was missing.

This commit adds the EventsOffAll export and type declaration to all
frontend framework templates:
- Common templates (generate/assets/common)
- React / React-TS
- Vue / Vue-TS
- Svelte / Svelte-TS
- Preact / Preact-TS
- Lit / Lit-TS
- Vanilla / Vanilla-TS

Fixes #4703

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: add EventsOffAll to runtime events documentation

Added EventsOffAll function documentation to both English and Chinese
versions of the runtime events reference.

The function was already implemented in Go (pkg/runtime/events.go) and
JavaScript (internal/frontend/runtime/desktop/events.js), but was missing
from the public documentation.

Related to #4703

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* chore: add EventsOffAll fix to changelog

Added entry for the EventsOffAll fix to the Unreleased section
of the changelog.

Related to #4703

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: translate EventsOffAll description to Chinese

Improved consistency in Chinese documentation by translating the
EventsOffAll description from English to Chinese, matching the
style of other event method descriptions.

Related to #4703

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: cc <Zhuanz@MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
2026-01-25 13:10:24 +11:00
Lea Anthony
7ee2b2d856
fix(v2/linux): fix crash on panic in JS-bound Go methods (#4855)
* fix(v2/linux): fix crash on panic in JS-bound Go methods

WebKit2GTK installs signal handlers after gtk_main() starts, overriding
our SA_ONSTACK fix. This causes Go panics (e.g., nil pointer dereference)
in JS-bound methods to crash with 'non-Go code set up signal handler
without SA_ONSTACK flag'.

Fix by deferring signal handler installation via g_idle_add() to run
after GTK main loop starts, ensuring we fix handlers AFTER WebKit
has installed its own.

Fixes #3965

* docs: add changelog entry for Linux signal handler fix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:38:58 +11:00
Salman
696c55c6ee
Update code block range in howdoesitwork.mdx (#4884)
* Update code block range in howdoesitwork.mdx

* docs: add changelog entry for PR #4884

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:13:28 +11:00
Xelus22
0357730294
Fix up templates to correct go version (1.23.0) (#4618)
* Fix up templates to correct 1.23.0

* add changelog

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
2026-01-25 11:16:42 +11:00
xiejiahe
158fd41a8b
fix(v2): Replace ioutl.Discard with io.Discard (#4877)
* fix(v2): Replace ioutl.Discard with io.Discard

* docs: Update website changelog

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
2026-01-20 11:20:02 +11:00
98 changed files with 5625 additions and 171 deletions

View file

@ -10,7 +10,7 @@ on:
sdk_version: sdk_version:
description: 'macOS SDK version' description: 'macOS SDK version'
required: true required: true
default: '26.1' default: '14.5'
zig_version: zig_version:
description: 'Zig version' description: 'Zig version'
required: true required: true
@ -18,7 +18,17 @@ on:
image_version: image_version:
description: 'Image version tag' description: 'Image version tag'
required: true required: true
default: '1.0.0' default: 'latest'
skip_tests:
description: 'Skip cross-compilation tests'
required: false
default: 'false'
type: boolean
push:
branches:
- v3-alpha
paths:
- 'v3/internal/commands/build_assets/docker/Dockerfile.cross'
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
@ -30,12 +40,17 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
outputs:
image_tag: ${{ steps.vars.outputs.image_version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.branch }} ref: ${{ inputs.branch || github.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -47,6 +62,13 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set build variables
id: vars
run: |
echo "sdk_version=${{ inputs.sdk_version || '14.5' }}" >> $GITHUB_OUTPUT
echo "zig_version=${{ inputs.zig_version || '0.14.0' }}" >> $GITHUB_OUTPUT
echo "image_version=${{ inputs.image_version || 'latest' }}" >> $GITHUB_OUTPUT
- name: Extract metadata - name: Extract metadata
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
@ -54,20 +76,348 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: | tags: |
type=raw,value=latest type=raw,value=latest
type=raw,value=${{ inputs.image_version }} type=raw,value=${{ steps.vars.outputs.image_version }}
type=raw,value=sdk-${{ inputs.sdk_version }} type=raw,value=sdk-${{ steps.vars.outputs.sdk_version }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: v3/internal/setupwizard/docker context: v3/internal/commands/build_assets/docker
file: v3/internal/setupwizard/docker/Dockerfile.cross file: v3/internal/commands/build_assets/docker/Dockerfile.cross
platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: |
${{ steps.meta.outputs.labels }}
io.wails.zig.version=${{ steps.vars.outputs.zig_version }}
io.wails.sdk.version=${{ steps.vars.outputs.sdk_version }}
build-args: | build-args: |
ZIG_VERSION=${{ inputs.zig_version }} ZIG_VERSION=${{ steps.vars.outputs.zig_version }}
MACOS_SDK_VERSION=${{ inputs.sdk_version }} MACOS_SDK_VERSION=${{ steps.vars.outputs.sdk_version }}
IMAGE_VERSION=${{ inputs.image_version }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
# Test cross-compilation for all platforms
test-cross-compile:
needs: build
if: ${{ inputs.skip_tests != 'true' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# Darwin targets (Zig + macOS SDK) - no platform emulation needed
- os: darwin
arch: arm64
platform: ""
expected_file: "Mach-O 64-bit.*arm64"
- os: darwin
arch: amd64
platform: ""
expected_file: "Mach-O 64-bit.*x86_64"
# Linux targets (GCC) - need platform to match architecture
- os: linux
arch: amd64
platform: "linux/amd64"
expected_file: "ELF 64-bit LSB.*x86-64"
- os: linux
arch: arm64
platform: "linux/arm64"
expected_file: "ELF 64-bit LSB.*ARM aarch64"
# Windows targets (Zig + mingw) - no platform emulation needed
- os: windows
arch: amd64
platform: ""
expected_file: "PE32\\+ executable.*x86-64"
- os: windows
arch: arm64
platform: ""
expected_file: "PE32\\+ executable.*Aarch64"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch || github.ref }}
- name: Set up QEMU
if: matrix.platform != ''
uses: docker/setup-qemu-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create test CGO project
run: |
mkdir -p test-project
cd test-project
# Create a minimal CGO test program
cat > main.go << 'EOF'
package main
/*
#include <stdlib.h>
int add(int a, int b) {
return a + b;
}
*/
import "C"
import "fmt"
func main() {
result := C.add(1, 2)
fmt.Printf("CGO test: 1 + 2 = %d\n", result)
}
EOF
cat > go.mod << 'EOF'
module test-cgo
go 1.21
EOF
- name: Build ${{ matrix.os }}/${{ matrix.arch }} (CGO)
run: |
cd test-project
PLATFORM_FLAG=""
if [ -n "${{ matrix.platform }}" ]; then
PLATFORM_FLAG="--platform ${{ matrix.platform }}"
fi
docker run --rm $PLATFORM_FLAG \
-v "$(pwd):/app" \
-e APP_NAME="test-cgo" \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \
${{ matrix.os }} ${{ matrix.arch }}
- name: Verify binary format
run: |
cd test-project/bin
ls -la
# Find the built binary
if [ "${{ matrix.os }}" = "windows" ]; then
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }}.exe 2>/dev/null || ls *.exe | head -1)
else
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1)
fi
echo "Binary: $BINARY"
FILE_OUTPUT=$(file "$BINARY")
echo "File output: $FILE_OUTPUT"
# Verify the binary format matches expected
if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then
echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }}"
else
echo "❌ Binary format mismatch!"
echo "Expected pattern: ${{ matrix.expected_file }}"
echo "Got: $FILE_OUTPUT"
exit 1
fi
- name: Check library dependencies (Linux only)
if: matrix.os == 'linux'
run: |
cd test-project/bin
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1)
echo "## Library Dependencies for $BINARY"
echo ""
# Use readelf to show dynamic dependencies
echo "### NEEDED libraries:"
readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)"
# Verify expected libraries are linked
echo ""
echo "### Verifying required libraries..."
NEEDED=$(readelf -d "$BINARY" | grep NEEDED)
MISSING=""
for lib in libwebkit2gtk-4.1.so libgtk-3.so libglib-2.0.so libc.so; do
if echo "$NEEDED" | grep -q "$lib"; then
echo "✅ $lib"
else
echo "❌ $lib MISSING"
MISSING="$MISSING $lib"
fi
done
if [ -n "$MISSING" ]; then
echo ""
echo "ERROR: Missing required libraries:$MISSING"
exit 1
fi
# Test non-CGO builds (pure Go cross-compilation)
test-non-cgo:
needs: build
if: ${{ inputs.skip_tests != 'true' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- os: darwin
arch: arm64
expected_file: "Mach-O 64-bit.*arm64"
- os: darwin
arch: amd64
expected_file: "Mach-O 64-bit.*x86_64"
- os: linux
arch: amd64
expected_file: "ELF 64-bit LSB"
- os: linux
arch: arm64
expected_file: "ELF 64-bit LSB.*ARM aarch64"
- os: windows
arch: amd64
expected_file: "PE32\\+ executable.*x86-64"
- os: windows
arch: arm64
expected_file: "PE32\\+ executable.*Aarch64"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch || github.ref }}
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create test non-CGO project
run: |
mkdir -p test-project
cd test-project
# Create a pure Go test program (no CGO)
cat > main.go << 'EOF'
package main
import "fmt"
func main() {
fmt.Println("Pure Go cross-compilation test")
}
EOF
cat > go.mod << 'EOF'
module test-pure-go
go 1.21
EOF
- name: Build ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)
run: |
cd test-project
# For non-CGO, we can use any platform since Go handles cross-compilation
# We set CGO_ENABLED=0 to ensure pure Go build
docker run --rm \
-v "$(pwd):/app" \
-e APP_NAME="test-pure-go" \
-e CGO_ENABLED=0 \
--entrypoint /bin/sh \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \
-c "GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o bin/test-pure-go-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} ."
- name: Verify binary format
run: |
cd test-project/bin
ls -la
# Find the built binary
if [ "${{ matrix.os }}" = "windows" ]; then
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}.exe"
else
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}"
fi
echo "Binary: $BINARY"
FILE_OUTPUT=$(file "$BINARY")
echo "File output: $FILE_OUTPUT"
# Verify the binary format matches expected
if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then
echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)"
else
echo "❌ Binary format mismatch!"
echo "Expected pattern: ${{ matrix.expected_file }}"
echo "Got: $FILE_OUTPUT"
exit 1
fi
- name: Check library dependencies (Linux only)
if: matrix.os == 'linux'
run: |
cd test-project/bin
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}"
echo "## Library Dependencies for $BINARY (non-CGO)"
echo ""
# Non-CGO builds should have minimal dependencies (just libc or statically linked)
echo "### NEEDED libraries:"
readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)"
# Verify NO GTK/WebKit libraries (since CGO is disabled)
NEEDED=$(readelf -d "$BINARY" | grep NEEDED || true)
if echo "$NEEDED" | grep -q "libwebkit\|libgtk"; then
echo "❌ ERROR: Non-CGO binary should not link to GTK/WebKit!"
exit 1
else
echo "✅ Confirmed: No GTK/WebKit dependencies (expected for non-CGO)"
fi
# Summary job
test-summary:
needs: [build, test-cross-compile, test-non-cgo]
if: always() && inputs.skip_tests != 'true'
runs-on: ubuntu-latest
steps:
- name: Check test results
run: |
echo "## Cross-Compilation Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.test-cross-compile.result }}" = "success" ]; then
echo "✅ **CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY
else
echo "❌ **CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.test-non-cgo.result }}" = "success" ]; then
echo "✅ **Non-CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY
else
echo "❌ **Non-CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Tested Platforms" >> $GITHUB_STEP_SUMMARY
echo "| Platform | Architecture | CGO | Non-CGO |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------------|-----|---------|" >> $GITHUB_STEP_SUMMARY
echo "| Darwin | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
echo "| Darwin | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
echo "| Linux | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
echo "| Linux | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
echo "| Windows | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
echo "| Windows | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
# Fail if any test failed
if [ "${{ needs.test-cross-compile.result }}" != "success" ] || [ "${{ needs.test-non-cgo.result }}" != "success" ]; then
echo ""
echo "❌ Some tests failed. Check the individual job logs for details."
exit 1
fi

View file

@ -0,0 +1,44 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

50
.github/workflows/claude.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

View file

@ -0,0 +1,76 @@
# Panic Recovery Test
This example demonstrates the Linux signal handler issue (#3965) and verifies the fix using `runtime.ResetSignalHandlers()`.
## The Problem
On Linux, WebKit installs signal handlers without the `SA_ONSTACK` flag, which prevents Go from recovering panics caused by nil pointer dereferences (SIGSEGV). Without the fix, the application crashes with:
```
signal 11 received but handler not on signal stack
fatal error: non-Go code set up signal handler without SA_ONSTACK flag
```
## The Solution
Call `runtime.ResetSignalHandlers()` immediately before code that might panic:
```go
import "github.com/wailsapp/wails/v2/pkg/runtime"
go func() {
defer func() {
if err := recover(); err != nil {
log.Printf("Recovered: %v", err)
}
}()
runtime.ResetSignalHandlers()
// Code that might panic...
}()
```
## How to Reproduce
### Prerequisites
- Linux with WebKit2GTK 4.1 installed
- Go 1.21+
- Wails CLI
### Steps
1. Build the example:
```bash
cd v2/examples/panic-recovery-test
wails build -tags webkit2_41
```
2. Run the application:
```bash
./build/bin/panic-recovery-test
```
3. Wait ~10 seconds (the app auto-calls `Greet` after 5s, then waits another 5s before the nil pointer dereference)
### Expected Result (with fix)
The panic is recovered and you see:
```
------------------------------"invalid memory address or nil pointer dereference"
```
The application continues running.
### Without the fix
Comment out the `runtime.ResetSignalHandlers()` call in `app.go` and rebuild. The application will crash with a fatal signal 11 error.
## Files
- `app.go` - Contains the `Greet` function that demonstrates panic recovery
- `frontend/src/main.js` - Auto-calls `Greet` after 5 seconds to trigger the test
## Related
- Issue: https://github.com/wailsapp/wails/issues/3965
- Original fix PR: https://github.com/wailsapp/wails/pull/2152

View file

@ -0,0 +1,44 @@
package main
import (
"context"
"fmt"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) string {
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("------------------------------%#v\n", err)
}
}()
time.Sleep(5 * time.Second)
// Fix signal handlers right before potential panic using the Wails runtime
runtime.ResetSignalHandlers()
// Nil pointer dereference - causes SIGSEGV
var t *time.Time
fmt.Println(t.Unix())
}()
return fmt.Sprintf("Hello %s, It's show time!", name)
}

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>panic-test</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.js" type="module"></script>
</body>
</html>

View file

@ -0,0 +1,13 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^3.0.7"
}
}

View file

@ -0,0 +1,54 @@
#logo {
display: block;
width: 50%;
height: 50%;
margin: auto;
padding: 10% 0 0;
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
background-origin: content-box;
}
.result {
height: 20px;
line-height: 20px;
margin: 1.5rem auto;
}
.input-box .btn {
width: 60px;
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;
margin: 0 0 0 20px;
padding: 0 8px;
cursor: pointer;
}
.input-box .btn:hover {
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
color: #333333;
}
.input-box .input {
border: none;
border-radius: 3px;
outline: none;
height: 30px;
line-height: 30px;
padding: 0 10px;
background-color: rgba(240, 240, 240, 1);
-webkit-font-smoothing: antialiased;
}
.input-box .input:hover {
border: none;
background-color: rgba(255, 255, 255, 1);
}
.input-box .input:focus {
border: none;
background-color: rgba(255, 255, 255, 1);
}

View file

@ -0,0 +1,93 @@
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View file

@ -0,0 +1,55 @@
import './style.css';
import './app.css';
import logo from './assets/images/logo-universal.png';
import {Greet} from '../wailsjs/go/main/App';
document.querySelector('#app').innerHTML = `
<img id="logo" class="logo">
<div class="result" id="result">Please enter your name below 👇</div>
<div class="input-box" id="input">
<input class="input" id="name" type="text" autocomplete="off" />
<button class="btn" onclick="greet()">Greet</button>
</div>
</div>
`;
document.getElementById('logo').src = logo;
let nameElement = document.getElementById("name");
nameElement.focus();
let resultElement = document.getElementById("result");
// Setup the greet function
window.greet = function () {
// Get name
let name = nameElement.value;
// Check if the input is empty
if (name === "") return;
// Call App.Greet(name)
try {
Greet(name)
.then((result) => {
// Update result with data back from App.Greet()
resultElement.innerText = result;
})
.catch((err) => {
console.error(err);
});
} catch (err) {
console.error(err);
}
};
// Auto-call Greet after 5 seconds to trigger the panic test
setTimeout(() => {
console.log("Auto-calling Greet to trigger panic test...");
Greet("PanicTest")
.then((result) => {
resultElement.innerText = result + " (auto-called - panic will occur in 5s)";
})
.catch((err) => {
console.error("Error:", err);
});
}, 5000);

View file

@ -0,0 +1,26 @@
html {
background-color: rgba(27, 38, 54, 1);
text-align: center;
color: white;
}
body {
margin: 0;
color: white;
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
}
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 400;
src: local(""),
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
}
#app {
height: 100vh;
text-align: center;
}

View file

@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1:string):Promise<string>;

View file

@ -0,0 +1,7 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}

View file

@ -0,0 +1,24 @@
{
"name": "@wailsapp/runtime",
"version": "2.0.0",
"description": "Wails Javascript runtime library",
"main": "runtime.js",
"types": "runtime.d.ts",
"scripts": {
},
"repository": {
"type": "git",
"url": "git+https://github.com/wailsapp/wails.git"
},
"keywords": [
"Wails",
"Javascript",
"Go"
],
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/wails/issues"
},
"homepage": "https://github.com/wailsapp/wails#readme"
}

View file

@ -0,0 +1,249 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export interface Position {
x: number;
y: number;
}
export interface Size {
w: number;
h: number;
}
export interface Screen {
isCurrent: boolean;
isPrimary: boolean;
width : number
height : number
}
// Environment information such as platform, buildtype, ...
export interface EnvironmentInfo {
buildType: string;
platform: string;
arch: string;
}
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
// emits the given event. Optional data may be passed with the event.
// This will trigger any event listeners.
export function EventsEmit(eventName: string, ...data: any): void;
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
// sets up a listener for the given event name, but will only trigger a given number times.
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
// sets up a listener for the given event name, but will only trigger once.
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
// unregisters the listener for the given event name.
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message
export function LogPrint(message: string): void;
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
// logs the given message at the `trace` log level.
export function LogTrace(message: string): void;
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
// logs the given message at the `debug` log level.
export function LogDebug(message: string): void;
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
// logs the given message at the `error` log level.
export function LogError(message: string): void;
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
// logs the given message at the `fatal` log level.
// The application will quit after calling this method.
export function LogFatal(message: string): void;
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
// logs the given message at the `info` log level.
export function LogInfo(message: string): void;
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
// logs the given message at the `warning` log level.
export function LogWarning(message: string): void;
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
// Forces a reload by the main application as well as connected browsers.
export function WindowReload(): void;
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
// Reloads the application frontend.
export function WindowReloadApp(): void;
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
// Sets the window AlwaysOnTop or not on top.
export function WindowSetAlwaysOnTop(b: boolean): void;
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
// *Windows only*
// Sets window theme to system default (dark/light).
export function WindowSetSystemDefaultTheme(): void;
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
// *Windows only*
// Sets window to light theme.
export function WindowSetLightTheme(): void;
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
// *Windows only*
// Sets window to dark theme.
export function WindowSetDarkTheme(): void;
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
// Centers the window on the monitor the window is currently on.
export function WindowCenter(): void;
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
// Sets the text in the window title bar.
export function WindowSetTitle(title: string): void;
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
// Makes the window full screen.
export function WindowFullscreen(): void;
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
// Restores the previous window dimensions and position prior to full screen.
export function WindowUnfullscreen(): void;
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): void;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window.
export function WindowGetSize(): Promise<Size>;
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMaxSize(width: number, height: number): void;
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMinSize(width: number, height: number): void;
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
// Sets the window position relative to the monitor the window is currently on.
export function WindowSetPosition(x: number, y: number): void;
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
// Gets the window position relative to the monitor the window is currently on.
export function WindowGetPosition(): Promise<Position>;
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
// Hides the window.
export function WindowHide(): void;
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
// Shows the window, if it is currently hidden.
export function WindowShow(): void;
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
// Maximises the window to fill the screen.
export function WindowMaximise(): void;
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
// Toggles between Maximised and UnMaximised.
export function WindowToggleMaximise(): void;
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
// Restores the window to the dimensions and position prior to maximising.
export function WindowUnmaximise(): void;
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
// Returns the state of the window, i.e. whether the window is maximised or not.
export function WindowIsMaximised(): Promise<boolean>;
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
// Minimises the window.
export function WindowMinimise(): void;
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
// Restores the window to the dimensions and position prior to minimising.
export function WindowUnminimise(): void;
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
// Returns the state of the window, i.e. whether the window is minimised or not.
export function WindowIsMinimised(): Promise<boolean>;
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
// Returns the state of the window, i.e. whether the window is normal or not.
export function WindowIsNormal(): Promise<boolean>;
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
export function ScreenGetAll(): Promise<Screen[]>;
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
// Opens the given URL in the system browser.
export function BrowserOpenURL(url: string): void;
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
// Returns information about the environment
export function Environment(): Promise<EnvironmentInfo>;
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
// Quits the application.
export function Quit(): void;
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
// Hides the application.
export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application.
export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
// OnFileDropOff removes the drag and drop listeners and handlers.
export function OnFileDropOff() :void
// Check if the file path resolver is available
export function CanResolveFilePaths(): boolean;
// Resolves file paths for an array of files
export function ResolveFilePaths(files: File[]): void

View file

@ -0,0 +1,242 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export function LogPrint(message) {
window.runtime.LogPrint(message);
}
export function LogTrace(message) {
window.runtime.LogTrace(message);
}
export function LogDebug(message) {
window.runtime.LogDebug(message);
}
export function LogInfo(message) {
window.runtime.LogInfo(message);
}
export function LogWarning(message) {
window.runtime.LogWarning(message);
}
export function LogError(message) {
window.runtime.LogError(message);
}
export function LogFatal(message) {
window.runtime.LogFatal(message);
}
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
}
export function EventsOn(eventName, callback) {
return EventsOnMultiple(eventName, callback, -1);
}
export function EventsOff(eventName, ...additionalEventNames) {
return window.runtime.EventsOff(eventName, ...additionalEventNames);
}
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1);
}
export function EventsEmit(eventName) {
let args = [eventName].slice.call(arguments);
return window.runtime.EventsEmit.apply(null, args);
}
export function WindowReload() {
window.runtime.WindowReload();
}
export function WindowReloadApp() {
window.runtime.WindowReloadApp();
}
export function WindowSetAlwaysOnTop(b) {
window.runtime.WindowSetAlwaysOnTop(b);
}
export function WindowSetSystemDefaultTheme() {
window.runtime.WindowSetSystemDefaultTheme();
}
export function WindowSetLightTheme() {
window.runtime.WindowSetLightTheme();
}
export function WindowSetDarkTheme() {
window.runtime.WindowSetDarkTheme();
}
export function WindowCenter() {
window.runtime.WindowCenter();
}
export function WindowSetTitle(title) {
window.runtime.WindowSetTitle(title);
}
export function WindowFullscreen() {
window.runtime.WindowFullscreen();
}
export function WindowUnfullscreen() {
window.runtime.WindowUnfullscreen();
}
export function WindowIsFullscreen() {
return window.runtime.WindowIsFullscreen();
}
export function WindowGetSize() {
return window.runtime.WindowGetSize();
}
export function WindowSetSize(width, height) {
window.runtime.WindowSetSize(width, height);
}
export function WindowSetMaxSize(width, height) {
window.runtime.WindowSetMaxSize(width, height);
}
export function WindowSetMinSize(width, height) {
window.runtime.WindowSetMinSize(width, height);
}
export function WindowSetPosition(x, y) {
window.runtime.WindowSetPosition(x, y);
}
export function WindowGetPosition() {
return window.runtime.WindowGetPosition();
}
export function WindowHide() {
window.runtime.WindowHide();
}
export function WindowShow() {
window.runtime.WindowShow();
}
export function WindowMaximise() {
window.runtime.WindowMaximise();
}
export function WindowToggleMaximise() {
window.runtime.WindowToggleMaximise();
}
export function WindowUnmaximise() {
window.runtime.WindowUnmaximise();
}
export function WindowIsMaximised() {
return window.runtime.WindowIsMaximised();
}
export function WindowMinimise() {
window.runtime.WindowMinimise();
}
export function WindowUnminimise() {
window.runtime.WindowUnminimise();
}
export function WindowSetBackgroundColour(R, G, B, A) {
window.runtime.WindowSetBackgroundColour(R, G, B, A);
}
export function ScreenGetAll() {
return window.runtime.ScreenGetAll();
}
export function WindowIsMinimised() {
return window.runtime.WindowIsMinimised();
}
export function WindowIsNormal() {
return window.runtime.WindowIsNormal();
}
export function BrowserOpenURL(url) {
window.runtime.BrowserOpenURL(url);
}
export function Environment() {
return window.runtime.Environment();
}
export function Quit() {
window.runtime.Quit();
}
export function Hide() {
window.runtime.Hide();
}
export function Show() {
window.runtime.Show();
}
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}
/**
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
*
* @export
* @callback OnFileDropCallback
* @param {number} x - x coordinate of the drop
* @param {number} y - y coordinate of the drop
* @param {string[]} paths - A list of file paths.
*/
/**
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
*
* @export
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
*/
export function OnFileDrop(callback, useDropTarget) {
return window.runtime.OnFileDrop(callback, useDropTarget);
}
/**
* OnFileDropOff removes the drag and drop listeners and handlers.
*/
export function OnFileDropOff() {
return window.runtime.OnFileDropOff();
}
export function CanResolveFilePaths() {
return window.runtime.CanResolveFilePaths();
}
export function ResolveFilePaths(files) {
return window.runtime.ResolveFilePaths(files);
}

View file

@ -0,0 +1,5 @@
module panic-recovery-test
go 1.21
require github.com/wailsapp/wails/v2 v2.11.0

View file

@ -0,0 +1,36 @@
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
// Create an instance of the app structure
app := NewApp()
// Create application with options
err := wails.Run(&options.App{
Title: "panic-test",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
Bind: []interface{}{
app,
},
})
if err != nil {
println("Error:", err.Error())
}
}

View file

@ -0,0 +1,13 @@
{
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "panic-recovery-test",
"outputfilename": "panic-recovery-test",
"frontend:install": "npm install",
"frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev",
"frontend:dev:serverUrl": "auto",
"author": {
"name": "Lea Anthony",
"email": "lea.anthony@gmail.com"
}
}

View file

@ -17,7 +17,7 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/jackmordaunt/icns v1.0.0 github.com/jackmordaunt/icns v1.0.0
github.com/jaypipes/ghw v0.13.0 github.com/jaypipes/ghw v0.21.3
github.com/labstack/echo/v4 v4.13.3 github.com/labstack/echo/v4 v4.13.3
github.com/labstack/gommon v0.4.2 github.com/labstack/gommon v0.4.2
github.com/leaanthony/clir v1.3.0 github.com/leaanthony/clir v1.3.0
@ -51,9 +51,9 @@ require (
atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect atomicgo.dev/schedule v0.1.0 // indirect
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
@ -72,7 +72,7 @@ require (
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/itchyny/gojq v0.12.13 // indirect github.com/itchyny/gojq v0.12.13 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/jaypipes/pcidb v1.0.1 // indirect github.com/jaypipes/pcidb v1.1.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // 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/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
@ -82,7 +82,6 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
@ -101,6 +100,7 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.4 // indirect github.com/yuin/goldmark v1.7.4 // indirect
github.com/yuin/goldmark-emoji v1.0.3 // indirect github.com/yuin/goldmark-emoji v1.0.3 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/crypto v0.33.0 // indirect golang.org/x/crypto v0.33.0 // indirect
golang.org/x/image v0.12.0 // indirect golang.org/x/image v0.12.0 // indirect
golang.org/x/sync v0.11.0 // indirect golang.org/x/sync v0.11.0 // indirect
@ -108,6 +108,6 @@ require (
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect
mvdan.cc/sh/v3 v3.7.0 // indirect mvdan.cc/sh/v3 v3.7.0 // indirect
) )

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= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.0/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/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
@ -24,8 +26,6 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
@ -88,7 +88,7 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
@ -117,10 +117,10 @@ github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ= github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ=
github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo=
github.com/jaypipes/ghw v0.13.0 h1:log8MXuB8hzTNnSktqpXMHc0c/2k/WgjOMSUtnI1RV4= github.com/jaypipes/ghw v0.21.3 h1:v5mUHM+RN854Vqmk49Uh213jyUA4+8uqaRajlYESsh8=
github.com/jaypipes/ghw v0.13.0/go.mod h1:In8SsaDqlb1oTyrbmTC14uy+fbBMvp+xdqX51MidlD8= github.com/jaypipes/ghw v0.21.3/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE=
github.com/jaypipes/pcidb v1.0.1 h1:WB2zh27T3nwg8AE8ei81sNRb9yWBii3JGNJtT7K9Oic= github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro=
github.com/jaypipes/pcidb v1.0.1/go.mod h1:6xYUz/yYEyOkIkUt2t2J2folIuZ4Yg6uByCGFXMCeE4= github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
@ -178,8 +178,6 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
@ -263,6 +261,8 @@ github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4=
github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@ -340,7 +340,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
@ -348,7 +347,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 h1:eeH1AIcPvSc0Z25ThsYF+Xoqbn0CI/YnXVYoTLFdGQw=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= howett.net/plist v1.0.2-0.20250314012144-ee69052608d9/go.mod h1:fyFX5Hj5tP1Mpk8obqA9MZgXT416Q5711SDT7dQLTLk=
mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=
mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8=

View file

@ -69,6 +69,21 @@ void UpdateMenuItem(void* nsmenuitem, int checked);
void RunMainLoop(void); void RunMainLoop(void);
void ReleaseContext(void *inctx); void ReleaseContext(void *inctx);
/* Notifications */
bool IsNotificationAvailable(void *inctx);
bool CheckBundleIdentifier(void *inctx);
bool EnsureDelegateInitialized(void *inctx);
void RequestNotificationAuthorization(void *inctx, int channelID);
void CheckNotificationAuthorization(void *inctx, int channelID);
void SendNotification(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json);
void SendNotificationWithActions(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json);
void RegisterNotificationCategory(void *inctx, int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle);
void RemoveNotificationCategory(void *inctx, int channelID, const char *categoryId);
void RemoveAllPendingNotifications(void *inctx);
void RemovePendingNotification(void *inctx, const char *identifier);
void RemoveAllDeliveredNotifications(void *inctx);
void RemoveDeliveredNotification(void *inctx, const char *identifier);
NSString* safeInit(const char* input); NSString* safeInit(const char* input);
#endif /* Application_h */ #endif /* Application_h */

View file

@ -367,6 +367,74 @@ void AppendSeparator(void* inMenu) {
} }
bool IsNotificationAvailable(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
return [ctx IsNotificationAvailable];
}
bool CheckBundleIdentifier(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
return [ctx CheckBundleIdentifier];
}
bool EnsureDelegateInitialized(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
return [ctx EnsureDelegateInitialized];
}
void RequestNotificationAuthorization(void *inctx, int channelID) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RequestNotificationAuthorization:channelID];
}
void CheckNotificationAuthorization(void *inctx, int channelID) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx CheckNotificationAuthorization:channelID];
}
void SendNotification(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx SendNotification:channelID :identifier :title :subtitle :body :data_json];
}
void SendNotificationWithActions(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx SendNotificationWithActions:channelID :identifier :title :subtitle :body :categoryId :actions_json];
}
void RegisterNotificationCategory(void *inctx, int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RegisterNotificationCategory:channelID :categoryId :actions_json :hasReplyField :replyPlaceholder :replyButtonTitle];
}
void RemoveNotificationCategory(void *inctx, int channelID, const char *categoryId) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RemoveNotificationCategory:channelID :categoryId];
}
void RemoveAllPendingNotifications(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RemoveAllPendingNotifications];
}
void RemovePendingNotification(void *inctx, const char *identifier) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RemovePendingNotification:identifier];
}
void RemoveAllDeliveredNotifications(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RemoveAllDeliveredNotifications];
}
void RemoveDeliveredNotification(void *inctx, const char *identifier) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RemoveDeliveredNotification:identifier];
}
void Run(void *inctx, const char* url) { void Run(void *inctx, const char* url) {
WailsContext *ctx = (__bridge WailsContext*) inctx; WailsContext *ctx = (__bridge WailsContext*) inctx;

View file

@ -92,10 +92,24 @@ struct Preferences {
- (void) ShowApplication; - (void) ShowApplication;
- (void) Quit; - (void) Quit;
-(void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength; - (void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength;
- (void) OpenFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)allowDirectories :(bool)allowFiles :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)resolveAliases :(bool)showHiddenFiles :(bool)allowMultipleSelection :(NSString*)filters; - (void) OpenFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)allowDirectories :(bool)allowFiles :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)resolveAliases :(bool)showHiddenFiles :(bool)allowMultipleSelection :(NSString*)filters;
- (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters; - (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters;
- (bool) IsNotificationAvailable;
- (bool) CheckBundleIdentifier;
- (bool) EnsureDelegateInitialized;
- (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 *)dataJSON;
- (void) SendNotificationWithActions:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)categoryId :(const char *)actionsJSON;
- (void) RegisterNotificationCategory:(int)channelID :(const char *)categoryId :(const char *)actionsJSON :(bool)hasReplyField :(const char *)replyPlaceholder :(const char *)replyButtonTitle;
- (void) RemoveNotificationCategory:(int)channelID :(const char *)categoryId;
- (void) RemoveAllPendingNotifications;
- (void) RemovePendingNotification:(const char *)identifier;
- (void) RemoveAllDeliveredNotifications;
- (void) RemoveDeliveredNotification:(const char *)identifier;
- (void) loadRequest:(NSString*)url; - (void) loadRequest:(NSString*)url;
- (void) ExecJS:(NSString*)script; - (void) ExecJS:(NSString*)script;
- (NSScreen*) getCurrentScreen; - (NSScreen*) getCurrentScreen;

View file

@ -5,6 +5,7 @@
// Created by Lea Anthony on 10/10/21. // Created by Lea Anthony on 10/10/21.
// //
#include "Application.h"
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <WebKit/WebKit.h> #import <WebKit/WebKit.h>
#import "WailsContext.h" #import "WailsContext.h"
@ -36,6 +37,14 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
@end @end
// Notifications
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400
#import <UserNotifications/UserNotifications.h>
#endif
extern void captureResult(int channelID, bool success, const char* error);
extern void didReceiveNotificationResponse(const char *jsonPayload, const char* error);
@implementation WailsContext @implementation WailsContext
- (void) SetSize:(int)width :(int)height { - (void) SetSize:(int)width :(int)height {
@ -723,6 +732,357 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
} }
/***** Notifications ******/
- (bool) IsNotificationAvailable {
if (@available(macOS 10.14, *)) {
return YES;
} else {
return NO;
}
}
- (bool) CheckBundleIdentifier {
NSBundle *main = [NSBundle mainBundle];
if (main.bundleIdentifier == nil) {
return NO;
}
return YES;
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler API_AVAILABLE(macos(10.14)) {
UNNotificationPresentationOptions options = UNNotificationPresentationOptionSound;
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 API_AVAILABLE(macos(10.14)) {
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:@"categoryId"];
}
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();
}
- (bool) EnsureDelegateInitialized {
if (@available(macOS 10.14, *)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = (id<UNUserNotificationCenterDelegate>)self;
return YES;
}
return NO;
}
- (void) RequestNotificationAuthorization :(int)channelID {
if (@available(macOS 10.14, *)) {
if (![self 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);
}
}];
} else {
captureResult(channelID, false, "Notifications not available on macOS versions prior to 10.14");
}
}
- (void) CheckNotificationAuthorization :(int) channelID {
if (@available(macOS 10.14, *)) {
if (![self 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);
}];
} else {
captureResult(channelID, false, "Notifications not available on macOS versions prior to 10.14");
}
}
- (UNMutableNotificationContent *)createNotificationContent:(const char *)title subtitle:(const char *)subtitle body:(const char *)body dataJSON:(const char *)dataJSON error:(NSError **)contentError API_AVAILABLE(macos(10.14)) {
if (title == NULL) title = "";
if (body == NULL) body = "";
NSString *nsTitle = [NSString stringWithUTF8String:title];
NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
NSString *nsBody = [NSString stringWithUTF8String:body];
UNMutableNotificationContent *content = [[[UNMutableNotificationContent alloc] init] autorelease];
content.title = nsTitle;
if (![nsSubtitle isEqualToString:@""]) {
content.subtitle = nsSubtitle;
}
content.body = nsBody;
content.sound = [UNNotificationSound defaultSound];
// Parse JSON data if provided
if (dataJSON) {
NSString *dataJsonStr = [NSString stringWithUTF8String:dataJSON];
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) {
if (contentError) *contentError = error;
}
}
return content;
}
- (void) sendNotificationWithRequest:(UNNotificationRequest *)request channelID:(int)channelID API_AVAILABLE(macos(10.14)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[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) SendNotification:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)dataJSON API_AVAILABLE(macos(10.14)) {
if (![self EnsureDelegateInitialized]) {
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
captureResult(channelID, false, [errorMsg UTF8String]);
return;
}
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
NSError *contentError = nil;
UNMutableNotificationContent *content = [self createNotificationContent:title subtitle:subtitle body:body dataJSON:dataJSON error:&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];
[self sendNotificationWithRequest:request channelID:channelID];
}
- (void) SendNotificationWithActions:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)categoryId :(const char *)dataJSON API_AVAILABLE(macos(10.14)) {
if (![self EnsureDelegateInitialized]) {
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
captureResult(channelID, false, [errorMsg UTF8String]);
return;
}
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
NSError *contentError = nil;
UNMutableNotificationContent *content = [self createNotificationContent:title subtitle:subtitle body:body dataJSON:dataJSON error:&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];
[self sendNotificationWithRequest:request channelID:channelID];
}
- (void) RegisterNotificationCategory:(int)channelID :(const char *)categoryId :(const char *)actionsJSON :(bool)hasReplyField :(const char *)replyPlaceholder :(const char *)replyButtonTitle API_AVAILABLE(macos(10.14)) {
if (![self 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 = actionsJSON ? [NSString stringWithUTF8String:actionsJSON] : @"[]";
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) {
// Defensive NULL checks: if hasReplyField is true, both strings must be non-NULL
if (!replyPlaceholder || !replyButtonTitle) {
NSString *errorMsg = @"hasReplyField is true but replyPlaceholder or replyButtonTitle is NULL";
captureResult(channelID, false, [errorMsg UTF8String]);
return;
}
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 identifier if found
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 API_AVAILABLE(macos(10.14)) {
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories];
// Find and remove the matching category
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 API_AVAILABLE(macos(10.14)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center removeAllPendingNotificationRequests];
}
- (void) RemovePendingNotification:(const char *)identifier API_AVAILABLE(macos(10.14)) {
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center removePendingNotificationRequestsWithIdentifiers:@[nsIdentifier]];
}
- (void) RemoveAllDeliveredNotifications API_AVAILABLE(macos(10.14)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center removeAllDeliveredNotifications];
}
- (void) RemoveDeliveredNotification:(const char *)identifier API_AVAILABLE(macos(10.14)) {
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center removeDeliveredNotificationsWithIdentifiers:@[nsIdentifier]];
}
- (void) SetAbout :(NSString*)title :(NSString*)description :(void*)imagedata :(int)datalen { - (void) SetAbout :(NSString*)title :(NSString*)description :(void*)imagedata :(int)datalen {
self.aboutTitle = title; self.aboutTitle = title;
self.aboutDescription = description; self.aboutDescription = description;
@ -731,7 +1091,7 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
self.aboutImage = [[NSImage alloc] initWithData:imageData]; self.aboutImage = [[NSImage alloc] initWithData:imageData];
} }
-(void) About { - (void) About {
WailsAlert *alert = [WailsAlert new]; WailsAlert *alert = [WailsAlert new];
[alert setAlertStyle:NSAlertStyleInformational]; [alert setAlertStyle:NSAlertStyleInformational];

View file

@ -0,0 +1,465 @@
//go:build darwin
// +build darwin
package darwin
/*
#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 "Application.h"
#import "WailsContext.h"
*/
import "C"
import (
"context"
"encoding/json"
"fmt"
"os"
"sync"
"time"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend"
)
// Package-scoped variable only accessible within this file
var (
currentFrontend *Frontend
frontendMutex sync.RWMutex
// Notification channels
channels map[int]chan notificationChannel
channelsLock sync.Mutex
nextChannelID int
notificationResultCallback func(result frontend.NotificationResult)
callbackLock sync.RWMutex
)
const DefaultActionIdentifier = "DEFAULT_ACTION"
const AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier"
// setCurrentFrontend sets the current frontend instance
// This is called when RequestNotificationAuthorization or CheckNotificationAuthorization is called
func setCurrentFrontend(f *Frontend) {
frontendMutex.Lock()
defer frontendMutex.Unlock()
currentFrontend = f
}
// getCurrentFrontend gets the current frontend instance
func getCurrentFrontend() *Frontend {
frontendMutex.RLock()
defer frontendMutex.RUnlock()
return currentFrontend
}
type notificationChannel struct {
Success bool
Error error
}
func (f *Frontend) InitializeNotifications() error {
if !f.IsNotificationAvailable() {
return fmt.Errorf("notifications are not available on this system")
}
if !f.checkBundleIdentifier() {
return fmt.Errorf("notifications require a valid bundle identifier")
}
if !bool(C.EnsureDelegateInitialized(f.mainWindow.context)) {
return fmt.Errorf("failed to initialize notification center delegate")
}
channels = make(map[int]chan notificationChannel)
nextChannelID = 0
setCurrentFrontend(f)
return nil
}
// CleanupNotifications is a macOS stub that does nothing.
// (Linux-specific cleanup)
func (f *Frontend) CleanupNotifications() {
// No cleanup needed on macOS
}
func (f *Frontend) IsNotificationAvailable() bool {
return bool(C.IsNotificationAvailable(f.mainWindow.context))
}
func (f *Frontend) checkBundleIdentifier() bool {
return bool(C.CheckBundleIdentifier(f.mainWindow.context))
}
func (f *Frontend) RequestNotificationAuthorization() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
defer cancel()
id, resultCh := f.registerChannel()
C.RequestNotificationAuthorization(f.mainWindow.context, C.int(id))
select {
case result := <-resultCh:
close(resultCh)
return result.Success, result.Error
case <-ctx.Done():
f.cleanupChannel(id)
return false, fmt.Errorf("notification authorization timed out after 3 minutes: %w", ctx.Err())
}
}
func (f *Frontend) CheckNotificationAuthorization() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
id, resultCh := f.registerChannel()
C.CheckNotificationAuthorization(f.mainWindow.context, C.int(id))
select {
case result := <-resultCh:
close(resultCh)
return result.Success, result.Error
case <-ctx.Done():
f.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 (f *Frontend) SendNotification(options frontend.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 := f.registerChannel()
C.SendNotification(f.mainWindow.context, C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cDataJSON)
select {
case result := <-resultCh:
close(resultCh)
if !result.Success {
if result.Error != nil {
return result.Error
}
return fmt.Errorf("sending notification failed")
}
return nil
case <-ctx.Done():
f.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 (f *Frontend) SendNotificationWithActions(options frontend.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 := f.registerChannel()
C.SendNotificationWithActions(f.mainWindow.context, C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cCategoryID, cDataJSON)
select {
case result := <-resultCh:
close(resultCh)
if !result.Success {
if result.Error != nil {
return result.Error
}
return fmt.Errorf("sending notification failed")
}
return nil
case <-ctx.Done():
f.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 (f *Frontend) RegisterNotificationCategory(category frontend.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 := f.registerChannel()
C.RegisterNotificationCategory(f.mainWindow.context, C.int(id), cCategoryID, cActionsJSON, C.bool(category.HasReplyField),
cReplyPlaceholder, cReplyButtonTitle)
select {
case result := <-resultCh:
close(resultCh)
if !result.Success {
if result.Error != nil {
return result.Error
}
return fmt.Errorf("category registration failed")
}
return nil
case <-ctx.Done():
f.cleanupChannel(id)
return fmt.Errorf("category registration timed out: %w", ctx.Err())
}
}
// RemoveNotificationCategory remove a previously registered NotificationCategory.
func (f *Frontend) 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 := f.registerChannel()
C.RemoveNotificationCategory(f.mainWindow.context, C.int(id), cCategoryID)
select {
case result := <-resultCh:
close(resultCh)
if !result.Success {
if result.Error != nil {
return result.Error
}
return fmt.Errorf("category removal failed")
}
return nil
case <-ctx.Done():
f.cleanupChannel(id)
return fmt.Errorf("category removal timed out: %w", ctx.Err())
}
}
// RemoveAllPendingNotifications removes all pending notifications.
func (f *Frontend) RemoveAllPendingNotifications() error {
C.RemoveAllPendingNotifications(f.mainWindow.context)
return nil
}
// RemovePendingNotification removes a pending notification matching the unique identifier.
func (f *Frontend) RemovePendingNotification(identifier string) error {
cIdentifier := C.CString(identifier)
defer C.free(unsafe.Pointer(cIdentifier))
C.RemovePendingNotification(f.mainWindow.context, cIdentifier)
return nil
}
// RemoveAllDeliveredNotifications removes all delivered notifications.
func (f *Frontend) RemoveAllDeliveredNotifications() error {
C.RemoveAllDeliveredNotifications(f.mainWindow.context)
return nil
}
// RemoveDeliveredNotification removes a delivered notification matching the unique identifier.
func (f *Frontend) RemoveDeliveredNotification(identifier string) error {
cIdentifier := C.CString(identifier)
defer C.free(unsafe.Pointer(cIdentifier))
C.RemoveDeliveredNotification(f.mainWindow.context, 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 (f *Frontend) RemoveNotification(identifier string) error {
return nil
}
func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) {
callbackLock.Lock()
notificationResultCallback = callback
callbackLock.Unlock()
}
//export captureResult
func captureResult(channelID C.int, success C.bool, errorMsg *C.char) {
f := getCurrentFrontend()
if f == nil {
return
}
resultCh, exists := f.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,
}
}
//export didReceiveNotificationResponse
func didReceiveNotificationResponse(jsonPayload *C.char, err *C.char) {
result := frontend.NotificationResult{}
if err != nil {
errMsg := C.GoString(err)
result.Error = fmt.Errorf("notification response error: %s", errMsg)
handleNotificationResult(result)
return
}
if jsonPayload == nil {
result.Error = fmt.Errorf("received nil JSON payload in notification response")
handleNotificationResult(result)
return
}
payload := C.GoString(jsonPayload)
var response frontend.NotificationResponse
if err := json.Unmarshal([]byte(payload), &response); err != nil {
result.Error = fmt.Errorf("failed to unmarshal notification response: %w", err)
handleNotificationResult(result)
return
}
if response.ActionIdentifier == AppleDefaultActionIdentifier {
response.ActionIdentifier = DefaultActionIdentifier
}
result.Response = response
handleNotificationResult(result)
}
func handleNotificationResult(result frontend.NotificationResult) {
callbackLock.Lock()
callback := notificationResultCallback
callbackLock.Unlock()
if callback != nil {
go func() {
defer func() {
if r := recover(); r != nil {
// Log panic but don't crash the app
fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r)
}
}()
callback(result)
}()
}
}
// Helper methods
func (f *Frontend) registerChannel() (int, chan notificationChannel) {
channelsLock.Lock()
defer channelsLock.Unlock()
// Initialize channels map if it's nil
if channels == nil {
channels = make(map[int]chan notificationChannel)
nextChannelID = 0
}
id := nextChannelID
nextChannelID++
resultCh := make(chan notificationChannel, 1)
channels[id] = resultCh
return id, resultCh
}
func (f *Frontend) GetChannel(id int) (chan notificationChannel, bool) {
channelsLock.Lock()
defer channelsLock.Unlock()
if channels == nil {
return nil, false
}
ch, exists := channels[id]
if exists {
delete(channels, id)
}
return ch, exists
}
func (f *Frontend) cleanupChannel(id int) {
channelsLock.Lock()
defer channelsLock.Unlock()
if channels == nil {
return
}
if ch, exists := channels[id]; exists {
delete(channels, id)
close(ch)
}
}

View file

@ -73,6 +73,16 @@ static void install_signal_handlers()
#endif #endif
} }
static gboolean install_signal_handlers_idle(gpointer data) {
(void)data;
install_signal_handlers();
return G_SOURCE_REMOVE;
}
static void fix_signal_handlers_after_gtk_init() {
g_idle_add(install_signal_handlers_idle, NULL);
}
*/ */
import "C" import "C"
import ( import (
@ -204,7 +214,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
result.mainWindow = NewWindow(appoptions, result.debug, result.devtoolsEnabled) result.mainWindow = NewWindow(appoptions, result.debug, result.devtoolsEnabled)
C.install_signal_handlers() C.fix_signal_handlers_after_gtk_init()
if appoptions.Linux != nil && appoptions.Linux.ProgramName != "" { if appoptions.Linux != nil && appoptions.Linux.ProgramName != "" {
prgname := C.CString(appoptions.Linux.ProgramName) prgname := C.CString(appoptions.Linux.ProgramName)

View file

@ -0,0 +1,594 @@
//go:build linux
// +build linux
package linux
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/godbus/dbus/v5"
"github.com/wailsapp/wails/v2/internal/frontend"
)
var (
conn *dbus.Conn
categories map[string]frontend.NotificationCategory = make(map[string]frontend.NotificationCategory)
categoriesLock sync.RWMutex
notifications map[uint32]*notificationData = make(map[uint32]*notificationData)
notificationsLock sync.RWMutex
notificationResultCallback func(result frontend.NotificationResult)
callbackLock 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"
DefaultActionIdentifier = "DEFAULT_ACTION"
)
// Creates a new Notifications Service.
func (f *Frontend) InitializeNotifications() error {
// Clean up any previous initialization
f.CleanupNotifications()
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable: %w", err)
}
appName = filepath.Base(exe)
_conn, err := dbus.ConnectSessionBus()
if err != nil {
return fmt.Errorf("failed to connect to session bus: %w", err)
}
conn = _conn
if err := f.loadCategories(); err != nil {
f.logger.Warning("Failed to load notification categories: %v", err)
}
var signalCtx context.Context
signalCtx, cancel = context.WithCancel(context.Background())
if err := f.setupSignalHandling(signalCtx); err != nil {
return fmt.Errorf("failed to set up notification signal handling: %w", err)
}
return nil
}
// CleanupNotifications cleans up notification resources
func (f *Frontend) CleanupNotifications() {
if cancel != nil {
cancel()
cancel = nil
}
if conn != nil {
conn.Close()
conn = nil
}
}
func (f *Frontend) IsNotificationAvailable() bool {
return true
}
// RequestNotificationAuthorization is a Linux stub that always returns true, nil.
// (authorization is macOS-specific)
func (f *Frontend) RequestNotificationAuthorization() (bool, error) {
return true, nil
}
// CheckNotificationAuthorization is a Linux stub that always returns true.
// (authorization is macOS-specific)
func (f *Frontend) CheckNotificationAuthorization() (bool, error) {
return true, nil
}
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
func (f *Frontend) SendNotification(options frontend.NotificationOptions) error {
if conn == nil {
return fmt.Errorf("notifications not initialized")
}
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 := conn.Object(dbusNotificationInterface, dbusNotificationPath)
call := obj.Call(
dbusNotificationInterface+".Notify",
0,
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,
}
notificationsLock.Lock()
notifications[dbusID] = notification
notificationsLock.Unlock()
return nil
}
// SendNotificationWithActions sends a notification with additional actions.
func (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error {
if conn == nil {
return fmt.Errorf("notifications not initialized")
}
categoriesLock.RLock()
category, exists := categories[options.CategoryID]
categoriesLock.RUnlock()
if options.CategoryID == "" || !exists {
// Fall back to basic notification
return f.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 := conn.Object(dbusNotificationInterface, dbusNotificationPath)
call := obj.Call(
dbusNotificationInterface+".Notify",
0,
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,
}
notificationsLock.Lock()
notifications[dbusID] = notification
notificationsLock.Unlock()
return nil
}
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
func (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error {
categoriesLock.Lock()
categories[category.ID] = category
categoriesLock.Unlock()
if err := f.saveCategories(); err != nil {
f.logger.Warning("Failed to save notification categories: %v", err)
}
return nil
}
// RemoveNotificationCategory removes a previously registered NotificationCategory.
func (f *Frontend) RemoveNotificationCategory(categoryId string) error {
categoriesLock.Lock()
delete(categories, categoryId)
categoriesLock.Unlock()
if err := f.saveCategories(); err != nil {
f.logger.Warning("Failed to save notification categories: %v", err)
}
return nil
}
// RemoveAllPendingNotifications attempts to remove all active notifications.
func (f *Frontend) RemoveAllPendingNotifications() error {
notificationsLock.Lock()
dbusIDs := make([]uint32, 0, len(notifications))
for id := range notifications {
dbusIDs = append(dbusIDs, id)
}
notificationsLock.Unlock()
for _, id := range dbusIDs {
f.closeNotification(id)
}
return nil
}
// RemovePendingNotification removes a pending notification.
func (f *Frontend) RemovePendingNotification(identifier string) error {
var dbusID uint32
found := false
notificationsLock.Lock()
for id, notif := range notifications {
if notif.ID == identifier {
dbusID = id
found = true
break
}
}
notificationsLock.Unlock()
if !found {
return nil
}
return f.closeNotification(dbusID)
}
// RemoveAllDeliveredNotifications functionally equivalent to RemoveAllPendingNotification on Linux.
func (f *Frontend) RemoveAllDeliveredNotifications() error {
return f.RemoveAllPendingNotifications()
}
// RemoveDeliveredNotification functionally equivalent RemovePendingNotification on Linux.
func (f *Frontend) RemoveDeliveredNotification(identifier string) error {
return f.RemovePendingNotification(identifier)
}
// RemoveNotification removes a notification by identifier.
func (f *Frontend) RemoveNotification(identifier string) error {
return f.RemovePendingNotification(identifier)
}
func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) {
callbackLock.Lock()
defer callbackLock.Unlock()
notificationResultCallback = callback
}
// Helper method to close a notification.
func (f *Frontend) closeNotification(id uint32) error {
if conn == nil {
return fmt.Errorf("notifications not initialized")
}
obj := 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 (f *Frontend) 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, 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 (f *Frontend) saveCategories() error {
configDir, err := f.getConfigDir()
if err != nil {
return err
}
categoriesFile := filepath.Join(configDir, "notification-categories.json")
categoriesLock.RLock()
categoriesData, err := json.MarshalIndent(categories, "", " ")
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 (f *Frontend) loadCategories() error {
configDir, err := f.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]frontend.NotificationCategory)
if err := json.Unmarshal(categoriesData, &_categories); err != nil {
return fmt.Errorf("failed to unmarshal notification categories: %w", err)
}
categoriesLock.Lock()
categories = _categories
categoriesLock.Unlock()
return nil
}
// Setup signal handling for notification actions.
func (f *Frontend) setupSignalHandling(ctx context.Context) error {
if err := conn.AddMatchSignal(
dbus.WithMatchInterface(dbusNotificationInterface),
dbus.WithMatchMember("ActionInvoked"),
); err != nil {
return err
}
if err := conn.AddMatchSignal(
dbus.WithMatchInterface(dbusNotificationInterface),
dbus.WithMatchMember("NotificationClosed"),
); err != nil {
return err
}
c := make(chan *dbus.Signal, 10)
conn.Signal(c)
go f.handleSignals(ctx, c)
return nil
}
// Handle incoming D-Bus signals.
func (f *Frontend) 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":
f.handleActionInvoked(signal)
case dbusNotificationInterface + ".NotificationClosed":
f.handleNotificationClosed(signal)
}
}
}
}
// Handle ActionInvoked signal.
func (f *Frontend) 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
}
notificationsLock.Lock()
notification, exists := notifications[dbusID]
if exists {
delete(notifications, dbusID)
}
notificationsLock.Unlock()
if !exists {
return
}
appActionID, ok := notification.ActionMap[actionID]
if !ok {
appActionID = actionID
}
response := frontend.NotificationResponse{
ID: notification.ID,
ActionIdentifier: appActionID,
Title: notification.Title,
Subtitle: notification.Subtitle,
Body: notification.Body,
CategoryID: notification.CategoryID,
UserInfo: notification.Data,
}
result := frontend.NotificationResult{
Response: response,
}
handleNotificationResult(result)
}
func handleNotificationResult(result frontend.NotificationResult) {
callbackLock.Lock()
callback := notificationResultCallback
callbackLock.Unlock()
if callback != nil {
go func() {
defer func() {
if r := recover(); r != nil {
// Log panic but don't crash the app
fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r)
}
}()
callback(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 (f *Frontend) 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
}
notificationsLock.Lock()
notification, exists := notifications[dbusID]
if exists {
delete(notifications, dbusID)
}
notificationsLock.Unlock()
if !exists {
return
}
if reason == 2 {
response := frontend.NotificationResponse{
ID: notification.ID,
ActionIdentifier: DefaultActionIdentifier,
Title: notification.Title,
Subtitle: notification.Subtitle,
Body: notification.Body,
CategoryID: notification.CategoryID,
UserInfo: notification.Data,
}
result := frontend.NotificationResult{
Response: response,
}
handleNotificationResult(result)
}
}

View file

@ -0,0 +1,489 @@
//go:build windows
// +build windows
package windows
import (
"encoding/base64"
"encoding/json"
"log"
"sync"
wintoast "git.sr.ht/~jackmordaunt/go-toast/v2/wintoast"
"github.com/google/uuid"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
"fmt"
"os"
"path/filepath"
_ "unsafe" // for go:linkname
"git.sr.ht/~jackmordaunt/go-toast/v2"
"golang.org/x/sys/windows/registry"
)
var (
categories map[string]frontend.NotificationCategory
categoriesLock sync.RWMutex
appName string
appGUID string
iconPath string = ""
exePath string
iconOnce sync.Once
iconErr error
notificationResultCallback func(result frontend.NotificationResult)
callbackLock sync.RWMutex
)
const DefaultActionIdentifier = "DEFAULT_ACTION"
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 frontend.NotificationOptions `json:"payload,omitempty"`
}
func (f *Frontend) InitializeNotifications() error {
categoriesLock.Lock()
defer categoriesLock.Unlock()
categories = make(map[string]frontend.NotificationCategory)
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable: %w", err)
}
exePath = exe
appName = filepath.Base(exePath)
appGUID, err = getGUID()
if err != nil {
return err
}
iconPath = filepath.Join(os.TempDir(), appName+appGUID+".png")
// Create the registry key for the toast activator
key, _, err := registry.CreateKey(registry.CURRENT_USER,
`Software\Classes\CLSID\`+appGUID+`\LocalServer32`, registry.ALL_ACCESS)
if err != nil {
return fmt.Errorf("failed to create CLSID key: %w", err)
}
defer key.Close()
if err := key.SetStringValue("", fmt.Sprintf("\"%s\" %%1", exePath)); err != nil {
return fmt.Errorf("failed to set CLSID server path: %w", err)
}
toast.SetAppData(toast.AppData{
AppID: appName,
GUID: appGUID,
IconPath: iconPath,
ActivationExe: exePath,
})
toast.SetActivationCallback(func(args string, data []toast.UserData) {
result := frontend.NotificationResult{}
actionIdentifier, options, err := parseNotificationResponse(args)
if err != nil {
result.Error = err
} else {
// Subtitle is retained but was not shown with the notification
response := frontend.NotificationResponse{
ID: options.ID,
ActionIdentifier: actionIdentifier,
Title: options.Title,
Subtitle: options.Subtitle,
Body: options.Body,
CategoryID: options.CategoryID,
UserInfo: options.Data,
}
if userText, found := getUserText(data); found {
response.UserText = userText
}
result.Response = response
}
handleNotificationResult(result)
})
// Register the COM class factory for toast activation.
// This is required for Windows to activate the app when users interact with notifications.
// The go-toast library's SetAppData and SetActivationCallback handle the callback setup,
// but the COM class factory registration is not exposed via public APIs, so we use
// go:linkname to access the internal registerClassFactory function.
if err := registerToastClassFactory(wintoast.ClassFactory); err != nil {
return fmt.Errorf("CoRegisterClassObject failed: %w", err)
}
return loadCategoriesFromRegistry()
}
// registerToastClassFactory registers the COM class factory required for Windows toast notification activation.
// This function uses go:linkname to access the unexported registerClassFactory function from go-toast.
// The class factory is necessary for Windows COM activation when users click notification actions.
// Without this registration, notification actions will not activate the application.
//
// This is a workaround until go-toast exports this functionality via a public API.
// See: https://git.sr.ht/~jackmordaunt/go-toast
//
//go:linkname registerToastClassFactory git.sr.ht/~jackmordaunt/go-toast/v2/wintoast.registerClassFactory
func registerToastClassFactory(factory *wintoast.IClassFactory) error
// CleanupNotifications is a Windows stub that does nothing.
// (Linux-specific cleanup)
func (f *Frontend) CleanupNotifications() {
// No cleanup needed on Windows
}
func (f *Frontend) IsNotificationAvailable() bool {
return true
}
func (f *Frontend) RequestNotificationAuthorization() (bool, error) {
return true, nil
}
func (f *Frontend) 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 (f *Frontend) SendNotification(options frontend.NotificationOptions) error {
if err := f.saveIconToDir(); err != nil {
f.logger.Warning("Error saving icon: %v", err)
}
n := toast.Notification{
Title: options.Title,
Body: options.Body,
ActivationType: toast.Foreground,
ActivationArguments: DefaultActionIdentifier,
}
encodedPayload, err := 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 (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error {
if err := f.saveIconToDir(); err != nil {
f.logger.Warning("Error saving icon: %v", err)
}
categoriesLock.RLock()
nCategory, categoryExists := categories[options.CategoryID]
categoriesLock.RUnlock()
if options.CategoryID == "" || !categoryExists {
f.logger.Warning("Category '%s' not found, sending basic notification without actions", options.CategoryID)
return f.SendNotification(options)
}
n := toast.Notification{
Title: options.Title,
Body: options.Body,
ActivationType: toast.Foreground,
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",
})
}
encodedPayload, err := 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 := 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 (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error {
categoriesLock.Lock()
defer categoriesLock.Unlock()
categories[category.ID] = frontend.NotificationCategory{
ID: category.ID,
Actions: category.Actions,
HasReplyField: category.HasReplyField,
ReplyPlaceholder: category.ReplyPlaceholder,
ReplyButtonTitle: category.ReplyButtonTitle,
}
return saveCategoriesToRegistry()
}
// RemoveNotificationCategory removes a previously registered NotificationCategory.
func (f *Frontend) RemoveNotificationCategory(categoryId string) error {
categoriesLock.Lock()
defer categoriesLock.Unlock()
delete(categories, categoryId)
return saveCategoriesToRegistry()
}
// RemoveAllPendingNotifications is a Windows stub that always returns nil.
// (macOS and Linux only)
func (f *Frontend) RemoveAllPendingNotifications() error {
return nil
}
// RemovePendingNotification is a Windows stub that always returns nil.
// (macOS and Linux only)
func (f *Frontend) RemovePendingNotification(_ string) error {
return nil
}
// RemoveAllDeliveredNotifications is a Windows stub that always returns nil.
// (macOS and Linux only)
func (f *Frontend) RemoveAllDeliveredNotifications() error {
return nil
}
// RemoveDeliveredNotification is a Windows stub that always returns nil.
// (macOS and Linux only)
func (f *Frontend) RemoveDeliveredNotification(_ string) error {
return nil
}
// RemoveNotification is a Windows stub that always returns nil.
// (Linux-specific)
func (f *Frontend) RemoveNotification(identifier string) error {
return nil
}
func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) {
callbackLock.Lock()
defer callbackLock.Unlock()
notificationResultCallback = callback
}
func (f *Frontend) saveIconToDir() error {
iconOnce.Do(func() {
hIcon := w32.ExtractIcon(exePath, 0)
if hIcon == 0 {
iconErr = fmt.Errorf("ExtractIcon failed for %s", exePath)
return
}
defer w32.DestroyIcon(hIcon)
iconErr = winc.SaveHIconAsPNG(hIcon, iconPath)
})
return iconErr
}
func saveCategoriesToRegistry() error {
// We assume lock is held by caller
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName)
key, _, err := registry.CreateKey(
registry.CURRENT_USER,
registryPath,
registry.ALL_ACCESS,
)
if err != nil {
return err
}
defer key.Close()
data, err := json.Marshal(categories)
if err != nil {
return err
}
return key.SetStringValue(NotificationCategoriesRegistryKey, string(data))
}
func loadCategoriesFromRegistry() error {
// We assume lock is held by caller
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, 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]frontend.NotificationCategory)
if err := json.Unmarshal([]byte(data), &_categories); err != nil {
return fmt.Errorf("failed to parse notification categories from registry: %w", err)
}
categories = _categories
return nil
}
func getUserText(data []toast.UserData) (string, bool) {
for _, d := range data {
if d.Key == "userText" {
return d.Value, true
}
}
return "", false
}
// encodePayload combines an action ID and user data into a single encoded string
func encodePayload(actionID string, options frontend.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, frontend.NotificationOptions, error) {
jsonData, err := base64.StdEncoding.DecodeString(encodedString)
if err != nil {
return encodedString, frontend.NotificationOptions{}, fmt.Errorf("failed to decode base64 payload: %w", err)
}
var payload NotificationPayload
if err := json.Unmarshal(jsonData, &payload); err != nil {
return encodedString, frontend.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 frontend.NotificationOptions, err error) {
actionID, options, err := decodePayload(response)
if err != nil {
log.Printf("Warning: Failed to decode notification response: %v", err)
return response, frontend.NotificationOptions{}, err
}
return actionID, options, nil
}
func handleNotificationResult(result frontend.NotificationResult) {
callbackLock.RLock()
callback := notificationResultCallback
callbackLock.RUnlock()
if callback != nil {
go func() {
defer func() {
if r := recover(); r != nil {
// Log panic but don't crash the app
fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r)
}
}()
callback(result)
}()
}
}
// Helper functions
func getGUID() (string, error) {
keyPath := ToastRegistryPath + 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 := 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 generateGUID() string {
guid := uuid.New()
return fmt.Sprintf("{%s}", guid.String())
}

View file

@ -10,11 +10,86 @@ package winc
import ( import (
"errors" "errors"
"fmt" "fmt"
"image"
"image/png"
"os"
"syscall" "syscall"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
) )
var (
user32 = syscall.NewLazyDLL("user32.dll")
gdi32 = syscall.NewLazyDLL("gdi32.dll")
procGetIconInfo = user32.NewProc("GetIconInfo")
procDeleteObject = gdi32.NewProc("DeleteObject")
procGetObject = gdi32.NewProc("GetObjectW")
procGetDIBits = gdi32.NewProc("GetDIBits")
procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
procSelectObject = gdi32.NewProc("SelectObject")
procDeleteDC = gdi32.NewProc("DeleteDC")
)
func init() {
// Validate DLL loads at initialization time to surface missing APIs early
if err := user32.Load(); err != nil {
panic(fmt.Sprintf("failed to load user32.dll: %v", err))
}
if err := gdi32.Load(); err != nil {
panic(fmt.Sprintf("failed to load gdi32.dll: %v", err))
}
}
// ICONINFO mirrors the Win32 ICONINFO struct
type ICONINFO struct {
FIcon int32
XHotspot uint32
YHotspot uint32
HbmMask uintptr
HbmColor uintptr
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183376.aspx
type BITMAPINFOHEADER struct {
BiSize uint32
BiWidth int32
BiHeight int32
BiPlanes uint16
BiBitCount uint16
BiCompression uint32
BiSizeImage uint32
BiXPelsPerMeter int32
BiYPelsPerMeter int32
BiClrUsed uint32
BiClrImportant uint32
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162938.aspx
type RGBQUAD struct {
RgbBlue byte
RgbGreen byte
RgbRed byte
RgbReserved byte
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183375.aspx
type BITMAPINFO struct {
BmiHeader BITMAPINFOHEADER
BmiColors *RGBQUAD
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183371.aspx
type BITMAP struct {
BmType int32
BmWidth int32
BmHeight int32
BmWidthBytes int32
BmPlanes uint16
BmBitsPixel uint16
BmBits unsafe.Pointer
}
type Icon struct { type Icon struct {
handle w32.HICON handle w32.HICON
} }
@ -46,6 +121,95 @@ func ExtractIcon(fileName string, index int) (*Icon, error) {
return ico, err return ico, err
} }
func SaveHIconAsPNG(hIcon w32.HICON, filePath string) error {
// Get icon info
var iconInfo ICONINFO
ret, _, err := procGetIconInfo.Call(
uintptr(hIcon),
uintptr(unsafe.Pointer(&iconInfo)),
)
if ret == 0 {
return err
}
defer procDeleteObject.Call(uintptr(iconInfo.HbmMask))
defer procDeleteObject.Call(uintptr(iconInfo.HbmColor))
// Get bitmap info
var bmp BITMAP
ret, _, err = procGetObject.Call(
uintptr(iconInfo.HbmColor),
unsafe.Sizeof(bmp),
uintptr(unsafe.Pointer(&bmp)),
)
if ret == 0 {
return err
}
// Get screen DC for GetDIBits (bitmap must not be selected into a DC)
screenDC := w32.GetDC(0)
if screenDC == 0 {
return fmt.Errorf("failed to get screen DC")
}
defer w32.ReleaseDC(0, screenDC)
// 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 = w32.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 using screen DC (bitmap must not be selected into any DC)
ret, _, err = procGetDIBits.Call(
uintptr(screenDC),
uintptr(iconInfo.HbmColor),
0,
uintptr(bmp.BmHeight),
uintptr(unsafe.Pointer(&bits[0])),
uintptr(unsafe.Pointer(&bi)),
w32.DIB_RGB_COLORS,
)
if ret == 0 {
return fmt.Errorf("failed to get bitmap bits: %w", 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
// RGBA image is top-down
imgIndex := (y*width + x) * 4
// BGRA to RGBA
img.Pix[imgIndex] = bits[dibIndex+2] // R
img.Pix[imgIndex+1] = bits[dibIndex+1] // G
img.Pix[imgIndex+2] = bits[dibIndex] // B
img.Pix[imgIndex+3] = bits[dibIndex+3] // 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 (ic *Icon) Destroy() bool { func (ic *Icon) Destroy() bool {
return w32.DestroyIcon(ic.handle) return w32.DestroyIcon(ic.handle)
} }

View file

@ -61,6 +61,102 @@ func (d *Dispatcher) processSystemCall(payload callMessage, sender frontend.Fron
return false, err return false, err
} }
return true, nil return true, nil
case "InitializeNotifications":
err := sender.InitializeNotifications()
return nil, err
case "CleanupNotifications":
sender.CleanupNotifications()
return nil, nil
case "IsNotificationAvailable":
return sender.IsNotificationAvailable(), nil
case "RequestNotificationAuthorization":
authorized, err := sender.RequestNotificationAuthorization()
if err != nil {
return nil, err
}
return authorized, nil
case "CheckNotificationAuthorization":
authorized, err := sender.CheckNotificationAuthorization()
if err != nil {
return nil, err
}
return authorized, nil
case "SendNotification":
if len(payload.Args) < 1 {
return nil, errors.New("empty argument, cannot send notification")
}
var options frontend.NotificationOptions
if err := json.Unmarshal(payload.Args[0], &options); err != nil {
return nil, err
}
err := sender.SendNotification(options)
return nil, err
case "SendNotificationWithActions":
if len(payload.Args) < 1 {
return nil, errors.New("empty argument, cannot send notification")
}
var options frontend.NotificationOptions
if err := json.Unmarshal(payload.Args[0], &options); err != nil {
return nil, err
}
err := sender.SendNotificationWithActions(options)
return nil, err
case "RegisterNotificationCategory":
if len(payload.Args) < 1 {
return nil, errors.New("empty argument, cannot register category")
}
var category frontend.NotificationCategory
if err := json.Unmarshal(payload.Args[0], &category); err != nil {
return nil, err
}
err := sender.RegisterNotificationCategory(category)
return nil, err
case "RemoveNotificationCategory":
if len(payload.Args) < 1 {
return nil, errors.New("empty argument, cannot remove category")
}
var categoryId string
if err := json.Unmarshal(payload.Args[0], &categoryId); err != nil {
return nil, err
}
err := sender.RemoveNotificationCategory(categoryId)
return nil, err
case "RemoveAllPendingNotifications":
err := sender.RemoveAllPendingNotifications()
return nil, err
case "RemovePendingNotification":
if len(payload.Args) < 1 {
return nil, errors.New("empty argument, cannot remove notification")
}
var identifier string
if err := json.Unmarshal(payload.Args[0], &identifier); err != nil {
return nil, err
}
err := sender.RemovePendingNotification(identifier)
return nil, err
case "RemoveAllDeliveredNotifications":
err := sender.RemoveAllDeliveredNotifications()
return nil, err
case "RemoveDeliveredNotification":
if len(payload.Args) < 1 {
return nil, errors.New("empty argument, cannot remove notification")
}
var identifier string
if err := json.Unmarshal(payload.Args[0], &identifier); err != nil {
return nil, err
}
err := sender.RemoveDeliveredNotification(identifier)
return nil, err
case "RemoveNotification":
if len(payload.Args) < 1 {
return nil, errors.New("empty argument, cannot remove notification")
}
var identifier string
if err := json.Unmarshal(payload.Args[0], &identifier); err != nil {
return nil, err
}
err := sender.RemoveNotification(identifier)
return nil, err
default: default:
return nil, fmt.Errorf("unknown systemcall message: %s", payload.Name) return nil, fmt.Errorf("unknown systemcall message: %s", payload.Name)
} }

View file

@ -76,6 +76,51 @@ type MessageDialogOptions struct {
Icon []byte Icon []byte
} }
// 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"`
}
// 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"`
}
// 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:"categoryId,omitempty"` // Consistent with NotificationOptions
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
}
type Frontend interface { type Frontend interface {
Run(ctx context.Context) error Run(ctx context.Context) error
RunMainLoop() RunMainLoop()
@ -139,4 +184,21 @@ type Frontend interface {
// Clipboard // Clipboard
ClipboardGetText() (string, error) ClipboardGetText() (string, error)
ClipboardSetText(text string) error ClipboardSetText(text string) error
// Notifications
InitializeNotifications() error
CleanupNotifications()
IsNotificationAvailable() bool
RequestNotificationAuthorization() (bool, error)
CheckNotificationAuthorization() (bool, error)
OnNotificationResponse(callback func(result NotificationResult))
SendNotification(options NotificationOptions) error
SendNotificationWithActions(options NotificationOptions) error
RegisterNotificationCategory(category NotificationCategory) error
RemoveNotificationCategory(categoryId string) error
RemoveAllPendingNotifications() error
RemovePendingNotification(identifier string) error
RemoveAllDeliveredNotifications() error
RemoveDeliveredNotification(identifier string) error
RemoveNotification(identifier string) error
} }

View file

@ -27,6 +27,7 @@ import * as Browser from "./browser";
import * as Clipboard from "./clipboard"; import * as Clipboard from "./clipboard";
import * as DragAndDrop from "./draganddrop"; import * as DragAndDrop from "./draganddrop";
import * as ContextMenu from "./contextmenu"; import * as ContextMenu from "./contextmenu";
import * as Notifications from "./notifications";
export function Quit() { export function Quit() {
window.WailsInvoke('Q'); window.WailsInvoke('Q');
@ -52,6 +53,7 @@ window.runtime = {
...Screen, ...Screen,
...Clipboard, ...Clipboard,
...DragAndDrop, ...DragAndDrop,
...Notifications,
EventsOn, EventsOn,
EventsOnce, EventsOnce,
EventsOnMultiple, EventsOnMultiple,

View file

@ -0,0 +1,200 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 9 */
import {Call} from "./calls";
/**
* Initialize the notification service for the application.
* This must be called before sending any notifications.
* On macOS, this also ensures the notification delegate is properly initialized.
*
* @export
* @return {Promise<void>}
*/
export function InitializeNotifications() {
return Call(":wails:InitializeNotifications");
}
/**
* Clean up notification resources and release any held connections.
* This should be called when shutting down the application to properly release resources
* (primarily needed on Linux to close D-Bus connections).
*
* @export
* @return {Promise<void>}
*/
export function CleanupNotifications() {
return Call(":wails:CleanupNotifications");
}
/**
* Check if notifications are available on the current platform.
*
* @export
* @return {Promise<boolean>} True if notifications are available, false otherwise
*/
export function IsNotificationAvailable() {
return Call(":wails:IsNotificationAvailable");
}
/**
* Request notification authorization from the user.
* On macOS, this prompts the user to allow notifications.
* On other platforms, this always returns true.
*
* @export
* @return {Promise<boolean>} True if authorization was granted, false otherwise
*/
export function RequestNotificationAuthorization() {
return Call(":wails:RequestNotificationAuthorization");
}
/**
* Check the current notification authorization status.
* On macOS, this checks if the app has notification permissions.
* On other platforms, this always returns true.
*
* @export
* @return {Promise<boolean>} True if authorized, false otherwise
*/
export function CheckNotificationAuthorization() {
return Call(":wails:CheckNotificationAuthorization");
}
/**
* Send a basic notification with the given options.
* The notification will display with the provided title, subtitle (if supported), and body text.
*
* @export
* @param {Object} options - Notification options
* @param {string} options.id - Unique identifier for the notification
* @param {string} options.title - Notification title
* @param {string} [options.subtitle] - Notification subtitle (macOS and Linux only)
* @param {string} [options.body] - Notification body text
* @param {string} [options.categoryId] - Category ID for action buttons (requires SendNotificationWithActions)
* @param {Object<string, any>} [options.data] - Additional user data to attach to the notification
* @return {Promise<void>}
*/
export function SendNotification(options) {
return Call(":wails:SendNotification", [options]);
}
/**
* Send a notification with action buttons.
* A NotificationCategory must be registered first using RegisterNotificationCategory.
* The options.categoryId must match a previously registered category ID.
* If the category is not found, a basic notification will be sent instead.
*
* @export
* @param {Object} options - Notification options
* @param {string} options.id - Unique identifier for the notification
* @param {string} options.title - Notification title
* @param {string} [options.subtitle] - Notification subtitle (macOS and Linux only)
* @param {string} [options.body] - Notification body text
* @param {string} options.categoryId - Category ID that matches a registered category
* @param {Object<string, any>} [options.data] - Additional user data to attach to the notification
* @return {Promise<void>}
*/
export function SendNotificationWithActions(options) {
return Call(":wails:SendNotificationWithActions", [options]);
}
/**
* Register a notification category that can be used with SendNotificationWithActions.
* Categories define the action buttons and optional reply fields that will appear on notifications.
* Registering a category with the same ID as a previously registered category will override it.
*
* @export
* @param {Object} category - Notification category definition
* @param {string} category.id - Unique identifier for the category
* @param {Array<Object>} [category.actions] - Array of action buttons
* @param {string} category.actions[].id - Unique identifier for the action
* @param {string} category.actions[].title - Display title for the action button
* @param {boolean} [category.actions[].destructive] - Whether the action is destructive (macOS-specific)
* @param {boolean} [category.hasReplyField] - Whether to include a text input field for replies
* @param {string} [category.replyPlaceholder] - Placeholder text for the reply field (required if hasReplyField is true)
* @param {string} [category.replyButtonTitle] - Title for the reply button (required if hasReplyField is true)
* @return {Promise<void>}
*/
export function RegisterNotificationCategory(category) {
return Call(":wails:RegisterNotificationCategory", [category]);
}
/**
* Remove a previously registered notification category.
*
* @export
* @param {string} categoryId - The ID of the category to remove
* @return {Promise<void>}
*/
export function RemoveNotificationCategory(categoryId) {
return Call(":wails:RemoveNotificationCategory", [categoryId]);
}
/**
* Remove all pending notifications from the notification center.
* On Windows, this is a no-op as the platform manages notification lifecycle automatically.
*
* @export
* @return {Promise<void>}
*/
export function RemoveAllPendingNotifications() {
return Call(":wails:RemoveAllPendingNotifications");
}
/**
* Remove a specific pending notification by its identifier.
* On Windows, this is a no-op as the platform manages notification lifecycle automatically.
*
* @export
* @param {string} identifier - The ID of the notification to remove
* @return {Promise<void>}
*/
export function RemovePendingNotification(identifier) {
return Call(":wails:RemovePendingNotification", [identifier]);
}
/**
* Remove all delivered notifications from the notification center.
* On Windows, this is a no-op as the platform manages notification lifecycle automatically.
*
* @export
* @return {Promise<void>}
*/
export function RemoveAllDeliveredNotifications() {
return Call(":wails:RemoveAllDeliveredNotifications");
}
/**
* Remove a specific delivered notification by its identifier.
* On Windows, this is a no-op as the platform manages notification lifecycle automatically.
*
* @export
* @param {string} identifier - The ID of the notification to remove
* @return {Promise<void>}
*/
export function RemoveDeliveredNotification(identifier) {
return Call(":wails:RemoveDeliveredNotification", [identifier]);
}
/**
* Remove a notification by its identifier.
* This is a convenience function that works across platforms.
* On macOS, use the more specific RemovePendingNotification or RemoveDeliveredNotification functions.
*
* @export
* @param {string} identifier - The ID of the notification to remove
* @return {Promise<void>}
*/
export function RemoveNotification(identifier) {
return Call(":wails:RemoveNotification", [identifier]);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -247,3 +247,84 @@ export function CanResolveFilePaths(): boolean;
// Resolves file paths for an array of files // Resolves file paths for an array of files
export function ResolveFilePaths(files: File[]): void export function ResolveFilePaths(files: File[]): void
// Notification types
export interface NotificationOptions {
id: string;
title: string;
subtitle?: string; // macOS and Linux only
body?: string;
categoryId?: string;
data?: { [key: string]: any };
}
export interface NotificationAction {
id?: string;
title?: string;
destructive?: boolean; // macOS-specific
}
export interface NotificationCategory {
id?: string;
actions?: NotificationAction[];
hasReplyField?: boolean;
replyPlaceholder?: string;
replyButtonTitle?: string;
}
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
// Initializes the notification service for the application.
// This must be called before sending any notifications.
export function InitializeNotifications(): Promise<void>;
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
// Cleans up notification resources and releases any held connections.
export function CleanupNotifications(): Promise<void>;
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
// Checks if notifications are available on the current platform.
export function IsNotificationAvailable(): Promise<boolean>;
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
// Requests notification authorization from the user (macOS only).
export function RequestNotificationAuthorization(): Promise<boolean>;
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
// Checks the current notification authorization status (macOS only).
export function CheckNotificationAuthorization(): Promise<boolean>;
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
// Sends a basic notification with the given options.
export function SendNotification(options: NotificationOptions): Promise<void>;
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
// Sends a notification with action buttons. Requires a registered category.
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
// Registers a notification category that can be used with SendNotificationWithActions.
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
// Removes a previously registered notification category.
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
// Removes all pending notifications from the notification center.
export function RemoveAllPendingNotifications(): Promise<void>;
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
// Removes a specific pending notification by its identifier.
export function RemovePendingNotification(identifier: string): Promise<void>;
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
// Removes all delivered notifications from the notification center.
export function RemoveAllDeliveredNotifications(): Promise<void>;
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
// Removes a specific delivered notification by its identifier.
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
// Removes a notification by its identifier (cross-platform convenience function).
export function RemoveNotification(identifier: string): Promise<void>;

View file

@ -240,3 +240,59 @@ export function CanResolveFilePaths() {
export function ResolveFilePaths(files) { export function ResolveFilePaths(files) {
return window.runtime.ResolveFilePaths(files); return window.runtime.ResolveFilePaths(files);
} }
export function InitializeNotifications() {
return window.runtime.InitializeNotifications();
}
export function CleanupNotifications() {
return window.runtime.CleanupNotifications();
}
export function IsNotificationAvailable() {
return window.runtime.IsNotificationAvailable();
}
export function RequestNotificationAuthorization() {
return window.runtime.RequestNotificationAuthorization();
}
export function CheckNotificationAuthorization() {
return window.runtime.CheckNotificationAuthorization();
}
export function SendNotification(options) {
return window.runtime.SendNotification(options);
}
export function SendNotificationWithActions(options) {
return window.runtime.SendNotificationWithActions(options);
}
export function RegisterNotificationCategory(category) {
return window.runtime.RegisterNotificationCategory(category);
}
export function RemoveNotificationCategory(categoryId) {
return window.runtime.RemoveNotificationCategory(categoryId);
}
export function RemoveAllPendingNotifications() {
return window.runtime.RemoveAllPendingNotifications();
}
export function RemovePendingNotification(identifier) {
return window.runtime.RemovePendingNotification(identifier);
}
export function RemoveAllDeliveredNotifications() {
return window.runtime.RemoveAllDeliveredNotifications();
}
export function RemoveDeliveredNotification(identifier) {
return window.runtime.RemoveDeliveredNotification(identifier);
}
export function RemoveNotification(identifier) {
return window.runtime.RemoveNotification(identifier);
}

View file

@ -5,7 +5,6 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -29,7 +28,7 @@ func checkError(err error) {
func mute() { func mute() {
originalOutput = Output originalOutput = Output
Output = ioutil.Discard Output = io.Discard
} }
func unmute() { func unmute() {

View file

@ -3,6 +3,7 @@ package build
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
@ -357,6 +358,16 @@ func execBuildApplication(builder Builder, options *Options) (string, error) {
pterm.Println("Done.") pterm.Println("Done.")
} }
if runtime.GOOS == "darwin" && options.Platform == "darwin" {
// On macOS, self-sign the .app bundle so notifications work
printBulletPoint("Self-signing application: ")
cmd := exec.Command("/usr/bin/codesign", "--force", "--deep", "--sign", "-", options.CompiledBinary)
if out, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("codesign failed: %v %s", err, out)
}
pterm.Println("Done.")
}
if options.Platform == "windows" { if options.Platform == "windows" {
const nativeWebView2Loader = "native_webview2loader" const nativeWebView2Loader = "native_webview2loader"

View file

@ -0,0 +1,136 @@
package runtime
import (
"context"
"github.com/wailsapp/wails/v2/internal/frontend"
)
// NotificationOptions contains configuration for a notification.
type NotificationOptions = frontend.NotificationOptions
// NotificationAction represents an action button for a notification.
type NotificationAction = frontend.NotificationAction
// NotificationCategory groups actions for notifications.
type NotificationCategory = frontend.NotificationCategory
// NotificationResponse represents the response sent by interacting with a notification.
type NotificationResponse = frontend.NotificationResponse
// NotificationResult represents the result of a notification response,
// returning the response or any errors that occurred.
type NotificationResult = frontend.NotificationResult
// InitializeNotifications initializes the notification service for the application.
// This must be called before sending any notifications. On macOS, this also ensures
// the notification delegate is properly initialized.
func InitializeNotifications(ctx context.Context) error {
fe := getFrontend(ctx)
return fe.InitializeNotifications()
}
// CleanupNotifications cleans up notification resources and releases any held connections.
// This should be called when shutting down the application to properly release resources
// (primarily needed on Linux to close D-Bus connections).
func CleanupNotifications(ctx context.Context) {
fe := getFrontend(ctx)
fe.CleanupNotifications()
}
// IsNotificationAvailable checks if notifications are available on the current platform.
func IsNotificationAvailable(ctx context.Context) bool {
fe := getFrontend(ctx)
return fe.IsNotificationAvailable()
}
// RequestNotificationAuthorization requests notification authorization from the user.
// On macOS, this prompts the user to allow notifications. On other platforms, this
// always returns true. Returns true if authorization was granted, false otherwise.
func RequestNotificationAuthorization(ctx context.Context) (bool, error) {
fe := getFrontend(ctx)
return fe.RequestNotificationAuthorization()
}
// CheckNotificationAuthorization checks the current notification authorization status.
// On macOS, this checks if the app has notification permissions. On other platforms,
// this always returns true.
func CheckNotificationAuthorization(ctx context.Context) (bool, error) {
fe := getFrontend(ctx)
return fe.CheckNotificationAuthorization()
}
// SendNotification sends a basic notification with the given options.
// The notification will display with the provided title, subtitle (if supported),
// and body text.
func SendNotification(ctx context.Context, options NotificationOptions) error {
fe := getFrontend(ctx)
return fe.SendNotification(options)
}
// SendNotificationWithActions sends a notification with action buttons.
// A NotificationCategory must be registered first using RegisterNotificationCategory.
// The options.CategoryID must match a previously registered category ID.
// If the category is not found, a basic notification will be sent instead.
func SendNotificationWithActions(ctx context.Context, options NotificationOptions) error {
fe := getFrontend(ctx)
return fe.SendNotificationWithActions(options)
}
// RegisterNotificationCategory registers a notification category that can be used
// with SendNotificationWithActions. Categories define the action buttons and optional
// reply fields that will appear on notifications.
func RegisterNotificationCategory(ctx context.Context, category NotificationCategory) error {
fe := getFrontend(ctx)
return fe.RegisterNotificationCategory(category)
}
// RemoveNotificationCategory removes a previously registered notification category.
func RemoveNotificationCategory(ctx context.Context, categoryId string) error {
fe := getFrontend(ctx)
return fe.RemoveNotificationCategory(categoryId)
}
// RemoveAllPendingNotifications removes all pending notifications from the notification center.
// On Windows, this is a no-op as the platform manages notification lifecycle automatically.
func RemoveAllPendingNotifications(ctx context.Context) error {
fe := getFrontend(ctx)
return fe.RemoveAllPendingNotifications()
}
// RemovePendingNotification removes a specific pending notification by its identifier.
// On Windows, this is a no-op as the platform manages notification lifecycle automatically.
func RemovePendingNotification(ctx context.Context, identifier string) error {
fe := getFrontend(ctx)
return fe.RemovePendingNotification(identifier)
}
// RemoveAllDeliveredNotifications removes all delivered notifications from the notification center.
// On Windows, this is a no-op as the platform manages notification lifecycle automatically.
func RemoveAllDeliveredNotifications(ctx context.Context) error {
fe := getFrontend(ctx)
return fe.RemoveAllDeliveredNotifications()
}
// RemoveDeliveredNotification removes a specific delivered notification by its identifier.
// On Windows, this is a no-op as the platform manages notification lifecycle automatically.
func RemoveDeliveredNotification(ctx context.Context, identifier string) error {
fe := getFrontend(ctx)
return fe.RemoveDeliveredNotification(identifier)
}
// RemoveNotification removes a notification by its identifier.
// This is a convenience function that works across platforms. On macOS, use the
// more specific RemovePendingNotification or RemoveDeliveredNotification functions.
func RemoveNotification(ctx context.Context, identifier string) error {
fe := getFrontend(ctx)
return fe.RemoveNotification(identifier)
}
// OnNotificationResponse registers a callback function that will be invoked when
// a user interacts with a notification (e.g., clicks an action button or the notification itself).
// The callback receives a NotificationResult containing the response details or any errors.
func OnNotificationResponse(ctx context.Context, callback func(result NotificationResult)) {
fe := getFrontend(ctx)
fe.OnNotificationResponse(callback)
}

View file

@ -0,0 +1,65 @@
//go:build linux
package runtime
/*
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
static void fix_signal(int signum)
{
struct sigaction st;
if (sigaction(signum, NULL, &st) < 0) {
return;
}
st.sa_flags |= SA_ONSTACK;
sigaction(signum, &st, NULL);
}
static void fix_all_signals()
{
#if defined(SIGSEGV)
fix_signal(SIGSEGV);
#endif
#if defined(SIGBUS)
fix_signal(SIGBUS);
#endif
#if defined(SIGFPE)
fix_signal(SIGFPE);
#endif
#if defined(SIGABRT)
fix_signal(SIGABRT);
#endif
}
*/
import "C"
// ResetSignalHandlers resets signal handlers to allow panic recovery.
//
// On Linux, WebKit (used for the webview) may install signal handlers without
// the SA_ONSTACK flag, which prevents Go from properly recovering from panics
// caused by nil pointer dereferences or other memory access violations.
//
// Call this function immediately before code that might panic to ensure
// the signal handlers are properly configured for Go's panic recovery mechanism.
//
// Example usage:
//
// go func() {
// defer func() {
// if err := recover(); err != nil {
// log.Printf("Recovered from panic: %v", err)
// }
// }()
// runtime.ResetSignalHandlers()
// // Code that might panic...
// }()
//
// Note: This function only has an effect on Linux. On other platforms,
// it is a no-op.
func ResetSignalHandlers() {
C.fix_all_signals()
}

View file

@ -0,0 +1,18 @@
//go:build !linux
package runtime
// ResetSignalHandlers resets signal handlers to allow panic recovery.
//
// On Linux, WebKit (used for the webview) may install signal handlers without
// the SA_ONSTACK flag, which prevents Go from properly recovering from panics
// caused by nil pointer dereferences or other memory access violations.
//
// Call this function immediately before code that might panic to ensure
// the signal handlers are properly configured for Go's panic recovery mechanism.
//
// Note: This function only has an effect on Linux. On other platforms,
// it is a no-op.
func ResetSignalHandlers() {
// No-op on non-Linux platforms
}

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.18 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.18 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -186,7 +186,16 @@ func Install(options *Options) (bool, *Template, error) {
return false, nil, err return false, nil, err
} }
options.TargetDir = targetDir options.TargetDir = targetDir
if !fs.DirExists(options.TargetDir) { if fs.DirExists(options.TargetDir) {
// Check if directory is non-empty
entries, err := os.ReadDir(options.TargetDir)
if err != nil {
return false, nil, err
}
if len(entries) > 0 {
return false, nil, fmt.Errorf("cannot initialise project in non-empty directory: %s", options.TargetDir)
}
} else {
err := fs.Mkdir(options.TargetDir) err := fs.Mkdir(options.TargetDir)
if err != nil { if err != nil {
return false, nil, err return false, nil, err

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void):
// unregisters the listener for the given event name. // unregisters the listener for the given event name.
export function EventsOff(eventName: string): void; export function EventsOff(eventName: string): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all event listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message // logs the given message as a raw message
export function LogPrint(message: string): void; export function LogPrint(message: string): void;

View file

@ -48,6 +48,10 @@ export function EventsOff(eventName) {
return window.runtime.EventsOff(eventName); return window.runtime.EventsOff(eventName);
} }
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); EventsOnMultiple(eventName, callback, 1);
} }

View file

@ -1,6 +1,6 @@
module changeme module changeme
go 1.23 go 1.23.0
require github.com/wailsapp/wails/v2 {{.WailsVersion}} require github.com/wailsapp/wails/v2 {{.WailsVersion}}

View file

@ -52,3 +52,48 @@ func TestInstall(t *testing.T) {
is2.NoErr(err) is2.NoErr(err)
} }
func TestInstallFailsInNonEmptyDirectory(t *testing.T) {
is2 := is.New(t)
// Create a temp directory with a file in it
tempDir, err := os.MkdirTemp("", "wails-test-nonempty-*")
is2.NoErr(err)
defer func() {
_ = os.RemoveAll(tempDir)
}()
// Create a file in the directory to make it non-empty
err = os.WriteFile(filepath.Join(tempDir, "existing-file.txt"), []byte("test"), 0644)
is2.NoErr(err)
options := &Options{
ProjectName: "test",
TemplateName: "vanilla",
TargetDir: tempDir,
}
_, _, err = Install(options)
is2.True(err != nil) // Should fail
is2.True(err.Error() == "cannot initialise project in non-empty directory: "+tempDir)
}
func TestInstallSucceedsInEmptyDirectory(t *testing.T) {
is2 := is.New(t)
// Create an empty temp directory
tempDir, err := os.MkdirTemp("", "wails-test-empty-*")
is2.NoErr(err)
defer func() {
_ = os.RemoveAll(tempDir)
}()
options := &Options{
ProjectName: "test",
TemplateName: "vanilla",
TargetDir: tempDir,
}
_, _, err = Install(options)
is2.NoErr(err) // Should succeed in empty directory
}

View file

@ -70,3 +70,57 @@ If the added package does not resolve the issue, additional GStreamer dependenci
- This issue impacts [Tauri apps](https://tauri.app/). - This issue impacts [Tauri apps](https://tauri.app/).
Source: [developomp](https://github.com/developomp) on the [Tauri discussion board](https://github.com/tauri-apps/tauri/issues/4642#issuecomment-1643229562). Source: [developomp](https://github.com/developomp) on the [Tauri discussion board](https://github.com/tauri-apps/tauri/issues/4642#issuecomment-1643229562).
## Panic Recovery / Signal Handling Issues
### App crashes with "non-Go code set up signal handler without SA_ONSTACK flag"
On Linux, if your application crashes with an error like:
```
signal 11 received but handler not on signal stack
fatal error: non-Go code set up signal handler without SA_ONSTACK flag
```
This occurs because WebKit (used for the webview) installs signal handlers that interfere with Go's panic recovery mechanism.
Normally, Go can convert signals like SIGSEGV (from nil pointer dereferences) into recoverable panics, but WebKit's signal
handlers prevent this.
### Solution
Use the `runtime.ResetSignalHandlers()` function immediately before code that might panic:
```go
import "github.com/wailsapp/wails/v2/pkg/runtime"
go func() {
defer func() {
if err := recover(); err != nil {
log.Printf("Recovered from panic: %v", err)
}
}()
// Reset signal handlers right before potentially dangerous code
runtime.ResetSignalHandlers()
// Your code that might panic...
}()
```
:::warning Important
- Call `ResetSignalHandlers()` in each goroutine where you need panic recovery
- Call it immediately before the code that might panic, as WebKit may reset the handlers at any time
- This is only necessary on Linux - the function is a no-op on other platforms
:::
### Why This Happens
WebKit installs its own signal handlers for garbage collection and other internal processes. These handlers don't include
the `SA_ONSTACK` flag that Go requires to properly handle signals on the correct stack. When a signal like SIGSEGV occurs,
Go's runtime can't recover because the signal is being handled on the wrong stack.
The `ResetSignalHandlers()` function adds the `SA_ONSTACK` flag to the signal handlers for SIGSEGV, SIGBUS, SIGFPE, and
SIGABRT, allowing Go's panic recovery to work correctly.
Source: [GitHub Issue #3965](https://github.com/wailsapp/wails/issues/3965)

View file

@ -0,0 +1,233 @@
# Notifications
Wails provides a comprehensive cross-platform notification system for desktop applications. This runtime allows you to display native system notifications with support for interactive elements like action buttons and text input fields.
:::info JavaScript
Notifications are currently unsupported in the JS runtime.
:::
## Basic Usage
### Initializing Notifications
First, initialize the notification system. This should be called during app startup (typically in `OnStartup`):
```go
err := runtime.InitializeNotifications(a.ctx)
if err != nil {
// Handle initialization error
// On macOS, this may fail if bundle identifier is not set
}
```
Then, check if notifications are available on the current platform:
```go
if runtime.IsNotificationAvailable(a.ctx) {
// Notifications are supported
// On macOS, this checks for macOS 10.14+
// On Windows and Linux, this always returns true
}
```
On macOS, you'll need to request permission before sending notifications:
```go
authorized, err := runtime.CheckNotificationAuthorization(a.ctx)
if err != nil {
// Handle authorization error
}
if !authorized {
authorized, err = runtime.RequestNotificationAuthorization(a.ctx)
if err != nil || !authorized {
// Handle permission denial
}
}
```
On Windows and Linux, authorization is not required as these platforms don't have permission systems.
### Sending Basic Notifications
Send a basic notification with a unique ID, title, optional subtitle (macOS and Linux), and body text:
```go
err := runtime.SendNotification(a.ctx, runtime.NotificationOptions{
ID: "calendar-invite-001",
Title: "New Calendar Invite",
Subtitle: "From: Jane Doe", // Optional - macOS and Linux only
Body: "Tap to view the event",
})
if err != nil {
// Handle error
}
```
## Interactive Notifications
Interactive notifications allow users to respond with button actions or text input. You must first register a notification category that defines the available actions.
### Creating Notification Categories
Define a category with action buttons and optional text input:
```go
categoryID := "message-category"
category := runtime.NotificationCategory{
ID: categoryID,
Actions: []runtime.NotificationAction{
{
ID: "OPEN",
Title: "Open",
},
{
ID: "ARCHIVE",
Title: "Archive",
Destructive: true, // macOS-specific - shows as red button
},
},
HasReplyField: true,
ReplyPlaceholder: "Type your reply...",
ReplyButtonTitle: "Reply",
}
err := runtime.RegisterNotificationCategory(a.ctx, category)
if err != nil {
// Handle error
}
```
### Sending Interactive Notifications
Send an interactive notification using the registered category. If the category is not found or `CategoryID` is empty, a basic notification will be sent instead:
```go
err := runtime.SendNotificationWithActions(a.ctx, runtime.NotificationOptions{
ID: "message-001",
Title: "New Message",
Subtitle: "From: John Smith", // Optional - macOS and Linux only
Body: "Hey, are you free for lunch?",
CategoryID: categoryID,
})
if err != nil {
// Handle error
}
```
## Handling Notification Responses
Listen for user interactions with notifications by registering a callback:
```go
runtime.OnNotificationResponse(a.ctx, func(result runtime.NotificationResult) {
if result.Error != nil {
// Handle response error
return
}
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)
}
// You can also emit events to the frontend
runtime.EventsEmit(a.ctx, "notification", response)
})
```
## Adding Custom Data
Basic and interactive notifications can include custom data that will be returned in the response:
```go
err := runtime.SendNotification(a.ctx, runtime.NotificationOptions{
ID: "event-001",
Title: "Team Meeting",
Subtitle: "In 30 minutes",
Body: "Don't forget your presentation materials!",
Data: map[string]interface{}{
"eventId": "meeting-123",
"startTime": "2024-01-15T14:00:00Z",
"attendees": []string{"john@company.com", "jane@company.com"},
"priority": "high",
},
})
// In the response handler:
runtime.OnNotificationResponse(a.ctx, func(result runtime.NotificationResult) {
response := result.Response
if eventId, ok := response.UserInfo["eventId"].(string); ok {
fmt.Printf("Event ID: %s\n", eventId)
}
})
```
## Managing Notifications
### Removing Notification Categories
Remove a previously registered notification category:
```go
err := runtime.RemoveNotificationCategory(a.ctx, "message-category")
```
### Managing Notifications Lifecycle
Control notification visibility:
```go
// Remove a specific pending notification (macOS and Linux only)
err := runtime.RemovePendingNotification(a.ctx, "notification-id")
// Remove all pending notifications (macOS and Linux only)
err = runtime.RemoveAllPendingNotifications(a.ctx)
// Remove a specific delivered notification (macOS and Linux only)
err = runtime.RemoveDeliveredNotification(a.ctx, "notification-id")
// Remove all delivered notifications (macOS and Linux only)
err = runtime.RemoveAllDeliveredNotifications(a.ctx)
// Remove a notification (Linux-specific)
err = runtime.RemoveNotification(a.ctx, "notification-id")
```
## Platform Considerations
### macOS
- **Authorization Required**: Apps must request notification permission
- **Notarization**: Required for app distribution on macOS
- **Features**: Supports subtitles, user text input, destructive actions, dark/light mode
- **Behavior**: Notifications appear in the system notification center
### Windows
- **No Authorization**: No permission system required
- **Features**: Supports user text input, high DPI displays, Windows theme adaptation
- **Limitations**: Does not support subtitles
- **Behavior**: Uses Windows toast notifications
### Linux
- **Desktop Environment Dependent**: Behavior varies by DE (GNOME, KDE, etc.)
- **Features**: Supports subtitles and themes
- **Limitations**: Does not support user text input
- **Behavior**: Uses native notification system when available
## Best Practices
1. **Check Platform Support**: Always verify notifications are available before using them
2. **Handle Authorization**: Properly request and check permissions on macOS
3. **Use Descriptive Content**: Provide clear titles, subtitles, and action button labels
4. **Handle Responses**: Always implement proper error handling for notification responses
5. **Test Across Platforms**: Verify functionality on your target platforms
6. **Clean Up**: Remove old notification categories when they're no longer needed

View file

@ -235,7 +235,7 @@ var AllWeekdays = []struct {
} }
``` ```
```go {10-12} ```go {14-16}
//... //...
err := wails.Run(&options.App{ err := wails.Run(&options.App{
Title: "Basic Demo", Title: "Basic Demo",

View file

@ -23,6 +23,13 @@ This method unregisters the listener for the given event name, optionally multip
Go: `EventsOff(ctx context.Context, eventName string, additionalEventNames ...string)`<br/> Go: `EventsOff(ctx context.Context, eventName string, additionalEventNames ...string)`<br/>
JS: `EventsOff(eventName string, ...additionalEventNames)` JS: `EventsOff(eventName string, ...additionalEventNames)`
### EventsOffAll
This method unregisters all event listeners.
Go: `EventsOffAll(ctx context.Context)`<br/>
JS: `EventsOffAll()`
### EventsOnce ### EventsOnce
This method sets up a listener for the given event name, but will only trigger once. It returns a function to cancel This method sets up a listener for the given event name, but will only trigger once. It returns a function to cancel

View file

@ -98,3 +98,46 @@ interface EnvironmentInfo {
arch: string; arch: string;
} }
``` ```
### ResetSignalHandlers
Resets signal handlers to allow panic recovery from nil pointer dereferences and other memory access violations.
Go: `ResetSignalHandlers()`
:::info Linux Only
This function only has an effect on Linux. On macOS and Windows, it is a no-op.
On Linux, WebKit (used for the webview) may install signal handlers without the `SA_ONSTACK` flag, which prevents
Go from properly recovering from panics caused by nil pointer dereferences (SIGSEGV) or other memory access violations.
Call this function immediately before code that might panic to ensure the signal handlers are properly configured
for Go's panic recovery mechanism.
:::
#### Example
```go
go func() {
defer func() {
if err := recover(); err != nil {
log.Printf("Recovered from panic: %v", err)
}
}()
// Reset signal handlers right before potentially dangerous code
runtime.ResetSignalHandlers()
// Code that might cause a nil pointer dereference...
var t *time.Time
fmt.Println(t.Unix()) // This would normally crash on Linux
}()
```
:::warning
This function must be called in each goroutine where you want panic recovery to work, and should be called
immediately before the code that might panic, as WebKit may reset the signal handlers at any time.
:::

View file

@ -0,0 +1,601 @@
---
sidebar_position: 6
---
# Notification
This part of the runtime provides access to native system notifications with support for interactive elements like action buttons and text input fields.
### InitializeNotifications
Initializes the notification system. It should be called during app startup.
**Go:** `InitializeNotifications(ctx context.Context) error`
**JavaScript:** `InitializeNotifications(): Promise<void>`
Returns: Error if initialization fails
**Example:**
```go
err := runtime.InitializeNotifications(ctx)
if err != nil {
log.Fatal(err)
}
```
```javascript
await runtime.InitializeNotifications();
```
### IsNotificationAvailable
Checks if notifications are supported on the current platform.
**Go:** `IsNotificationAvailable(ctx context.Context) bool`
**JavaScript:** `IsNotificationAvailable(): Promise<boolean>`
Returns: `true` if notifications are supported, `false` otherwise
**Example:**
```go
if !runtime.IsNotificationAvailable(ctx) {
log.Println("Notifications not available on this platform")
}
```
```javascript
const available = await runtime.IsNotificationAvailable();
if (!available) {
console.log("Notifications not available on this platform");
}
```
### RequestNotificationAuthorization
Requests permission to display notifications (macOS only). On Windows and Linux, this always returns `true`.
**Go:** `RequestNotificationAuthorization(ctx context.Context) (bool, error)`
**JavaScript:** `RequestNotificationAuthorization(): Promise<boolean>`
Returns: Authorization status and error
**Example:**
```go
authorized, err := runtime.RequestNotificationAuthorization(ctx)
```
```javascript
const authorized = await runtime.RequestNotificationAuthorization();
```
### CheckNotificationAuthorization
Checks the current notification authorization status (macOS only). On Windows and Linux, this always returns `true`.
**Go:** `CheckNotificationAuthorization(ctx context.Context) (bool, error)`
**JavaScript:** `CheckNotificationAuthorization(): Promise<boolean>`
Returns: Authorization status and error
**Example:**
```go
authorized, err := runtime.CheckNotificationAuthorization(ctx)
```
```javascript
const authorized = await runtime.CheckNotificationAuthorization();
```
### CleanupNotifications
Cleans up notification resources and releases any held connections. This should be called when shutting down the application, particularly on Linux where it closes the D-Bus connection.
**Go:** `CleanupNotifications(ctx context.Context)`
**JavaScript:** `CleanupNotifications(): Promise<void>`
**Example:**
```go
runtime.CleanupNotifications(ctx)
```
```javascript
await runtime.CleanupNotifications();
```
### SendNotification
Sends a basic notification to the system.
**Go:** `SendNotification(ctx context.Context, options NotificationOptions) error`
**JavaScript:** `SendNotification(options: NotificationOptions): Promise<void>`
Returns: Error if the notification fails to send
**Example:**
```go
err := runtime.SendNotification(ctx, runtime.NotificationOptions{
ID: "notif-1",
Title: "Hello",
Body: "This is a notification",
})
```
```javascript
await runtime.SendNotification({
id: "notif-1",
title: "Hello",
body: "This is a notification"
});
```
### SendNotificationWithActions
Sends an interactive notification with predefined actions. Requires a registered notification category. If the category is not found or `CategoryID` is empty, a basic notification will be sent instead.
**Go:** `SendNotificationWithActions(ctx context.Context, options NotificationOptions) error`
**JavaScript:** `SendNotificationWithActions(options: NotificationOptions): Promise<void>`
Returns: Error if the notification fails to send
**Example:**
```go
err := runtime.SendNotificationWithActions(ctx, runtime.NotificationOptions{
ID: "notif-2",
Title: "Task Reminder",
Body: "Complete your task",
CategoryID: "TASK_CATEGORY",
})
```
```javascript
await runtime.SendNotificationWithActions({
id: "notif-2",
title: "Task Reminder",
body: "Complete your task",
categoryId: "TASK_CATEGORY"
});
```
### RegisterNotificationCategory
Registers a notification category that can be used with interactive notifications. Registering a category with the same ID as a previously registered category will override it.
**Go:** `RegisterNotificationCategory(ctx context.Context, category NotificationCategory) error`
**JavaScript:** `RegisterNotificationCategory(category: NotificationCategory): Promise<void>`
Returns: Error if registration fails
**Example:**
```go
err := runtime.RegisterNotificationCategory(ctx, runtime.NotificationCategory{
ID: "TASK_CATEGORY",
Actions: []runtime.NotificationAction{
{ID: "COMPLETE", Title: "Complete"},
{ID: "CANCEL", Title: "Cancel"},
},
})
```
```javascript
await runtime.RegisterNotificationCategory({
id: "TASK_CATEGORY",
actions: [
{id: "COMPLETE", title: "Complete"},
{id: "CANCEL", title: "Cancel"}
]
});
```
### RemoveNotificationCategory
Removes a previously registered notification category.
**Go:** `RemoveNotificationCategory(ctx context.Context, categoryId string) error`
**JavaScript:** `RemoveNotificationCategory(categoryId: string): Promise<void>`
Returns: Error if removal fails
**Example:**
```go
err := runtime.RemoveNotificationCategory(ctx, "TASK_CATEGORY")
```
```javascript
await runtime.RemoveNotificationCategory("TASK_CATEGORY");
```
### RemoveAllPendingNotifications
Removes all pending notifications (macOS and Linux only).
**Go:** `RemoveAllPendingNotifications(ctx context.Context) error`
**JavaScript:** `RemoveAllPendingNotifications(): Promise<void>`
Returns: Error if removal fails
**Example:**
```go
err := runtime.RemoveAllPendingNotifications(ctx)
```
```javascript
await runtime.RemoveAllPendingNotifications();
```
### RemovePendingNotification
Removes a specific pending notification (macOS and Linux only).
**Go:** `RemovePendingNotification(ctx context.Context, identifier string) error`
**JavaScript:** `RemovePendingNotification(identifier: string): Promise<void>`
Returns: Error if removal fails
**Example:**
```go
err := runtime.RemovePendingNotification(ctx, "notif-1")
```
```javascript
await runtime.RemovePendingNotification("notif-1");
```
### RemoveAllDeliveredNotifications
Removes all delivered notifications (macOS and Linux only).
**Go:** `RemoveAllDeliveredNotifications(ctx context.Context) error`
**JavaScript:** `RemoveAllDeliveredNotifications(): Promise<void>`
Returns: Error if removal fails
**Example:**
```go
err := runtime.RemoveAllDeliveredNotifications(ctx)
```
```javascript
await runtime.RemoveAllDeliveredNotifications();
```
### RemoveDeliveredNotification
Removes a specific delivered notification (macOS and Linux only).
**Go:** `RemoveDeliveredNotification(ctx context.Context, identifier string) error`
**JavaScript:** `RemoveDeliveredNotification(identifier: string): Promise<void>`
Returns: Error if removal fails
**Example:**
```go
err := runtime.RemoveDeliveredNotification(ctx, "notif-1")
```
```javascript
await runtime.RemoveDeliveredNotification("notif-1");
```
### RemoveNotification
Removes a notification by identifier (Linux only). On macOS and Windows, this is a stub that always returns `nil`.
**Go:** `RemoveNotification(ctx context.Context, identifier string) error`
**JavaScript:** `RemoveNotification(identifier: string): Promise<void>`
Returns: Error if removal fails
**Example:**
```go
err := runtime.RemoveNotification(ctx, "notif-1")
```
```javascript
await runtime.RemoveNotification("notif-1");
```
### OnNotificationResponse
Registers a callback function to handle notification responses when users interact with notifications.
**Go:** `OnNotificationResponse(ctx context.Context, callback func(result NotificationResult))`
:::note JavaScript
`OnNotificationResponse` is not available in the JavaScript runtime. Instead, JavaScript applications should use the [Events API](/docs/reference/runtime/events) to listen for notification responses. From your Go callback, emit an event that your JavaScript code can listen to.
**Example:**
```go
runtime.OnNotificationResponse(ctx, func(result runtime.NotificationResult) {
if result.Error != nil {
return
}
// Emit an event that JavaScript can listen to
runtime.EventsEmit(ctx, "notification-response", result.Response)
})
```
```javascript
runtime.EventsOn("notification-response", (response) => {
console.log("Notification response:", response);
switch (response.actionIdentifier) {
case "COMPLETE":
// Handle complete action
break;
case "CANCEL":
// Handle cancel action
break;
}
});
```
:::
## Options
### NotificationOptions
**Go:**
```go
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"`
}
```
**TypeScript:**
```typescript
interface NotificationOptions {
id: string;
title: string;
subtitle?: string; // macOS and Linux only
body?: string;
categoryId?: string;
data?: { [key: string]: any };
}
```
| Field | Description | Win | Mac | Lin |
|-------------|------------------------------------------------|-----|-----|-----|
| ID | Unique identifier for the notification | ✅ | ✅ | ✅ |
| Title | Main notification title | ✅ | ✅ | ✅ |
| Subtitle | Subtitle text (macOS and Linux only) | | ✅ | ✅ |
| Body | Main notification content | ✅ | ✅ | ✅ |
| CategoryID | Category identifier for interactive notifications | ✅ | ✅ | ✅ |
| Data | Custom data to associate with the notification | ✅ | ✅ | ✅ |
### NotificationCategory
**Go:**
```go
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"`
}
```
**TypeScript:**
```typescript
interface NotificationCategory {
id?: string;
actions?: NotificationAction[];
hasReplyField?: boolean;
replyPlaceholder?: string;
replyButtonTitle?: string;
}
```
| Field | Description | Win | Mac | Lin |
|------------------|------------------------------------------------|-----|-----|-----|
| ID | Unique identifier for the category | ✅ | ✅ | ✅ |
| Actions | Array of action buttons | ✅ | ✅ | ✅ |
| HasReplyField | Whether to include a text input field | ✅ | ✅ | |
| ReplyPlaceholder | Placeholder text for the input field | ✅ | ✅ | |
| ReplyButtonTitle | Text for the reply button | ✅ | ✅ | |
### NotificationAction
**Go:**
```go
type NotificationAction struct {
ID string `json:"id,omitempty"`
Title string `json:"title,omitempty"`
Destructive bool `json:"destructive,omitempty"` // (macOS-specific)
}
```
**TypeScript:**
```typescript
interface NotificationAction {
id?: string;
title?: string;
destructive?: boolean; // macOS-specific
}
```
| Field | Description | Win | Mac | Lin |
|-------------|------------------------------------------------|----------------|-----|-----|
| ID | Unique identifier for the action | ✅ | ✅ | ✅ |
| Title | Button text | ✅ | ✅ | ✅ |
| Destructive | Whether the action is destructive (macOS-only) | | ✅ | |
#### macOS-specific Behavior
On macOS, the `Destructive` flag causes the action button to appear in red, indicating it's a destructive action (like delete or cancel). On Windows and Linux, this flag is ignored.
Example:
```go
actions := []runtime.NotificationAction{
{ID: "SAVE", Title: "Save"},
{ID: "DELETE", Title: "Delete", Destructive: true}, // Shows as red button on macOS
}
```
### NotificationResponse
```go
type NotificationResponse struct {
ID string `json:"id,omitempty"`
ActionIdentifier string `json:"actionIdentifier,omitempty"`
CategoryID string `json:"categoryId,omitempty"` // Consistent with NotificationOptions
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"`
}
```
| Field | Description | Win | Mac | Lin |
|------------------|------------------------------------------------|-----|-----|-----|
| ID | Notification identifier | ✅ | ✅ | ✅ |
| ActionIdentifier | Action that was triggered | ✅ | ✅ | ✅ |
| CategoryID | Category of the notification | ✅ | ✅ | ✅ |
| Title | Title of the notification | ✅ | ✅ | ✅ |
| Subtitle | Subtitle of the notification (macOS and Linux only) | | ✅ | ✅ |
| Body | Body text of the notification | ✅ | ✅ | ✅ |
| UserText | Text entered by the user | ✅ | ✅ | |
| UserInfo | Custom data from the notification | ✅ | ✅ | ✅ |
### NotificationResult
```go
type NotificationResult struct {
Response NotificationResponse
Error error
}
```
| Field | Description |
|----------|------------------------------------------------|
| Response | The notification response data |
| Error | Any error that occurred during the interaction |
## Platform-Specific Behavior
### macOS
- **Authorization Required**: Apps must request notification permission before sending notifications
- **Notarization**: Apps must be notarized for distribution
- **Features**: All features supported including subtitles, text input, and destructive actions
- **Styling**: Automatically adapts to system dark/light mode
- **Center**: Notifications appear in macOS Notification Center
**Example:**
```go
// Check and request authorization
authorized, err := runtime.CheckNotificationAuthorization(ctx)
if err != nil {
return err
}
if !authorized {
authorized, err = runtime.RequestNotificationAuthorization(ctx)
if err != nil || !authorized {
return fmt.Errorf("notification authorization denied")
}
}
// Now send notifications
```
```javascript
// Check and request authorization
let authorized = await runtime.CheckNotificationAuthorization();
if (!authorized) {
authorized = await runtime.RequestNotificationAuthorization();
if (!authorized) {
throw new Error("Notification authorization denied");
}
}
// Now send notifications
```
### Windows
- **No Authorization**: Permission system not required
- **Features**: Supports text input and high DPI displays
- **Limitations**: Subtitle not supported
- **Styling**: Adapts to Windows theme settings
- **Behavior**: Uses Windows toast notification system
### Linux
- **Desktop Environment Dependent**: Behavior varies by DE (GNOME, KDE, XFCE, etc.)
- **Features**: Supports subtitles
- **Limitations**: User text input not supported
- **Styling**: Follows desktop environment theme
- **Behavior**: Uses native notification system when available
**Example:**
```go
// Check system support
if !runtime.IsNotificationAvailable(ctx) {
return fmt.Errorf("notifications not supported on this Linux desktop")
}
// Linux notifications may not support text input
// Only use actions that don't require user text
```
```javascript
// Check system support
const available = await runtime.IsNotificationAvailable();
if (!available) {
throw new Error("Notifications not supported on this Linux desktop");
}
// Linux notifications may not support text input
// Only use actions that don't require user text
```
## Action Identifiers
When handling notification responses, these special action identifiers may be present:
- `DEFAULT_ACTION`: Triggered when the user clicks the notification itself (not an action button)
- `TEXT_REPLY`: Triggered when the user submits text via the reply field
Example response handling:
```go
runtime.OnNotificationResponse(ctx, func(result runtime.NotificationResult) {
if result.Error != nil {
fmt.Printf("Response error: %v\n", result.Error)
return
}
response := result.Response
switch response.ActionIdentifier {
case "DEFAULT_ACTION":
fmt.Println("User clicked the notification")
case "TEXT_REPLY":
fmt.Printf("User replied: %s\n", response.UserText)
case "COMPLETE":
fmt.Println("User clicked Complete button")
case "CANCEL":
fmt.Println("User clicked Cancel button")
}
})
```

View file

@ -18,6 +18,13 @@ This method unregisters the listener for the given event name, optionally multip
Go: `EventsOff(ctx context.Context, eventName string, additionalEventNames ...string)`<br/> JS: `EventsOff(eventName string, ...additionalEventNames)` Go: `EventsOff(ctx context.Context, eventName string, additionalEventNames ...string)`<br/> JS: `EventsOff(eventName string, ...additionalEventNames)`
### EventsOffAll 移除所有事件侦听器
此方法注销所有事件侦听器。
Go: `EventsOffAll(ctx context.Context)`<br/>
JS: `EventsOffAll()`
### EventsOnce 添加只触发一次的事件侦听器 ### EventsOnce 添加只触发一次的事件侦听器
此方法为给定的事件名称设置一个侦听器,但只会触发一次。 它返回 一个函数来取消侦听器。 此方法为给定的事件名称设置一个侦听器,但只会触发一次。 它返回 一个函数来取消侦听器。

View file

@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed `wails init` to prevent initialization in non-empty directories when using the `-d` flag, avoiding accidental data loss [`#4940`](https://github.com/wailsapp/wails/issues/4940) by `@leaanthony`
- Fixed missing `EventsOffAll` in runtime templates for all frontend frameworks [#4883](https://github.com/wailsapp/wails/pull/4883) by @narcilee7
- Fixed Linux crash on panic in JS-bound Go methods due to WebKit overriding signal handlers [#3965](https://github.com/wailsapp/wails/issues/3965) by @leaanthony
- Fixed code block range in "How Does It Work?" documentation [#4884](https://github.com/wailsapp/wails/pull/4884) by @msal4
- Fixed WebView crash on macOS 26 (Tahoe) during rapid UI updates [#4592](https://github.com/wailsapp/wails/issues/4592) by @leaanthony - Fixed WebView crash on macOS 26 (Tahoe) during rapid UI updates [#4592](https://github.com/wailsapp/wails/issues/4592) by @leaanthony
- Updated menu reference docs with complete imports by @agilgur5 in [#4727](https://github.com/wailsapp/wails/pull/4727) and [#4742](https://github.com/wailsapp/wails/pull/4742) - Updated menu reference docs with complete imports by @agilgur5 in [#4727](https://github.com/wailsapp/wails/pull/4727) and [#4742](https://github.com/wailsapp/wails/pull/4742)
- Fixed menu reference syntax by @agilgur5 in [#4726](https://github.com/wailsapp/wails/pull/4726) - Fixed menu reference syntax by @agilgur5 in [#4726](https://github.com/wailsapp/wails/pull/4726)
@ -24,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed link to CoC in Community Guide when there was a trailing slash by @agilgur5 in [#4732](https://github.com/wailsapp/wails/pull/4732) - Fixed link to CoC in Community Guide when there was a trailing slash by @agilgur5 in [#4732](https://github.com/wailsapp/wails/pull/4732)
- Fixed indentation in "How does it work?" page by @agilgur5 in [#4733](https://github.com/wailsapp/wails/pull/4733) - Fixed indentation in "How does it work?" page by @agilgur5 in [#4733](https://github.com/wailsapp/wails/pull/4733)
- Updated wails installation documentation to allow copying the `install wails` command with one click by @tilak999 in [#4692](https://github.com/wailsapp/wails/pull/4692) - Updated wails installation documentation to allow copying the `install wails` command with one click by @tilak999 in [#4692](https://github.com/wailsapp/wails/pull/4692)
- Remove ioutl.Discard and replace it with io.Discard by @xjh22222228 in [#4877](https://github.com/wailsapp/wails/pull/4877)
## v2.11.0 - 2025-11-08 ## v2.11.0 - 2025-11-08
@ -35,11 +40,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `build:tags` to project specification for automatically adding compilation tags by @symball in [PR](https://github.com/wailsapp/wails/pull/4439) - Added `build:tags` to project specification for automatically adding compilation tags by @symball in [PR](https://github.com/wailsapp/wails/pull/4439)
- Support for binding generics in [PR](https://github.dev/wailsapp/wails/pull/3626) by @ktsivkov - Support for binding generics in [PR](https://github.dev/wailsapp/wails/pull/3626) by @ktsivkov
- Add universal link support for macOS by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/4693) - Add universal link support for macOS by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/4693)
- Notifications API in [PR](https://github.com/wailsapp/wails/pull/4256) by @popaprozac
### Fixed ### Fixed
- Added url validation for BrowserOpenURL by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/4484) - Added url validation for BrowserOpenURL by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/4484)
- Fixed C compilation error in onWayland on Linux due to declaration after label [#4446](https://github.com/wailsapp/wails/pull/4446) by [@jaesung9507](https://github.com/jaesung9507) - Fixed C compilation error in onWayland on Linux due to declaration after label [#4446](https://github.com/wailsapp/wails/pull/4446) by [@jaesung9507](https://github.com/jaesung9507)
- Use computed style when adding 'wails-drop-target-active' [PR](https://github.com/wailsapp/wails/pull/4420) by [@riannucci](https://github.com/riannucci) - Use computed style when adding 'wails-drop-target-active' [PR](https://github.com/wailsapp/wails/pull/4420) by [@riannucci](https://github.com/riannucci)
- Fixed initialisation templates to the correct Go version [PR](https://github.com/wailsapp/wails/pull/4618) by [@Xelus22](https://github.com/Xelus22)
- Fixed panic when adding menuroles on Linux [#4558](https://github.com/wailsapp/wails/pull/4558) by [@jaesung9507](https://github.com/jaesung9507) - Fixed panic when adding menuroles on Linux [#4558](https://github.com/wailsapp/wails/pull/4558) by [@jaesung9507](https://github.com/jaesung9507)
- Fixed generated enums ordering [#4664](https://github.com/wailsapp/wails/pull/4664) by [@rprtr258](https://github.com/rprtr258). - Fixed generated enums ordering [#4664](https://github.com/wailsapp/wails/pull/4664) by [@rprtr258](https://github.com/rprtr258).
- Fixed Discord badge in README by @sharkmu in [PR](https://github.com/wailsapp/wails/pull/4626) - Fixed Discord badge in README by @sharkmu in [PR](https://github.com/wailsapp/wails/pull/4626)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Before After
Before After