mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-15 15:15:51 +01:00
Compare commits
24 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51d30325fc |
||
|
|
f8595e3052 |
||
|
|
4d0abeb37c |
||
|
|
033650d792 |
||
|
|
4c49f27edf |
||
|
|
c84578721c |
||
|
|
354fee648e |
||
|
|
da3ce17161 |
||
|
|
bbd1b33122 |
||
|
|
ae40ca4ac1 |
||
|
|
093aa2d663 |
||
|
|
e906751c89 |
||
|
|
718fd92f85 |
||
|
|
01b661f6a5 |
||
|
|
896344eb66 |
||
|
|
8fd0340404 |
||
|
|
bc4ee373b5 | ||
|
|
5d39f1aa9a | ||
|
|
a27995940a |
||
|
|
4e8b705cda |
||
|
|
7ee2b2d856 |
||
|
|
696c55c6ee |
||
|
|
0357730294 |
||
|
|
158fd41a8b |
97 changed files with 5623 additions and 170 deletions
372
.github/workflows/build-cross-image.yml
vendored
372
.github/workflows/build-cross-image.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
44
.github/workflows/claude-code-review.yml
vendored
Normal file
44
.github/workflows/claude-code-review.yml
vendored
Normal 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
50
.github/workflows/claude.yml
vendored
Normal 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:*)'
|
||||||
|
|
||||||
76
v2/examples/panic-recovery-test/README.md
Normal file
76
v2/examples/panic-recovery-test/README.md
Normal 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
|
||||||
44
v2/examples/panic-recovery-test/app.go
Normal file
44
v2/examples/panic-recovery-test/app.go
Normal 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)
|
||||||
|
}
|
||||||
12
v2/examples/panic-recovery-test/frontend/index.html
Normal file
12
v2/examples/panic-recovery-test/frontend/index.html
Normal 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>
|
||||||
13
v2/examples/panic-recovery-test/frontend/package.json
Normal file
13
v2/examples/panic-recovery-test/frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
54
v2/examples/panic-recovery-test/frontend/src/app.css
Normal file
54
v2/examples/panic-recovery-test/frontend/src/app.css
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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.
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
55
v2/examples/panic-recovery-test/frontend/src/main.js
Normal file
55
v2/examples/panic-recovery-test/frontend/src/main.js
Normal 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);
|
||||||
26
v2/examples/panic-recovery-test/frontend/src/style.css
Normal file
26
v2/examples/panic-recovery-test/frontend/src/style.css
Normal 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;
|
||||||
|
}
|
||||||
4
v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
4
v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.d.ts
vendored
Executable 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>;
|
||||||
7
v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.js
Executable file
7
v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.js
Executable 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);
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
249
v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal 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
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
5
v2/examples/panic-recovery-test/go.mod
Normal file
5
v2/examples/panic-recovery-test/go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
module panic-recovery-test
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require github.com/wailsapp/wails/v2 v2.11.0
|
||||||
36
v2/examples/panic-recovery-test/main.go
Normal file
36
v2/examples/panic-recovery-test/main.go
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
13
v2/examples/panic-recovery-test/wails.json
Normal file
13
v2/examples/panic-recovery-test/wails.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
v2/go.mod
10
v2/go.mod
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
23
v2/go.sum
23
v2/go.sum
|
|
@ -8,6 +8,8 @@ atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
||||||
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
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=
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
465
v2/internal/frontend/desktop/darwin/notifications.go
Normal file
465
v2/internal/frontend/desktop/darwin/notifications.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
594
v2/internal/frontend/desktop/linux/notifications.go
Normal file
594
v2/internal/frontend/desktop/linux/notifications.go
Normal 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 := ¬ificationData{
|
||||||
|
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 := ¬ificationData{
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
489
v2/internal/frontend/desktop/windows/notifications.go
Normal file
489
v2/internal/frontend/desktop/windows/notifications.go
Normal 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())
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
200
v2/internal/frontend/runtime/desktop/notifications.js
Normal file
200
v2/internal/frontend/runtime/desktop/notifications.js
Normal 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
|
|
@ -246,4 +246,85 @@ export function OnFileDropOff() :void
|
||||||
export function CanResolveFilePaths(): boolean;
|
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>;
|
||||||
|
|
@ -239,4 +239,60 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
136
v2/pkg/runtime/notifications.go
Normal file
136
v2/pkg/runtime/notifications.go
Normal 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)
|
||||||
|
}
|
||||||
65
v2/pkg/runtime/signal_linux.go
Normal file
65
v2/pkg/runtime/signal_linux.go
Normal 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()
|
||||||
|
}
|
||||||
18
v2/pkg/runtime/signal_other.go
Normal file
18
v2/pkg/runtime/signal_other.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
233
website/docs/guides/notifications.mdx
Normal file
233
website/docs/guides/notifications.mdx
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
|
||||||
601
website/docs/reference/runtime/notification.mdx
Normal file
601
website/docs/reference/runtime/notification.mdx
Normal 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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
@ -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 添加只触发一次的事件侦听器
|
||||||
|
|
||||||
此方法为给定的事件名称设置一个侦听器,但只会触发一次。 它返回 一个函数来取消侦听器。
|
此方法为给定的事件名称设置一个侦听器,但只会触发一次。 它返回 一个函数来取消侦听器。
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ 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 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)
|
||||||
|
|
@ -25,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
|
||||||
|
|
||||||
|
|
@ -36,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 |
Loading…
Add table
Add a link
Reference in a new issue