Merge origin/v3-alpha into v3-alpha-feature/android-support

This commit is contained in:
Lea Anthony 2025-11-28 21:19:02 +11:00
commit ea2e0ec891
519 changed files with 48115 additions and 13814 deletions

View file

@ -79,6 +79,10 @@ jobs:
working-directory: v3/internal/runtime/desktop/@wailsio/runtime
run: npm run build
- name: Pack runtime for template tests
working-directory: v3/internal/runtime/desktop/@wailsio/runtime
run: npm pack
- name: Store runtime build artifacts
uses: actions/upload-artifact@v4
with:
@ -88,6 +92,12 @@ jobs:
v3/internal/runtime/desktop/@wailsio/runtime/types/
v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.tsbuildinfo
- name: Store runtime package
uses: actions/upload-artifact@v4
with:
name: runtime-package
path: v3/internal/runtime/desktop/@wailsio/runtime/*.tgz
test_go:
name: Run Go Tests v3
needs: [check_approval, test_js]
@ -165,17 +175,19 @@ jobs:
cleanup:
name: Cleanup build artifacts
if: always()
needs: [test_js, test_go]
needs: [test_js, test_go, test_templates]
runs-on: ubuntu-latest
steps:
- uses: geekyeggo/delete-artifact@v5
with:
name: runtime-build-artifacts
name: |
runtime-build-artifacts
runtime-package
failOnError: false
test_templates:
name: Test Templates
needs: test_go
needs: [test_js, test_go]
runs-on: ${{ matrix.os }}
if: github.base_ref == 'v3-alpha'
strategy:
@ -226,12 +238,28 @@ jobs:
task install
wails3 doctor
- name: Download runtime package
uses: actions/download-artifact@v4
with:
name: runtime-package
path: wails-runtime-temp
- name: Generate template '${{ matrix.template }}'
shell: bash
run: |
# Get absolute path - use pwd -W on Windows for native paths, pwd elsewhere
if [[ "$RUNNER_OS" == "Windows" ]]; then
RUNTIME_TGZ="$(cd wails-runtime-temp && pwd -W)/$(ls wails-runtime-temp/*.tgz | xargs basename)"
else
RUNTIME_TGZ="$(cd wails-runtime-temp && pwd)/$(ls wails-runtime-temp/*.tgz | xargs basename)"
fi
mkdir -p ./test-${{ matrix.template }}
cd ./test-${{ matrix.template }}
wails3 init -n ${{ matrix.template }} -t ${{ matrix.template }}
cd ${{ matrix.template }}
cd ${{ matrix.template }}/frontend
# Replace @wailsio/runtime version with local tarball
npm pkg set dependencies.@wailsio/runtime="file://$RUNTIME_TGZ"
cd ..
wails3 build
build_results:

View file

@ -168,562 +168,43 @@ jobs:
if: |
steps.quick_check.outputs.should_continue == 'true' ||
github.event.inputs.force_release == 'true'
env:
WAILS_REPO_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
GITHUB_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
run: |
cd v3
echo "🚀 Running release task..."
echo "======================================================="
# Initialize error tracking
RELEASE_ERRORS=""
RELEASE_SUCCESS=true
# Store the original version for comparison
ORIGINAL_VERSION=$(cat internal/version/version.txt 2>/dev/null || echo "unknown")
echo "📌 Current version: $ORIGINAL_VERSION"
# Run the release task and capture output with error handling
cd v3/tasks/release
ARGS=()
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
echo "🧪 DRY RUN MODE: Simulating release task execution"
# In dry run, we'll simulate the task without making actual changes
OUTPUT=$(task release 2>&1 || true)
RELEASE_EXIT_CODE=0 # Always succeed in dry run
echo "$OUTPUT"
else
echo "🚀 LIVE MODE: Executing release task"
OUTPUT=$(task release 2>&1)
RELEASE_EXIT_CODE=$?
echo "$OUTPUT"
if [ $RELEASE_EXIT_CODE -ne 0 ]; then
echo "❌ Release task failed with exit code $RELEASE_EXIT_CODE"
RELEASE_ERRORS="$RELEASE_ERRORS\n- Release task execution failed: $OUTPUT"
RELEASE_SUCCESS=false
else
echo "✅ Release task completed successfully"
fi
fi
# Verify version file exists and is readable
if [ ! -f "internal/version/version.txt" ]; then
echo "❌ Version file not found: internal/version/version.txt"
RELEASE_ERRORS="$RELEASE_ERRORS\n- Version file not found after release task execution"
RELEASE_SUCCESS=false
RELEASE_VERSION="unknown"
else
RELEASE_VERSION=$(cat internal/version/version.txt 2>/dev/null || echo "unknown")
if [ "$RELEASE_VERSION" == "unknown" ]; then
echo "❌ Failed to read version from file"
RELEASE_ERRORS="$RELEASE_ERRORS\n- Failed to read version from version.txt"
RELEASE_SUCCESS=false
else
echo "✅ Successfully read version: $RELEASE_VERSION"
fi
fi
# Check if version changed
VERSION_CHANGED="false"
if [ "$ORIGINAL_VERSION" != "$RELEASE_VERSION" ] && [ "$RELEASE_VERSION" != "unknown" ]; then
echo "✅ Version changed from $ORIGINAL_VERSION to $RELEASE_VERSION"
VERSION_CHANGED="true"
else
echo " Version unchanged: $RELEASE_VERSION"
fi
RELEASE_TAG="${RELEASE_VERSION}"
RELEASE_TITLE="Wails ${RELEASE_VERSION}"
# Set outputs for next steps
echo "version=$RELEASE_VERSION" >> $GITHUB_OUTPUT
echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
echo "title=$RELEASE_TITLE" >> $GITHUB_OUTPUT
echo "is_prerelease=true" >> $GITHUB_OUTPUT
echo "is_latest=false" >> $GITHUB_OUTPUT
echo "has_changes=${{ steps.changelog_check.outputs.has_unreleased_content }}" >> $GITHUB_OUTPUT
echo "success=$RELEASE_SUCCESS" >> $GITHUB_OUTPUT
echo "version_changed=$VERSION_CHANGED" >> $GITHUB_OUTPUT
# Generate release notes from UNRELEASED_CHANGELOG.md if it has content
if [ "${{ steps.changelog_check.outputs.has_unreleased_content }}" == "true" ] && [ "$RELEASE_SUCCESS" == "true" ]; then
echo "📝 Generating release notes from UNRELEASED_CHANGELOG.md..."
# Use the release script to extract changelog content
cd tasks/release
if CHANGELOG_CONTENT=$(go run release.go --extract-changelog 2>&1); then
if [ -n "$CHANGELOG_CONTENT" ] && [ "$CHANGELOG_CONTENT" != "No changelog content found." ]; then
echo "### Changes in this release:" > ../../release-notes.txt
echo "" >> ../../release-notes.txt
echo "$CHANGELOG_CONTENT" >> ../../release-notes.txt
echo "✅ Successfully extracted changelog content"
echo "release_notes_file=release-notes.txt" >> $GITHUB_OUTPUT
else
echo " No changelog content to extract"
echo "No detailed changelog available for this release." > ../../release-notes.txt
echo "release_notes_file=release-notes.txt" >> $GITHUB_OUTPUT
fi
else
echo "⚠️ Failed to extract changelog content: $CHANGELOG_CONTENT"
echo "No detailed changelog available for this release." > ../../release-notes.txt
RELEASE_ERRORS="$RELEASE_ERRORS\n- Failed to extract changelog content for release notes"
echo "release_notes_file=release-notes.txt" >> $GITHUB_OUTPUT
fi
cd ../..
else
echo "release_notes_file=" >> $GITHUB_OUTPUT
if [ "$RELEASE_SUCCESS" != "true" ]; then
echo "⚠️ Skipping release notes generation due to release task failure"
fi
fi
# Set error output for later steps
if [ -n "$RELEASE_ERRORS" ]; then
echo "release_errors<<EOF" >> $GITHUB_OUTPUT
echo -e "$RELEASE_ERRORS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "has_release_errors=true" >> $GITHUB_OUTPUT
else
echo "has_release_errors=false" >> $GITHUB_OUTPUT
fi
- name: Commit and push changes
id: git_commit
if: |
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
github.event.inputs.dry_run != 'true' &&
steps.release.outputs.success == 'true' &&
steps.release.outputs.version_changed == 'true'
env:
GITHUB_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
run: |
echo "📝 Committing and pushing changes..."
# Initialize error tracking
COMMIT_ERRORS=""
COMMIT_SUCCESS=true
# Add any changes made by the release script with error handling
if git add . 2>&1; then
echo "✅ Successfully staged changes"
else
echo "❌ Failed to stage changes"
COMMIT_ERRORS="$COMMIT_ERRORS\n- Failed to stage changes with git add"
COMMIT_SUCCESS=false
fi
# Check if there are changes to commit
if [ "$COMMIT_SUCCESS" == "true" ]; then
if ! git diff --cached --quiet; then
echo "📝 Changes detected, creating commit..."
# Create commit with error handling
if git commit -m "${{ steps.release.outputs.version }}" 2>&1; then
echo "✅ Successfully created commit"
# Push changes with retry logic
RETRY_COUNT=0
MAX_RETRIES=3
PUSH_SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$PUSH_SUCCESS" == "false" ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "🔄 Attempting to push changes (attempt $RETRY_COUNT/$MAX_RETRIES)..."
if git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" v3-alpha 2>&1; then
echo "✅ Successfully pushed changes to v3-alpha branch"
PUSH_SUCCESS=true
else
echo "❌ Failed to push changes (attempt $RETRY_COUNT/$MAX_RETRIES)"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "⏳ Waiting 5 seconds before retry..."
sleep 5
fi
fi
done
if [ "$PUSH_SUCCESS" == "false" ]; then
echo "❌ Failed to push changes after $MAX_RETRIES attempts"
COMMIT_ERRORS="$COMMIT_ERRORS\n- Failed to push changes after $MAX_RETRIES attempts"
COMMIT_SUCCESS=false
fi
else
echo "❌ Failed to create commit"
COMMIT_ERRORS="$COMMIT_ERRORS\n- Failed to create git commit"
COMMIT_SUCCESS=false
fi
else
echo " No changes to commit"
fi
fi
# Set outputs for later steps
echo "success=$COMMIT_SUCCESS" >> $GITHUB_OUTPUT
if [ -n "$COMMIT_ERRORS" ]; then
echo "commit_errors<<EOF" >> $GITHUB_OUTPUT
echo -e "$COMMIT_ERRORS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "has_commit_errors=true" >> $GITHUB_OUTPUT
else
echo "has_commit_errors=false" >> $GITHUB_OUTPUT
fi
- name: Create and push git tag
id: git_tag
if: |
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
steps.check_tag.outputs.has_tag == 'false' &&
github.event.inputs.dry_run != 'true' &&
steps.release.outputs.success == 'true' &&
steps.release.outputs.version_changed == 'true' &&
steps.git_commit.outputs.success == 'true'
env:
GITHUB_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
run: |
echo "🏷️ Creating and pushing git tag: ${{ steps.release.outputs.tag }}"
# Initialize error tracking
GIT_ERRORS=""
GIT_SUCCESS=true
# Create git tag with error handling
if git tag -a "${{ steps.release.outputs.tag }}" -m "Release ${{ steps.release.outputs.version }}" 2>&1; then
echo "✅ Successfully created git tag: ${{ steps.release.outputs.tag }}"
else
echo "❌ Failed to create git tag"
GIT_ERRORS="$GIT_ERRORS\n- Failed to create git tag: ${{ steps.release.outputs.tag }}"
GIT_SUCCESS=false
fi
# Push tag with retry logic and error handling
if [ "$GIT_SUCCESS" == "true" ]; then
RETRY_COUNT=0
MAX_RETRIES=3
PUSH_SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$PUSH_SUCCESS" == "false" ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "🔄 Attempting to push tag (attempt $RETRY_COUNT/$MAX_RETRIES)..."
if git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" "${{ steps.release.outputs.tag }}" 2>&1; then
echo "✅ Successfully pushed git tag to origin"
PUSH_SUCCESS=true
else
echo "❌ Failed to push git tag (attempt $RETRY_COUNT/$MAX_RETRIES)"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "⏳ Waiting 5 seconds before retry..."
sleep 5
fi
fi
done
if [ "$PUSH_SUCCESS" == "false" ]; then
echo "❌ Failed to push git tag after $MAX_RETRIES attempts"
GIT_ERRORS="$GIT_ERRORS\n- Failed to push git tag after $MAX_RETRIES attempts"
GIT_SUCCESS=false
fi
fi
# Set outputs for later steps
echo "success=$GIT_SUCCESS" >> $GITHUB_OUTPUT
if [ -n "$GIT_ERRORS" ]; then
echo "git_tag_errors<<EOF" >> $GITHUB_OUTPUT
echo -e "$GIT_ERRORS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "has_git_errors=true" >> $GITHUB_OUTPUT
else
echo "has_git_errors=false" >> $GITHUB_OUTPUT
fi
- name: Read release notes
id: read_notes
if: |
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
steps.release.outputs.release_notes_file != '' &&
steps.release.outputs.version_changed == 'true' &&
steps.git_commit.outputs.success == 'true'
run: |
cd v3
if [ -f "release-notes.txt" ]; then
# Read the release notes and handle multiline content
RELEASE_NOTES=$(cat release-notes.txt)
echo "release_notes<<EOF" >> $GITHUB_OUTPUT
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "release_notes=No release notes available" >> $GITHUB_OUTPUT
fi
- name: Test GitHub Release Creation (DRY RUN)
if: |
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
github.event.inputs.dry_run == 'true' &&
steps.release.outputs.version_changed == 'true'
run: |
echo "🧪 DRY RUN: Would create GitHub release with the following parameters:"
echo "======================================================================="
echo "Tag Name: ${{ steps.release.outputs.tag }}"
echo "Release Name: ${{ steps.release.outputs.title }}"
echo "Is Prerelease: ${{ steps.release.outputs.is_prerelease }}"
echo "Is Latest: ${{ steps.release.outputs.is_latest }}"
echo "Has Changes: ${{ steps.release.outputs.has_changes }}"
echo ""
echo "Release Body Preview:"
echo "## Wails v3 Alpha Release - ${{ steps.release.outputs.version }}"
echo ""
cat << 'RELEASE_NOTES_EOF'
${{ steps.read_notes.outputs.release_notes }}
RELEASE_NOTES_EOF
echo ""
echo ""
echo ""
echo "---"
echo ""
echo "🤖 This is an automated nightly release generated from the latest changes in the v3-alpha branch."
echo ""
echo "**Installation:**"
echo "\`\`\`bash"
echo "go install github.com/wailsapp/wails/v3/cmd/wails@${{ steps.release.outputs.tag }}"
echo "\`\`\`"
echo ""
echo "**⚠️ Alpha Warning:** This is pre-release software and may contain bugs or incomplete features."
echo ""
echo "✅ DRY RUN: GitHub release creation test completed successfully!"
- name: Create GitHub Release (LIVE)
id: github_release
if: |
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
github.event.inputs.dry_run != 'true' &&
steps.release.outputs.success == 'true' &&
steps.release.outputs.version_changed == 'true' &&
steps.git_commit.outputs.success == 'true' &&
steps.git_tag.outputs.success == 'true'
continue-on-error: true
run: |
echo "🚀 Creating GitHub release using gh CLI..."
# Create release notes in a temporary file
cat > release_notes.md << 'EOF'
## Wails v3 Alpha Release - ${{ steps.release.outputs.version }}
${{ steps.read_notes.outputs.release_notes }}
---
🤖 This is an automated nightly release generated from the latest changes in the v3-alpha branch.
**Installation:**
```bash
go install github.com/wailsapp/wails/v3/cmd/wails@${{ steps.release.outputs.tag }}
```
**⚠️ Alpha Warning:** This is pre-release software and may contain bugs or incomplete features.
EOF
# Create the release
if gh release create "${{ steps.release.outputs.tag }}" \
--title "${{ steps.release.outputs.title }}" \
--notes-file release_notes.md \
--target v3-alpha \
--prerelease; then
echo "✅ Successfully created GitHub release"
echo "outcome=success" >> $GITHUB_OUTPUT
else
echo "❌ Failed to create GitHub release"
echo "outcome=failure" >> $GITHUB_OUTPUT
fi
env:
GITHUB_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
- name: Handle GitHub Release Creation Result
id: release_result
if: |
(steps.quick_check.outputs.should_continue == 'true' || github.event.inputs.force_release == 'true') &&
github.event.inputs.dry_run != 'true' &&
steps.release.outputs.success == 'true' &&
steps.release.outputs.version_changed == 'true' &&
steps.git_commit.outputs.success == 'true' &&
steps.git_tag.outputs.success == 'true'
run: |
echo "📋 Checking GitHub release creation result..."
# Initialize error tracking
GITHUB_ERRORS=""
GITHUB_SUCCESS=true
# Check if GitHub release creation succeeded
if [ "${{ steps.github_release.outcome }}" == "success" ]; then
echo "✅ GitHub release created successfully"
echo "🔗 Release URL: https://github.com/${{ github.repository }}/releases/tag/${{ steps.release.outputs.tag }}"
else
echo "❌ GitHub release creation failed"
GITHUB_ERRORS="$GITHUB_ERRORS\n- GitHub release creation failed with outcome: ${{ steps.github_release.outcome }}"
GITHUB_SUCCESS=false
fi
# Set outputs for summary
echo "success=$GITHUB_SUCCESS" >> $GITHUB_OUTPUT
if [ -n "$GITHUB_ERRORS" ]; then
echo "github_errors<<EOF" >> $GITHUB_OUTPUT
echo -e "$GITHUB_ERRORS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "has_github_errors=true" >> $GITHUB_OUTPUT
else
echo "has_github_errors=false" >> $GITHUB_OUTPUT
fi
- name: Error Summary and Reporting
id: error_summary
if: always()
run: |
echo "📊 Generating comprehensive error summary..."
# Initialize error tracking
TOTAL_ERRORS=0
ERROR_SUMMARY=""
OVERALL_SUCCESS=true
# Check for changelog errors
if [ "${{ steps.changelog_check.outputs.has_errors }}" == "true" ]; then
echo "❌ Changelog processing errors detected"
ERROR_SUMMARY="$ERROR_SUMMARY\n### 📄 Changelog Processing Errors\n${{ steps.changelog_check.outputs.changelog_errors }}\n"
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
OVERALL_SUCCESS=false
fi
# Check for release script errors
if [ "${{ steps.release.outputs.has_release_errors }}" == "true" ]; then
echo "❌ Release script errors detected"
ERROR_SUMMARY="$ERROR_SUMMARY\n### 🚀 Release Script Errors\n${{ steps.release.outputs.release_errors }}\n"
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
OVERALL_SUCCESS=false
fi
# Check for git tag errors
if [ "${{ steps.git_tag.outputs.has_git_errors }}" == "true" ]; then
echo "❌ Git tag errors detected"
ERROR_SUMMARY="$ERROR_SUMMARY\n### 🏷️ Git Tag Errors\n${{ steps.git_tag.outputs.git_tag_errors }}\n"
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
OVERALL_SUCCESS=false
fi
# Check for git commit errors
if [ "${{ steps.git_commit.outputs.has_commit_errors }}" == "true" ]; then
echo "❌ Git commit errors detected"
ERROR_SUMMARY="$ERROR_SUMMARY\n### 📝 Git Commit Errors\n${{ steps.git_commit.outputs.commit_errors }}\n"
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
OVERALL_SUCCESS=false
fi
# Check for GitHub release errors
if [ "${{ steps.release_result.outputs.has_github_errors }}" == "true" ]; then
echo "❌ GitHub release errors detected"
ERROR_SUMMARY="$ERROR_SUMMARY\n### 🐙 GitHub Release Errors\n${{ steps.release_result.outputs.github_errors }}\n"
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
OVERALL_SUCCESS=false
fi
# Set outputs for final summary
echo "total_errors=$TOTAL_ERRORS" >> $GITHUB_OUTPUT
echo "overall_success=$OVERALL_SUCCESS" >> $GITHUB_OUTPUT
if [ -n "$ERROR_SUMMARY" ]; then
echo "error_summary<<EOF" >> $GITHUB_OUTPUT
echo -e "$ERROR_SUMMARY" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
fi
# Log summary
if [ "$OVERALL_SUCCESS" == "true" ]; then
echo "✅ Workflow completed successfully with no errors"
else
echo "⚠️ Workflow completed with $TOTAL_ERRORS error categories"
ARGS+=(--dry-run)
fi
go run release.go "${ARGS[@]}"
- name: Summary
if: always()
run: |
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
echo "## 🧪 DRY RUN Release Test Summary" >> $GITHUB_STEP_SUMMARY
echo "## 🧪 DRY RUN Release Summary" >> $GITHUB_STEP_SUMMARY
else
echo "## 🚀 Nightly Release Summary" >> $GITHUB_STEP_SUMMARY
fi
echo "================================" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Tag:** ${{ steps.release.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Version Changed:** ${{ steps.release.outputs.version_changed }}" >> $GITHUB_STEP_SUMMARY
echo "- **Has existing tag:** ${{ steps.check_tag.outputs.has_tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Has unreleased changelog content:** ${{ steps.changelog_check.outputs.has_unreleased_content }}" >> $GITHUB_STEP_SUMMARY
echo "- **Has changes:** ${{ steps.release.outputs.has_changes }}" >> $GITHUB_STEP_SUMMARY
echo "- **Is prerelease:** ${{ steps.release.outputs.is_prerelease }}" >> $GITHUB_STEP_SUMMARY
echo "- **Is latest:** ${{ steps.release.outputs.is_latest }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Overall status
if [ "${{ steps.error_summary.outputs.overall_success }}" == "true" ]; then
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
echo "- **Mode:** 🧪 DRY RUN (no actual release created)" >> $GITHUB_STEP_SUMMARY
echo "- **Status:** ✅ Test completed successfully" >> $GITHUB_STEP_SUMMARY
if [ -n "${{ steps.release.outputs.release_version }}" ]; then
echo "- **Version:** ${{ steps.release.outputs.release_version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Tag:** ${{ steps.release.outputs.release_tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Status:** ${{ steps.release.outcome == 'success' && '✅ Success' || '⚠️ Failed' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Mode:** ${{ steps.release.outputs.release_dry_run == 'true' && '🧪 Dry Run' || '🚀 Live release' }}" >> $GITHUB_STEP_SUMMARY
if [ -n "${{ steps.release.outputs.release_url }}" ]; then
echo "- **Release URL:** ${{ steps.release.outputs.release_url }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Changelog" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.changelog_check.outputs.has_unreleased_content }}" == "true" ]; then
echo "✅ Unreleased changelog processed and reset." >> $GITHUB_STEP_SUMMARY
else
echo "- **Mode:** 🚀 Live release" >> $GITHUB_STEP_SUMMARY
echo "- **Status:** ✅ Release created successfully" >> $GITHUB_STEP_SUMMARY
echo " No unreleased changelog content detected." >> $GITHUB_STEP_SUMMARY
fi
else
echo "- **Mode:** ${{ github.event.inputs.dry_run == 'true' && '🧪 DRY RUN' || '🚀 Live release' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Status:** ⚠️ Completed with ${{ steps.error_summary.outputs.total_errors }} error(s)" >> $GITHUB_STEP_SUMMARY
echo "- Release script did not run (skipped or failed before execution)." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Release Processing" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.release.outputs.version_changed }}" == "true" ]; then
echo "✅ **Version was incremented** and release created" >> $GITHUB_STEP_SUMMARY
else
echo " **Version was not changed** - no release created" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Changelog Processing" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.changelog_check.outputs.has_unreleased_content }}" == "true" ]; then
echo "✅ **UNRELEASED_CHANGELOG.md** had content and was processed" >> $GITHUB_STEP_SUMMARY
echo "- Content moved to main changelog" >> $GITHUB_STEP_SUMMARY
echo "- UNRELEASED_CHANGELOG.md reset with template" >> $GITHUB_STEP_SUMMARY
else
echo " **UNRELEASED_CHANGELOG.md** had no content to process" >> $GITHUB_STEP_SUMMARY
fi
# Error reporting section
if [ "${{ steps.error_summary.outputs.total_errors }}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "## ⚠️ Error Report" >> $GITHUB_STEP_SUMMARY
echo "**Total Error Categories:** ${{ steps.error_summary.outputs.total_errors }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.error_summary.outputs.error_summary }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🔧 Troubleshooting Tips" >> $GITHUB_STEP_SUMMARY
echo "- Check the individual step logs above for detailed error messages" >> $GITHUB_STEP_SUMMARY
echo "- Verify GitHub token permissions (contents: write, pull-requests: read)" >> $GITHUB_STEP_SUMMARY
echo "- Ensure UNRELEASED_CHANGELOG.md follows the expected format" >> $GITHUB_STEP_SUMMARY
echo "- Check for network connectivity issues if git/GitHub operations failed" >> $GITHUB_STEP_SUMMARY
echo "- Re-run the workflow with 'force_release=true' if needed" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Release Notes Preview" >> $GITHUB_STEP_SUMMARY
if [ -n "${{ steps.read_notes.outputs.release_notes }}" ]; then
echo "${{ steps.read_notes.outputs.release_notes }}" >> $GITHUB_STEP_SUMMARY
else
echo "No specific release notes generated" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "*Generated by automated nightly release workflow with enhanced error handling and changelog integration*" >> $GITHUB_STEP_SUMMARY
# Set final workflow status
if [ "${{ steps.error_summary.outputs.overall_success }}" != "true" ]; then
echo "⚠️ Workflow completed with errors. Check the summary above for details."
exit 1
fi

View file

@ -27,6 +27,7 @@ jobs:
with:
files: |
v3/internal/runtime/desktop/@wailsio/runtime/package.json
v3/internal/runtime/desktop/@wailsio/runtime/package-lock.json
- name: Detect committed source changes
if: >-
@ -49,18 +50,23 @@ jobs:
!failure() && !cancelled()
&& (github.event_name == 'workflow_dispatch' || needs.detect.outputs.changed == 'true')
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
pull-requests: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: 'v3-alpha'
ssh-key: ${{ secrets.DEPLOY_KEY }}
token: ${{ secrets.WAILS_REPO_TOKEN || github.token }}
- name: Configure git
run: |
git config --local user.email "github-actions@github.com"
git config --local user.name "GitHub Actions"
git config --global user.email "github-actions@github.com"
git config --global user.name "GitHub Actions"
git config --global url."https://x-access-token:${{ secrets.WAILS_REPO_TOKEN || github.token }}@github.com/".insteadOf "https://github.com/"
- name: Install Task
uses: arduino/setup-task@v2
@ -104,7 +110,6 @@ jobs:
git add .
git commit -m "[skip ci] Publish @wailsio/runtime ${{ steps.bump-version.outputs.version }}"
git push
fi
- name: Publish npm package
uses: JS-DevTools/npm-publish@v3

View file

@ -2,9 +2,12 @@ name: Deploy to GitHub Pages
on:
# Trigger the workflow every time you push to the `main` branch
# Using a different branch name? Replace `main` with your branchs name
# Using a different branch name? Replace `main` with your branch's name
push:
branches: [v3-alpha]
paths:
- 'docs/**'
- '.github/workflows/v3-docs.yml'
# Allows you to run this workflow manually from the Actions tab on GitHub.
workflow_dispatch:
@ -21,6 +24,14 @@ jobs:
steps:
- name: Checkout your repository using git
uses: actions/checkout@v4
- name: Install D2
run: |
curl -fsSL https://d2lang.com/install.sh > install.sh
chmod +x install.sh
./install.sh
sudo cp ~/.local/bin/d2 /usr/local/bin/d2
d2 --version
rm install.sh
- name: Install, build, and upload your site output
uses: withastro/action@v2
with:
@ -37,6 +48,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
uses: actions/deploy-pages@v4

View file

@ -1,17 +1,22 @@
# Starlight Starter Kit: Basics
# Wails v3 Documentation
[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)
```sh
npm create astro@latest -- --template starlight
```
World-class documentation for Wails v3, redesigned following Netflix documentation principles.
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
## 📚 Documentation Redesign (2025-10-01)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
This documentation has been completely redesigned to follow the **Netflix approach** to developer documentation:
- **Problem-first framing** - Start with why, not what
- **Progressive disclosure** - Multiple entry points for different skill levels
- **Real production examples** - No toy code
- **Story-Code-Context pattern** - Why → How → When
- **Scannable content** - Clear structure, visual aids
**Status:** Foundation complete (~20%), ready for content migration
See [IMPLEMENTATION_SUMMARY.md](./IMPLEMENTATION_SUMMARY.md) for full details.
## 🚀 Project Structure

View file

@ -2,29 +2,37 @@
version: '3'
vars:
# Change this to switch package managers: bun, npm, pnpm, yarn
PKG_MANAGER: bun
tasks:
setup:
summary: Setup the project
summary: Setup the project (including D2 diagram tool)
preconditions:
- sh: npm --version
msg: "Looks like npm isn't installed."
- sh: '{{.PKG_MANAGER}} --version'
msg: "Looks like {{.PKG_MANAGER}} isn't installed."
- sh: 'go version'
msg: "Go is not installed. Install from https://go.dev/dl/"
cmds:
- npm install
- '{{.PKG_MANAGER}} install'
- go install oss.terrastruct.com/d2@latest
- echo "✓ Setup complete. D2 installed to $(go env GOPATH)/bin/d2"
dev:
summary: Run the dev server
preconditions:
- sh: npm --version
msg: "Looks like npm isn't installed."
- sh: '{{.PKG_MANAGER}} --version'
msg: "Looks like {{.PKG_MANAGER}} isn't installed."
cmds:
- npm run dev
- '{{.PKG_MANAGER}} run dev'
build:
summary: Build the docs
preconditions:
- sh: npm --version
msg: "Looks like npm isn't installed."
- sh: '{{.PKG_MANAGER}} --version'
msg: "Looks like {{.PKG_MANAGER}} isn't installed."
cmds:
- npm run build
- '{{.PKG_MANAGER}} run build'

View file

@ -6,10 +6,11 @@ import starlightLinksValidator from "starlight-links-validator";
import starlightImageZoom from "starlight-image-zoom";
import starlightBlog from "starlight-blog";
import { authors } from "./src/content/authors";
import d2 from 'astro-d2';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
// TODO: update this
site: "https://wails.io",
trailingSlash: "ignore",
compressHTML: true,
@ -17,59 +18,54 @@ export default defineConfig({
build: { format: "directory" },
devToolbar: { enabled: true },
integrations: [
react(),
d2(),
sitemap(),
starlight({
title: "",
// If a title is added, also update the delimiter.
titleDelimiter: "",
logo: {
dark: "./src/assets/wails-logo-horizontal-dark.svg",
light: "./src/assets/wails-logo-horizontal-light.svg",
},
favicon: "./public/favicon.svg",
description: "Build desktop applications using Go & Web Technologies.",
description: "Build beautiful desktop applications using Go and modern web technologies.",
pagefind: true,
customCss: ["./src/stylesheets/extra.css"],
lastUpdated: true, // Note, this needs git clone with fetch depth 0 to work
lastUpdated: true,
pagination: true,
editLink: {
// TODO: update this
baseUrl: "https://github.com/wailsapp/wails/edit/v3-alpha/docs",
},
social: {
github: "https://github.com/wailsapp/wails",
discord: "https://discord.gg/JDdSxwjhGf",
"x.com": "https://x.com/wailsapp",
},
social: [
{ icon: 'github', label: 'GitHub', href: 'https://github.com/wailsapp/wails' },
{ icon: 'discord', label: 'Discord', href: 'https://discord.gg/JDdSxwjhGf' },
{ icon: 'x.com', label: 'X', href: 'https://x.com/wailsapp' },
],
head: [
{
tag: 'script',
content: `
document.addEventListener('DOMContentLoaded', () => {
const socialLinks = document.querySelector('.social-icons');
if (socialLinks) {
const sponsorLink = document.createElement('a');
sponsorLink.href = 'https://github.com/sponsors/leaanthony';
sponsorLink.className = 'sl-flex';
sponsorLink.title = 'Sponsor';
sponsorLink.innerHTML = '<span class="sr-only">Sponsor</span><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="#ef4444" stroke="none"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>';
socialLinks.appendChild(sponsorLink);
}
});
`,
},
],
defaultLocale: "root",
locales: {
root: { label: "English", lang: "en", dir: "ltr" },
// Example of how a new language is added.
// After this, you create a directory named after the language inside content/docs/
// with the same structure as the root language
// eg content/docs/gr/changelog.md or content/docs/gr/api/application.mdx
// gr: { label: "Greek", lang: "el", dir: "ltr" },
},
plugins: [
// https://starlight-links-validator.vercel.app/configuration/
// starlightLinksValidator({
// exclude: [
// // TODO: Fix these links in the blog/wails-v2-released file
// // "/docs/reference/options#theme",
// // "/docs/reference/options#customtheme",
// // "/docs/guides/application-development#application-menu",
// // "/docs/reference/runtime/dialog",
// // "/docs/reference/options#windowistranslucent",
// // "/docs/reference/options#windowistranslucent-1",
// // "/docs/guides/windows-installer",
// // "/docs/reference/runtime/intro",
// // "/docs/guides/obfuscated",
// // "/docs/howdoesitwork#calling-bound-go-methods",
// ],
// }),
// https://starlight-image-zoom.vercel.app/configuration/
starlightImageZoom(),
// https://starlight-blog-docs.vercel.app/configuration
starlightBlog({
title: "Wails Blog",
authors: authors,
@ -77,36 +73,239 @@ export default defineConfig({
],
sidebar: [
{ label: "Home", link: "/" },
// Progressive Onboarding - Netflix Principle: Start with the problem
{ label: "Why Wails?", link: "/quick-start/why-wails" },
{
label: "Getting Started",
autogenerate: { directory: "getting-started", collapsed: false },
label: "Quick Start",
collapsed: false,
items: [
{ label: "Installation", link: "/quick-start/installation" },
{ label: "Your First App", link: "/quick-start/first-app" },
{ label: "Next Steps", link: "/quick-start/next-steps" },
],
},
// Tutorials
{
label: "Tutorials",
collapsed: true,
autogenerate: { directory: "tutorials", collapsed: true },
autogenerate: { directory: "tutorials" },
},
// Core Concepts
{
label: "What's New",
link: "/whats-new",
badge: { text: "New", variant: "tip" },
},
{ label: "v3 Alpha Feedback", link: "/feedback" },
{
label: "Learn",
label: "Core Concepts",
collapsed: true,
autogenerate: { directory: "learn", collapsed: true },
items: [
{ label: "How Wails Works", link: "/concepts/architecture" },
{ label: "Manager API", link: "/concepts/manager-api" },
{ label: "Application Lifecycle", link: "/concepts/lifecycle" },
{ label: "Go-Frontend Bridge", link: "/concepts/bridge" },
{ label: "Build System", link: "/concepts/build-system" },
],
},
{
label: "Features",
collapsed: true,
items: [
{
label: "Windows",
collapsed: true,
items: [
{ label: "Window Basics", link: "/features/windows/basics" },
{ label: "Window Options", link: "/features/windows/options" },
{ label: "Multiple Windows", link: "/features/windows/multiple" },
{ label: "Frameless Windows", link: "/features/windows/frameless" },
{ label: "Window Events", link: "/features/windows/events" },
],
},
{
label: "Menus",
collapsed: true,
items: [
{ label: "Application Menus", link: "/features/menus/application" },
{ label: "Context Menus", link: "/features/menus/context" },
{ label: "System Tray Menus", link: "/features/menus/systray" },
{ label: "Menu Reference", link: "/features/menus/reference" },
],
},
{
label: "Bindings & Services",
collapsed: true,
items: [
{ label: "Method Binding", link: "/features/bindings/methods" },
{ label: "Services", link: "/features/bindings/services" },
{ label: "Advanced Binding", link: "/features/bindings/advanced" },
{ label: "Best Practices", link: "/features/bindings/best-practices" },
],
},
{
label: "Events",
collapsed: true,
items: [
{ label: "Event System", link: "/features/events/system" },
{ label: "Application Events", link: "/features/events/application" },
{ label: "Window Events", link: "/features/events/window" },
{ label: "Custom Events", link: "/features/events/custom" },
],
},
{
label: "Dialogs",
collapsed: true,
items: [
{ label: "File Dialogs", link: "/features/dialogs/file" },
{ label: "Message Dialogs", link: "/features/dialogs/message" },
{ label: "Custom Dialogs", link: "/features/dialogs/custom" },
],
},
{
label: "Clipboard",
collapsed: true,
autogenerate: { directory: "features/clipboard" },
},
{
label: "Browser",
collapsed: true,
autogenerate: { directory: "features/browser" },
},
{ label: "Drag & Drop", link: "/features/drag-drop" },
{
label: "Keyboard",
collapsed: true,
autogenerate: { directory: "features/keyboard" },
},
{
label: "Notifications",
collapsed: true,
autogenerate: { directory: "features/notifications" },
},
{
label: "Screens",
collapsed: true,
autogenerate: { directory: "features/screens" },
},
{
label: "Environment",
collapsed: true,
autogenerate: { directory: "features/environment" },
},
{
label: "Platform-Specific",
collapsed: true,
autogenerate: { directory: "features/platform" },
},
],
},
// Guides - Task-oriented patterns (Netflix: When to use it, when not to use it)
{
label: "Guides",
collapsed: true,
autogenerate: { directory: "guides", collapsed: true },
items: [
{
label: "Development",
collapsed: true,
items: [
{ label: "Project Structure", link: "/guides/dev/project-structure" },
{ label: "Development Workflow", link: "/guides/dev/workflow" },
{ label: "Debugging", link: "/guides/dev/debugging" },
{ label: "Testing", link: "/guides/dev/testing" },
],
},
{
label: "Building & Packaging",
collapsed: true,
items: [
{ label: "Building Applications", link: "/guides/build/building" },
{ label: "Build Customization", link: "/guides/build/customization" },
{ label: "Cross-Platform Builds", link: "/guides/build/cross-platform" },
{ label: "Code Signing", link: "/guides/build/signing" },
{ label: "Windows Packaging", link: "/guides/build/windows" },
{ label: "macOS Packaging", link: "/guides/build/macos" },
{ label: "Linux Packaging", link: "/guides/build/linux" },
{ label: "MSIX Packaging", link: "/guides/build/msix" },
],
},
{
label: "Distribution",
collapsed: true,
items: [
{ label: "Auto-Updates", link: "/guides/distribution/auto-updates" },
{ label: "File Associations", link: "/guides/distribution/file-associations" },
{ label: "Custom Protocols", link: "/guides/distribution/custom-protocols" },
{ label: "Single Instance", link: "/guides/distribution/single-instance" },
],
},
{
label: "Integration Patterns",
collapsed: true,
items: [
{ label: "Using Gin Router", link: "/guides/patterns/gin-routing" },
{ label: "Gin Services", link: "/guides/patterns/gin-services" },
{ label: "Database Integration", link: "/guides/patterns/database" },
{ label: "REST APIs", link: "/guides/patterns/rest-api" },
],
},
{
label: "Advanced Topics",
collapsed: true,
items: [
{ label: "Custom Templates", link: "/guides/advanced/custom-templates" },
{ label: "WML (Wails Markup)", link: "/guides/advanced/wml" },
{ label: "Panic Handling", link: "/guides/advanced/panic-handling" },
{ label: "Security Best Practices", link: "/guides/advanced/security" },
],
},
],
},
// {
// label: "API",
// collapsed: true,
// autogenerate: { directory: "api", collapsed: true },
// },
// Reference - Comprehensive API docs (Netflix: Complete technical reference)
{
label: "API Reference",
collapsed: true,
items: [
{ label: "Overview", link: "/reference/overview" },
{ label: "Application", link: "/reference/application" },
{ label: "Window", link: "/reference/window" },
{ label: "Menu", link: "/reference/menu" },
{ label: "Events", link: "/reference/events" },
{ label: "Dialogs", link: "/reference/dialogs" },
{ label: "Frontend Runtime", link: "/reference/frontend-runtime" },
{ label: "CLI", link: "/reference/cli" },
],
},
// Contributing
{
label: "Contributing",
collapsed: true,
items: [
{ label: "Getting Started", link: "/contributing/getting-started" },
{ label: "Development Setup", link: "/contributing/setup" },
{ label: "Coding Standards", link: "/contributing/standards" },
],
},
// Migration & Troubleshooting
{
label: "Migration",
collapsed: true,
items: [
{ label: "From v2 to v3", link: "/migration/v2-to-v3" },
{ label: "From Electron", link: "/migration/from-electron" },
],
},
{
label: "Troubleshooting",
collapsed: true,
autogenerate: { directory: "troubleshooting" },
},
// Community & Resources
{
label: "Community",
collapsed: true,
@ -115,18 +314,22 @@ export default defineConfig({
{ label: "Templates", link: "/community/templates" },
{
label: "Showcase",
autogenerate: {
directory: "community/showcase",
collapsed: true,
},
collapsed: true,
items: [
{ label: "Overview", link: "/community/showcase" },
{
label: "Applications",
autogenerate: {
directory: "community/showcase",
collapsed: true,
},
},
],
},
],
},
// {
// label: "Development",
// collapsed: true,
// autogenerate: { directory: "development", collapsed: true },
// },
{ label: "What's New", link: "/whats-new" },
{ label: "Status", link: "/status" },
{ label: "Changelog", link: "/changelog" },
{
@ -134,11 +337,7 @@ export default defineConfig({
link: "https://github.com/sponsors/leaanthony",
badge: { text: "❤️" },
},
{
label: "Credits",
link: "/credits",
badge: { text: "👑" },
},
{ label: "Credits", link: "/credits" },
],
}),
],

6052
docs/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,22 +10,22 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "0.9.4",
"@astrojs/react": "4.1.0",
"@astrojs/starlight": "0.29.2",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.2",
"astro": "4.16.17",
"framer-motion": "11.14.4",
"mermaid": "^10.9.3",
"motion": "11.14.4",
"react": "19.0.0",
"react-dom": "19.0.0",
"sharp": "0.33.5",
"starlight-blog": "0.15.0",
"starlight-image-zoom": "0.9.0",
"starlight-links-validator": "0.13.4",
"starlight-showcases": "0.2.0",
"typescript": "5.7.2"
"@astrojs/check": "^0.9.4",
"@astrojs/react": "^4.1.0",
"@astrojs/starlight": "0.36.2",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"astro": "^5.0.0",
"astro-d2": "^0.5.0",
"framer-motion": "^11.14.4",
"motion": "^11.14.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sharp": "^0.33.5",
"starlight-blog": "0.25.1",
"starlight-image-zoom": "^0.9.0",
"starlight-links-validator": "^0.13.4",
"starlight-showcases": "^0.2.0",
"typescript": "^5.7.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

View file

@ -1,59 +0,0 @@
---
export interface Props {
title?: string;
}
const { title = "" } = Astro.props;
---
<script>
import mermaid from "mermaid";
// Postpone mermaid initialization
mermaid.initialize({ startOnLoad: false });
function extractMermaidCode() {
// Find all mermaid components
const mermaidElements = document.querySelectorAll("figure.expandable-diagram");
mermaidElements.forEach((element) => {
// Find the code content in the details section
const codeElement = element.querySelector("details pre code");
if (!codeElement) return;
// Extract the text content
let code = codeElement.textContent || "";
// Clean up the code
code = code.trim();
// Construct the `pre` element for the diagram code
const preElement = document.createElement("pre");
preElement.className = "mermaid not-prose";
preElement.innerHTML = code;
// Find the diagram content container and override its content
const diagramContainer = element.querySelector(".diagram-content");
if (diagramContainer) {
diagramContainer.innerHTML = "";
diagramContainer.appendChild(preElement);
}
});
}
// Wait for the DOM to be fully loaded
document.addEventListener("DOMContentLoaded", async () => {
extractMermaidCode();
mermaid.initialize({ startOnLoad: true });
});
</script>
<figure class="expandable-diagram">
<figcaption>{title}</figcaption>
<div class="diagram-content">Loading diagram...</div>
<details>
<summary>Source</summary>
<pre><code><slot /></code></pre>
</details>
</figure>

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,25 @@
---
title: My Project
draft: true
---
<!-- This is an image -->
<!--
TEMPLATE FOR SHOWCASE ENTRIES
1. Replace "My Project" with your project name
2. Add your screenshot to: src/assets/showcase-images/your-project.webp
3. Update the image path below
4. Add a description of your project
5. Add a link to your project website/repo
6. Remove the "draft: true" line when ready to publish
-->
![My Project](../../../../assets/showcase-images/my-project.webp)
![My Project Screenshot](../../../../assets/showcase-images/your-project.webp)
<!-- Add some content here -->
<!-- Add a description of your project here -->
<!-- This is a link -->
Your project description goes here. Explain what it does, what makes it special, and why you built it with Wails.
[My Project](https://my-project.com)
<!-- Add a link to your project -->
[Visit Project Website](https://your-project.com) | [View on GitHub](https://github.com/yourusername/your-project)

View file

@ -0,0 +1,655 @@
---
title: How Wails Works
description: Understanding the Wails architecture and how it achieves native performance
sidebar:
order: 1
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
Wails is a framework for building desktop applications using **Go for the backend** and **web technologies for the frontend**. But unlike Electron, Wails doesn't bundle a browser—it uses the **operating system's native WebView**.
```d2
direction: right
User: "User" {
shape: person
style.fill: "#3B82F6"
}
Application: "Your Wails Application" {
Frontend: "Frontend\n(HTML/CSS/JS)" {
shape: rectangle
style.fill: "#8B5CF6"
}
Runtime: "Wails Runtime" {
Bridge: "Message Bridge" {
shape: diamond
style.fill: "#10B981"
}
Bindings: "Type-Safe Bindings" {
shape: rectangle
style.fill: "#10B981"
}
}
Backend: "Go Backend" {
Services: "Your Services" {
shape: rectangle
style.fill: "#00ADD8"
}
NativeAPIs: "OS APIs" {
shape: rectangle
style.fill: "#00ADD8"
}
}
}
OS: "Operating System" {
WebView: "Native WebView\n(WebKit/WebView2/WebKitGTK)" {
shape: rectangle
style.fill: "#6B7280"
}
SystemAPIs: "System APIs\n(Windows/macOS/Linux)" {
shape: rectangle
style.fill: "#6B7280"
}
}
User -> Application.Frontend: "Interacts with UI"
Application.Frontend <-> Application.Runtime.Bridge: "JSON messages"
Application.Runtime.Bridge <-> Application.Backend.Services: "Direct function calls"
Application.Runtime.Bindings -> Application.Frontend: "TypeScript definitions"
Application.Frontend -> OS.WebView: "Renders in"
Application.Backend.NativeAPIs -> OS.SystemAPIs: "Native calls"
```
**Key differences from Electron:**
| Aspect | Wails | Electron |
|--------|-------|----------|
| **Browser** | OS-provided WebView | Bundled Chromium (~100MB) |
| **Backend** | Go (compiled) | Node.js (interpreted) |
| **Communication** | In-memory bridge | IPC (inter-process) |
| **Bundle Size** | ~15MB | ~150MB |
| **Memory** | ~10MB | ~100MB+ |
| **Startup** | &lt;0.5s | 2-3s |
## Core Components
### 1. Native WebView
Wails uses the operating system's built-in web rendering engine:
<Tabs syncKey="platform">
<TabItem label="Windows" icon="seti:windows">
**WebView2** (Microsoft Edge WebView2)
- Based on Chromium (same as Edge browser)
- Pre-installed on Windows 10/11
- Automatic updates via Windows Update
- Full modern web standards support
</TabItem>
<TabItem label="macOS" icon="apple">
**WebKit** (Safari's rendering engine)
- Built into macOS
- Same engine as Safari browser
- Excellent performance and battery life
- Full modern web standards support
</TabItem>
<TabItem label="Linux" icon="linux">
**WebKitGTK** (GTK port of WebKit)
- Installed via package manager
- Same engine as GNOME Web (Epiphany)
- Good standards support
- Lightweight and performant
</TabItem>
</Tabs>
**Why this matters:**
- **No bundled browser** → Smaller app size
- **OS-native** → Better integration and performance
- **Auto-updates** → Security patches from OS updates
- **Familiar rendering** → Same as system browser
### 2. The Wails Bridge
The bridge is the heart of Wails—it enables **direct communication** between Go and JavaScript.
```d2
direction: down
Frontend: "Frontend (JavaScript)" {
shape: rectangle
style.fill: "#8B5CF6"
}
Bridge: "Wails Bridge" {
Encoder: "JSON Encoder" {
shape: rectangle
}
Router: "Method Router" {
shape: diamond
style.fill: "#10B981"
}
Decoder: "JSON Decoder" {
shape: rectangle
}
}
Backend: "Backend (Go)" {
Services: "Registered Services" {
shape: rectangle
style.fill: "#00ADD8"
}
}
Frontend -> Bridge.Encoder: "1. Call Go method\nGreet('Alice')"
Bridge.Encoder -> Bridge.Router: "2. Encode to JSON\n{method: 'Greet', args: ['Alice']}"
Bridge.Router -> Backend.Services: "3. Route to service\nGreetService.Greet('Alice')"
Backend.Services -> Bridge.Decoder: "4. Return result\n'Hello, Alice!'"
Bridge.Decoder -> Frontend: "5. Decode to JS\nPromise resolves"
```
**How it works:**
1. **Frontend calls a Go method** (via auto-generated binding)
2. **Bridge encodes the call** to JSON (method name + arguments)
3. **Router finds the Go method** in registered services
4. **Go method executes** and returns a value
5. **Bridge decodes the result** and sends back to frontend
6. **Promise resolves** in JavaScript with the result
**Performance characteristics:**
- **In-memory**: No network overhead, no HTTP
- **Zero-copy** where possible (for large data)
- **Async by default**: Non-blocking on both sides
- **Type-safe**: TypeScript definitions auto-generated
### 3. Service System
Services are the recommended way to expose Go functionality to the frontend.
```go
// Define a service (just a regular Go struct)
type GreetService struct {
prefix string
}
// Methods with exported names are automatically available
func (g *GreetService) Greet(name string) string {
return g.prefix + name + "!"
}
func (g *GreetService) GetTime() time.Time {
return time.Now()
}
// Register the service
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&GreetService{prefix: "Hello, "}),
},
})
```
**Service discovery:**
- Wails **scans your struct** at startup
- **Exported methods** become callable from frontend
- **Type information** is extracted for TypeScript bindings
- **Error handling** is automatic (Go errors → JS exceptions)
**Generated TypeScript binding:**
```typescript
// Auto-generated in frontend/bindings/GreetService.ts
export function Greet(name: string): Promise<string>
export function GetTime(): Promise<Date>
```
**Why services?**
- **Type-safe**: Full TypeScript support
- **Auto-discovery**: No manual registration of methods
- **Organised**: Group related functionality
- **Testable**: Services are just Go structs
[Learn more about services →](/features/bindings/services)
### 4. Event System
Events enable **pub/sub communication** between components.
```d2
direction: right
GoService: "Go Service" {
shape: rectangle
style.fill: "#00ADD8"
}
EventBus: "Event Bus" {
shape: cylinder
style.fill: "#10B981"
}
Frontend1: "Window 1" {
shape: rectangle
style.fill: "#8B5CF6"
}
Frontend2: "Window 2" {
shape: rectangle
style.fill: "#8B5CF6"
}
GoService -> EventBus: "Emit('data-updated', data)"
EventBus -> Frontend1: "Notify subscribers"
EventBus -> Frontend2: "Notify subscribers"
Frontend1 -> EventBus: "On('data-updated', handler)"
Frontend2 -> EventBus: "On('data-updated', handler)"
```
**Use cases:**
- **Window communication**: One window notifies others
- **Background tasks**: Go service notifies UI of progress
- **State synchronisation**: Keep multiple windows in sync
- **Loose coupling**: Components don't need direct references
**Example:**
```go
// Go: Emit an event
app.EmitEvent("user-logged-in", user)
```
```javascript
// JavaScript: Listen for event
import { On } from '@wailsio/runtime'
On('user-logged-in', (user) => {
console.log('User logged in:', user)
})
```
[Learn more about events →](/features/events/system)
## Application Lifecycle
Understanding the lifecycle helps you know when to initialise resources and clean up.
```d2
direction: down
Start: "Application Start" {
shape: oval
style.fill: "#10B981"
}
Init: "Initialisation" {
Create: "Create Application" {
shape: rectangle
}
Register: "Register Services" {
shape: rectangle
}
Setup: "Setup Windows/Menus" {
shape: rectangle
}
}
Run: "Event Loop" {
Events: "Process Events" {
shape: rectangle
}
Messages: "Handle Messages" {
shape: rectangle
}
Render: "Update UI" {
shape: rectangle
}
}
Shutdown: "Shutdown" {
Cleanup: "Cleanup Resources" {
shape: rectangle
}
Save: "Save State" {
shape: rectangle
}
}
End: "Application End" {
shape: oval
style.fill: "#EF4444"
}
Start -> Init.Create
Init.Create -> Init.Register
Init.Register -> Init.Setup
Init.Setup -> Run.Events
Run.Events -> Run.Messages
Run.Messages -> Run.Render
Run.Render -> Run.Events: "Loop"
Run.Events -> Shutdown.Cleanup: "Quit signal"
Shutdown.Cleanup -> Shutdown.Save
Shutdown.Save -> End
```
**Lifecycle hooks:**
```go
app := application.New(application.Options{
Name: "My App",
// Called before windows are created
OnStartup: func(ctx context.Context) {
// Initialise database, load config, etc.
},
// Called when app is about to quit
OnShutdown: func() {
// Save state, close connections, etc.
},
})
```
[Learn more about lifecycle →](/concepts/lifecycle)
## Build Process
Understanding how Wails builds your application:
```d2
direction: down
Source: "Source Code" {
Go: "Go Code\n(main.go, services)" {
shape: rectangle
style.fill: "#00ADD8"
}
Frontend: "Frontend Code\n(HTML/CSS/JS)" {
shape: rectangle
style.fill: "#8B5CF6"
}
}
Build: "Build Process" {
AnalyseGo: "Analyse Go Code" {
shape: rectangle
}
GenerateBindings: "Generate Bindings" {
shape: rectangle
}
BuildFrontend: "Build Frontend" {
shape: rectangle
}
CompileGo: "Compile Go" {
shape: rectangle
}
Embed: "Embed Assets" {
shape: rectangle
}
}
Output: "Output" {
Binary: "Native Binary\n(myapp.exe/.app)" {
shape: rectangle
style.fill: "#10B981"
}
}
Source.Go -> Build.AnalyseGo
Build.AnalyseGo -> Build.GenerateBindings: "Extract types"
Build.GenerateBindings -> Source.Frontend: "TypeScript bindings"
Source.Frontend -> Build.BuildFrontend: "Compile (Vite/webpack)"
Build.BuildFrontend -> Build.Embed: "Bundled assets"
Source.Go -> Build.CompileGo
Build.CompileGo -> Build.Embed
Build.Embed -> Output.Binary
```
**Build steps:**
1. **Analyse Go code**
- Scan services for exported methods
- Extract parameter and return types
- Generate method signatures
2. **Generate TypeScript bindings**
- Create `.ts` files for each service
- Include full type definitions
- Add JSDoc comments
3. **Build frontend**
- Run your bundler (Vite, webpack, etc.)
- Minify and optimise
- Output to `frontend/dist/`
4. **Compile Go**
- Compile with optimisations (`-ldflags="-s -w"`)
- Include build metadata
- Platform-specific compilation
5. **Embed assets**
- Embed frontend files into Go binary
- Compress assets
- Create single executable
**Result:** A single native executable with everything embedded.
[Learn more about building →](/guides/build/building)
## Development vs Production
Wails behaves differently in development and production:
<Tabs syncKey="mode">
<TabItem label="Development (wails3 dev)" icon="seti:config">
**Characteristics:**
- **Hot reload**: Frontend changes reload instantly
- **Source maps**: Debug with original source
- **DevTools**: Browser DevTools available
- **Logging**: Verbose logging enabled
- **External frontend**: Served from dev server (Vite)
**How it works:**
```d2
direction: right
WailsApp: "Wails App" {
shape: rectangle
style.fill: "#00ADD8"
}
DevServer: "Vite Dev Server\n(localhost:5173)" {
shape: rectangle
style.fill: "#8B5CF6"
}
WebView: "WebView" {
shape: rectangle
style.fill: "#6B7280"
}
WailsApp -> DevServer: "Proxy requests"
DevServer -> WebView: "Serve with HMR"
WebView -> WailsApp: "Call Go methods"
```
**Benefits:**
- Instant feedback on changes
- Full debugging capabilities
- Faster iteration
</TabItem>
<TabItem label="Production (wails3 build)" icon="rocket">
**Characteristics:**
- **Embedded assets**: Frontend built into binary
- **Optimised**: Minified, compressed
- **No DevTools**: Disabled by default
- **Minimal logging**: Errors only
- **Single file**: Everything in one executable
**How it works:**
```d2
direction: right
Binary: "Single Binary\n(myapp.exe)" {
GoCode: "Compiled Go" {
shape: rectangle
style.fill: "#00ADD8"
}
Assets: "Embedded Assets\n(HTML/CSS/JS)" {
shape: rectangle
style.fill: "#8B5CF6"
}
}
WebView: "WebView" {
shape: rectangle
style.fill: "#6B7280"
}
Binary.Assets -> WebView: "Serve from memory"
WebView -> Binary.GoCode: "Call Go methods"
```
**Benefits:**
- Single file distribution
- Smaller size (minified)
- Better performance
- No external dependencies
</TabItem>
</Tabs>
## Memory Model
Understanding memory usage helps you build efficient applications.
{/* VISUAL PLACEHOLDER: Memory Diagram
Description: Memory layout diagram showing:
1. Go Heap (services, application state)
2. WebView Memory (DOM, JavaScript heap)
3. Shared Memory (bridge communication)
4. Arrows showing data flow between regions
5. Annotations for zero-copy optimisations
Style: Technical diagram with memory regions as boxes, clear labels, size indicators
*/}
**Memory regions:**
1. **Go Heap**
- Your services and application state
- Managed by Go garbage collector
- Typically 5-10MB for simple apps
2. **WebView Memory**
- DOM, JavaScript heap, CSS
- Managed by WebView's engine
- Typically 10-20MB for simple apps
3. **Bridge Memory**
- Message buffers for communication
- Minimal overhead (\<1MB)
- Zero-copy for large data where possible
**Optimisation tips:**
- **Avoid large data transfers**: Pass IDs, fetch details on demand
- **Use events for updates**: Don't poll from frontend
- **Stream large files**: Don't load entirely into memory
- **Clean up listeners**: Remove event listeners when done
[Learn more about performance →](/guides/advanced/performance)
## Security Model
Wails provides a secure-by-default architecture:
```d2
direction: down
Frontend: "Frontend (Untrusted)" {
shape: rectangle
style.fill: "#EF4444"
}
Bridge: "Wails Bridge (Validation)" {
shape: diamond
style.fill: "#F59E0B"
}
Backend: "Backend (Trusted)" {
shape: rectangle
style.fill: "#10B981"
}
Frontend -> Bridge: "Call method"
Bridge -> Bridge: "Validate:\n- Method exists?\n- Types correct?\n- Access allowed?"
Bridge -> Backend: "Execute if valid"
Backend -> Bridge: "Return result"
Bridge -> Frontend: "Send response"
```
**Security features:**
1. **Method whitelisting**
- Only exported methods are callable
- Private methods are inaccessible
- Explicit service registration required
2. **Type validation**
- Arguments checked against Go types
- Invalid types rejected
- Prevents injection attacks
3. **No eval()**
- Frontend can't execute arbitrary Go code
- Only predefined methods callable
- No dynamic code execution
4. **Context isolation**
- Each window has its own context
- Services can check caller context
- Permissions per window possible
**Best practices:**
- **Validate user input** in Go (don't trust frontend)
- **Use context** for authentication/authorisation
- **Sanitise file paths** before file operations
- **Rate limit** expensive operations
[Learn more about security →](/guides/advanced/security)
## Next Steps
**Application Lifecycle** - Understand startup, shutdown, and lifecycle hooks
[Learn More →](/concepts/lifecycle)
**Go-Frontend Bridge** - Deep dive into how the bridge works
[Learn More →](/concepts/bridge)
**Build System** - Understand how Wails builds your application
[Learn More →](/concepts/build-system)
**Start Building** - Apply what you've learned in a tutorial
[Tutorials →](/tutorials/03-notes-vanilla)
---
**Questions about architecture?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [API reference](/reference/overview).

View file

@ -0,0 +1,700 @@
---
title: Go-Frontend Bridge
description: Deep dive into how Wails enables direct communication between Go and JavaScript
sidebar:
order: 3
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
## Direct Go-JavaScript Communication
Wails provides a **direct, in-memory bridge** between Go and JavaScript, enabling seamless communication without HTTP overhead, process boundaries, or serialisation bottlenecks.
## The Big Picture
```d2
direction: right
Frontend: "Frontend (JavaScript)" {
UI: "React/Vue/Vanilla" {
shape: rectangle
style.fill: "#8B5CF6"
}
Bindings: "Auto-Generated Bindings" {
shape: rectangle
style.fill: "#A78BFA"
}
}
Bridge: "Wails Bridge" {
Encoder: "JSON Encoder" {
shape: rectangle
style.fill: "#10B981"
}
Router: "Method Router" {
shape: diamond
style.fill: "#10B981"
}
Decoder: "JSON Decoder" {
shape: rectangle
style.fill: "#10B981"
}
TypeGen: "Type Generator" {
shape: rectangle
style.fill: "#10B981"
}
}
Backend: "Backend (Go)" {
Services: "Your Services" {
shape: rectangle
style.fill: "#00ADD8"
}
Registry: "Service Registry" {
shape: rectangle
style.fill: "#00ADD8"
}
}
Frontend.UI -> Frontend.Bindings: "import { Method }"
Frontend.Bindings -> Bridge.Encoder: "Call Method('arg')"
Bridge.Encoder -> Bridge.Router: "Encode to JSON"
Bridge.Router -> Backend.Registry: "Find service"
Backend.Registry -> Backend.Services: "Invoke method"
Backend.Services -> Bridge.Decoder: "Return result"
Bridge.Decoder -> Frontend.Bindings: "Decode to JS"
Frontend.Bindings -> Frontend.UI: "Promise resolves"
Bridge.TypeGen -> Frontend.Bindings: "Generate types"
```
**Key insight:** No HTTP, no IPC, no process boundaries. Just **direct function calls** with **type safety**.
## How It Works: Step by Step
### 1. Service Registration (Startup)
When your application starts, Wails scans your services:
```go
type GreetService struct {
prefix string
}
func (g *GreetService) Greet(name string) string {
return g.prefix + name + "!"
}
func (g *GreetService) Add(a, b int) int {
return a + b
}
// Register service
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&GreetService{prefix: "Hello, "}),
},
})
```
**What Wails does:**
1. **Scans the struct** for exported methods
2. **Extracts type information** (parameters, return types)
3. **Builds a registry** mapping method names to functions
4. **Generates TypeScript bindings** with full type definitions
### 2. Binding Generation (Build Time)
Wails generates TypeScript bindings automatically:
```typescript
// Auto-generated: frontend/bindings/GreetService.ts
export function Greet(name: string): Promise<string>
export function Add(a: number, b: number): Promise<number>
```
**Type mapping:**
| Go Type | TypeScript Type |
|---------|-----------------|
| `string` | `string` |
| `int`, `int32`, `int64` | `number` |
| `float32`, `float64` | `number` |
| `bool` | `boolean` |
| `[]T` | `T[]` |
| `map[string]T` | `Record<string, T>` |
| `struct` | `interface` |
| `time.Time` | `Date` |
| `error` | Exception (thrown) |
### 3. Frontend Call (Runtime)
Developer calls the Go method from JavaScript:
```javascript
import { Greet, Add } from './bindings/GreetService'
// Call Go from JavaScript
const greeting = await Greet("World")
console.log(greeting) // "Hello, World!"
const sum = await Add(5, 3)
console.log(sum) // 8
```
**What happens:**
1. **Binding function called** - `Greet("World")`
2. **Message created** - `{ service: "GreetService", method: "Greet", args: ["World"] }`
3. **Sent to bridge** - Via WebView's JavaScript bridge
4. **Promise returned** - Awaits response
### 4. Bridge Processing (Runtime)
The bridge receives the message and processes it:
```d2
direction: down
Receive: "Receive Message" {
shape: rectangle
style.fill: "#10B981"
}
Parse: "Parse JSON" {
shape: rectangle
}
Validate: "Validate" {
Check: "Service exists?" {
shape: diamond
}
CheckMethod: "Method exists?" {
shape: diamond
}
CheckTypes: "Types correct?" {
shape: diamond
}
}
Invoke: "Invoke Go Method" {
shape: rectangle
style.fill: "#00ADD8"
}
Encode: "Encode Result" {
shape: rectangle
}
Send: "Send Response" {
shape: rectangle
style.fill: "#10B981"
}
Error: "Send Error" {
shape: rectangle
style.fill: "#EF4444"
}
Receive -> Parse
Parse -> Validate.Check
Validate.Check -> Validate.CheckMethod: "Yes"
Validate.Check -> Error: "No"
Validate.CheckMethod -> Validate.CheckTypes: "Yes"
Validate.CheckMethod -> Error: "No"
Validate.CheckTypes -> Invoke: "Yes"
Validate.CheckTypes -> Error: "No"
Invoke -> Encode: "Success"
Invoke -> Error: "Error"
Encode -> Send
```
**Security:** Only registered services and exported methods are callable.
### 5. Go Execution (Runtime)
The Go method executes:
```go
func (g *GreetService) Greet(name string) string {
// This runs in Go
return g.prefix + name + "!"
}
```
**Execution context:**
- Runs in a **goroutine** (non-blocking)
- Has access to **all Go features** (file system, network, databases)
- Can call **other Go code** freely
- Returns result or error
### 6. Response (Runtime)
Result is sent back to JavaScript:
```javascript
// Promise resolves with result
const greeting = await Greet("World")
// greeting = "Hello, World!"
```
**Error handling:**
```go
func (g *GreetService) Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
```
```javascript
try {
const result = await Divide(10, 0)
} catch (error) {
console.error("Go error:", error) // "division by zero"
}
```
## Performance Characteristics
### Speed
**Typical call overhead:** &lt;1ms
```
Frontend Call → Bridge → Go Execution → Bridge → Frontend Response
↓ ↓ ↓ ↓ ↓
&lt;0.1ms &lt;0.1ms [varies] &lt;0.1ms &lt;0.1ms
```
**Compared to alternatives:**
- **HTTP/REST:** 5-50ms (network stack, serialisation)
- **IPC:** 1-10ms (process boundaries, marshalling)
- **Wails Bridge:** &lt;1ms (in-memory, direct call)
### Memory
**Per-call overhead:** ~1KB (message buffer)
**Zero-copy optimisation:** Large data (>1MB) uses shared memory where possible.
### Concurrency
**Calls are concurrent:**
- Each call runs in its own goroutine
- Multiple calls can execute simultaneously
- No blocking between calls
```javascript
// These run concurrently
const [result1, result2, result3] = await Promise.all([
SlowOperation1(),
SlowOperation2(),
SlowOperation3(),
])
```
## Type System
### Supported Types
#### Primitives
```go
// Go
func Example(
s string,
i int,
f float64,
b bool,
) (string, int, float64, bool) {
return s, i, f, b
}
```
```typescript
// TypeScript (auto-generated)
function Example(
s: string,
i: number,
f: number,
b: boolean,
): Promise<[string, number, number, boolean]>
```
#### Slices and Arrays
```go
// Go
func Sum(numbers []int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}
```
```typescript
// TypeScript
function Sum(numbers: number[]): Promise<number>
// Usage
const total = await Sum([1, 2, 3, 4, 5]) // 15
```
#### Maps
```go
// Go
func GetConfig() map[string]interface{} {
return map[string]interface{}{
"theme": "dark",
"fontSize": 14,
"enabled": true,
}
}
```
```typescript
// TypeScript
function GetConfig(): Promise<Record<string, any>>
// Usage
const config = await GetConfig()
console.log(config.theme) // "dark"
```
#### Structs
```go
// Go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func GetUser(id int) (*User, error) {
return &User{
ID: id,
Name: "Alice",
Email: "alice@example.com",
}, nil
}
```
```typescript
// TypeScript (auto-generated)
interface User {
id: number
name: string
email: string
}
function GetUser(id: number): Promise<User>
// Usage
const user = await GetUser(1)
console.log(user.name) // "Alice"
```
**JSON tags:** Use `json:` tags to control field names in TypeScript.
#### Time
```go
// Go
func GetTimestamp() time.Time {
return time.Now()
}
```
```typescript
// TypeScript
function GetTimestamp(): Promise<Date>
// Usage
const timestamp = await GetTimestamp()
console.log(timestamp.toISOString())
```
#### Errors
```go
// Go
func Validate(input string) error {
if input == "" {
return errors.New("input cannot be empty")
}
return nil
}
```
```typescript
// TypeScript
function Validate(input: string): Promise<void>
// Usage
try {
await Validate("")
} catch (error) {
console.error(error) // "input cannot be empty"
}
```
### Unsupported Types
These types **cannot** be passed across the bridge:
- **Channels** (`chan T`)
- **Functions** (`func()`)
- **Interfaces** (except `interface{}` / `any`)
- **Pointers** (except to structs)
- **Unexported fields** (lowercase)
**Workaround:** Use IDs or handles:
```go
// ❌ Can't pass file handle
func OpenFile(path string) (*os.File, error) {
return os.Open(path)
}
// ✅ Return file ID instead
var files = make(map[string]*os.File)
func OpenFile(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
id := generateID()
files[id] = file
return id, nil
}
func ReadFile(id string) ([]byte, error) {
file := files[id]
return io.ReadAll(file)
}
func CloseFile(id string) error {
file := files[id]
delete(files, id)
return file.Close()
}
```
## Advanced Patterns
### Context Passing
Services can access the call context:
```go
type UserService struct{}
func (s *UserService) GetCurrentUser(ctx context.Context) (*User, error) {
// Access window that made the call
window := application.ContextWindow(ctx)
// Access application
app := application.ContextApplication(ctx)
// Your logic
return getCurrentUser(), nil
}
```
**Context provides:**
- Window that made the call
- Application instance
- Request metadata
### Streaming Data
For large data, use events instead of return values:
```go
func ProcessLargeFile(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
// Emit progress events
app.EmitEvent("file-progress", map[string]interface{}{
"line": lineNum,
"text": scanner.Text(),
})
}
return scanner.Err()
}
```
```javascript
import { On } from '@wailsio/runtime'
import { ProcessLargeFile } from './bindings/FileService'
// Listen for progress
On('file-progress', (data) => {
console.log(`Line ${data.line}: ${data.text}`)
})
// Start processing
await ProcessLargeFile('/path/to/large/file.txt')
```
### Cancellation
Use context for cancellable operations:
```go
func LongRunningTask(ctx context.Context) error {
for i := 0; i < 1000; i++ {
// Check if cancelled
select {
case <-ctx.Done():
return ctx.Err()
default:
// Continue work
time.Sleep(100 * time.Millisecond)
}
}
return nil
}
```
**Note:** Context cancellation on frontend disconnect is automatic.
### Batch Operations
Reduce bridge overhead by batching:
```go
// ❌ Inefficient: N bridge calls
for _, item := range items {
await ProcessItem(item)
}
// ✅ Efficient: 1 bridge call
await ProcessItems(items)
```
```go
func ProcessItems(items []Item) ([]Result, error) {
results := make([]Result, len(items))
for i, item := range items {
results[i] = processItem(item)
}
return results, nil
}
```
## Debugging the Bridge
### Enable Debug Logging
```go
app := application.New(application.Options{
Name: "My App",
Logger: application.NewDefaultLogger(),
LogLevel: logger.DEBUG,
})
```
**Output shows:**
- Method calls
- Parameters
- Return values
- Errors
- Timing information
### Inspect Generated Bindings
Check `frontend/bindings/` to see generated TypeScript:
```typescript
// frontend/bindings/MyService.ts
export function MyMethod(arg: string): Promise<number> {
return window.wails.Call('MyService.MyMethod', arg)
}
```
### Test Services Directly
Test Go services without the frontend:
```go
func TestGreetService(t *testing.T) {
service := &GreetService{prefix: "Hello, "}
result := service.Greet("Test")
if result != "Hello, Test!" {
t.Errorf("Expected 'Hello, Test!', got '%s'", result)
}
}
```
## Performance Tips
### ✅ Do
- **Batch operations** - Reduce bridge calls
- **Use events for streaming** - Don't return large arrays
- **Keep methods fast** - &lt;100ms ideal
- **Use goroutines** - For long operations
- **Cache on Go side** - Avoid repeated calculations
### ❌ Don't
- **Don't make excessive calls** - Batch when possible
- **Don't return huge data** - Use pagination or streaming
- **Don't block** - Use goroutines for long operations
- **Don't pass complex types** - Keep it simple
- **Don't ignore errors** - Always handle them
## Security
The bridge is secure by default:
1. **Whitelist only** - Only registered services callable
2. **Type validation** - Arguments checked against Go types
3. **No eval()** - Frontend can't execute arbitrary Go code
4. **No reflection abuse** - Only exported methods accessible
**Best practices:**
- **Validate input** in Go (don't trust frontend)
- **Use context** for authentication/authorisation
- **Rate limit** expensive operations
- **Sanitise** file paths and user input
## Next Steps
**Build System** - Learn how Wails builds and bundles your application
[Learn More →](/concepts/build-system)
**Services** - Deep dive into the service system
[Learn More →](/features/bindings/services)
**Events** - Use events for pub/sub communication
[Learn More →](/features/events/system)
---
**Questions about the bridge?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [binding examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/binding).

View file

@ -0,0 +1,700 @@
---
title: Build System
description: Understanding how Wails builds and packages your application
sidebar:
order: 4
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
## Unified Build System
Wails provides a **unified build system** that compiles Go code, bundles frontend assets, embeds everything into a single executable, and handles platform-specific builds—all with one command.
```bash
wails3 build
```
**Output:** Native executable with everything embedded.
## Build Process Overview
{/*
TODO: Fix D2 diagram generation or embed as image.
The previous D2 code block was causing MDX parsing errors in the build pipeline.
*/}
**[Build Process Diagram Placeholder]**
## Build Phases
### 1. Analysis Phase
Wails scans your Go code to understand your services:
```go
type GreetService struct {
prefix string
}
func (g *GreetService) Greet(name string) string {
return g.prefix + name + "!"
}
```
**What Wails extracts:**
- Service name: `GreetService`
- Method name: `Greet`
- Parameter types: `string`
- Return types: `string`
**Used for:** Generating TypeScript bindings
### 2. Generation Phase
#### TypeScript Bindings
Wails generates type-safe bindings:
```typescript
// Auto-generated: frontend/bindings/GreetService.ts
export function Greet(name: string): Promise<string> {
return window.wails.Call('GreetService.Greet', name)
}
```
**Benefits:**
- Full type safety
- IDE autocomplete
- Compile-time errors
- JSDoc comments
#### Frontend Build
Your frontend bundler runs (Vite, webpack, etc.):
```bash
# Vite example
vite build --outDir dist
```
**What happens:**
- JavaScript/TypeScript compiled
- CSS processed and minified
- Assets optimised
- Source maps generated (dev only)
- Output to `frontend/dist/`
### 3. Compilation Phase
#### Go Compilation
Go code is compiled with optimisations:
```bash
go build -ldflags="-s -w" -o myapp.exe
```
**Flags:**
- `-s`: Strip symbol table
- `-w`: Strip DWARF debugging info
- Result: Smaller binary (~30% reduction)
**Platform-specific:**
- Windows: `.exe` with icon embedded
- macOS: `.app` bundle structure
- Linux: ELF binary
#### Asset Embedding
Frontend assets are embedded into the Go binary:
```go
//go:embed frontend/dist
var assets embed.FS
```
**Result:** Single executable with everything inside.
### 4. Output
**Single native binary:**
- Windows: `myapp.exe` (~15MB)
- macOS: `myapp.app` (~15MB)
- Linux: `myapp` (~15MB)
**No dependencies** (except system WebView).
## Development vs Production
<Tabs syncKey="mode">
<TabItem label="Development (wails3 dev)" icon="seti:config">
**Optimised for speed:**
```bash
wails3 dev
```
**What happens:**
1. Starts frontend dev server (Vite on port 5173)
2. Compiles Go without optimisations
3. Launches app pointing to dev server
4. Enables hot reload
5. Includes source maps
**Characteristics:**
- **Fast rebuilds** (&lt;1s for frontend changes)
- **No asset embedding** (served from dev server)
- **Debug symbols** included
- **Source maps** enabled
- **Verbose logging**
**File size:** Larger (~50MB with debug symbols)
</TabItem>
<TabItem label="Production (wails3 build)" icon="rocket">
**Optimised for size and performance:**
```bash
wails3 build
```
**What happens:**
1. Builds frontend for production (minified)
2. Compiles Go with optimisations
3. Strips debug symbols
4. Embeds assets
5. Creates single binary
**Characteristics:**
- **Optimised code** (minified, tree-shaken)
- **Assets embedded** (no external files)
- **Debug symbols stripped**
- **No source maps**
- **Minimal logging**
**File size:** Smaller (~15MB)
</TabItem>
</Tabs>
## Build Commands
### Basic Build
```bash
wails3 build
```
**Output:** `build/bin/myapp[.exe]`
### Build for Specific Platform
```bash
# Build for Windows (from any OS)
wails3 build -platform windows/amd64
# Build for macOS
wails3 build -platform darwin/amd64
wails3 build -platform darwin/arm64
# Build for Linux
wails3 build -platform linux/amd64
```
**Cross-compilation:** Build for any platform from any platform.
### Build with Options
```bash
# Custom output directory
wails3 build -o ./dist/myapp
# Skip frontend build (use existing)
wails3 build -skipbindings
# Clean build (remove cache)
wails3 build -clean
# Verbose output
wails3 build -v
```
### Build Modes
```bash
# Debug build (includes symbols)
wails3 build -debug
# Production build (default, optimised)
wails3 build
# Development build (fast, unoptimised)
wails3 build -devbuild
```
## Build Configuration
### Taskfile.yml
Wails uses [Taskfile](https://taskfile.dev/) for build configuration:
```yaml
# Taskfile.yml
version: '3'
tasks:
build:
desc: Build the application
cmds:
- wails3 build
build:windows:
desc: Build for Windows
cmds:
- wails3 build -platform windows/amd64
build:macos:
desc: Build for macOS (Universal)
cmds:
- wails3 build -platform darwin/amd64
- wails3 build -platform darwin/arm64
- lipo -create -output build/bin/myapp.app build/bin/myapp-amd64.app build/bin/myapp-arm64.app
build:linux:
desc: Build for Linux
cmds:
- wails3 build -platform linux/amd64
```
**Run tasks:**
```bash
task build:windows
task build:macos
task build:linux
```
### Build Options File
Create `build/build.json` for persistent configuration:
```json
{
"name": "My Application",
"version": "1.0.0",
"author": "Your Name",
"description": "Application description",
"icon": "build/appicon.png",
"outputFilename": "myapp",
"platforms": ["windows/amd64", "darwin/amd64", "linux/amd64"],
"frontend": {
"dir": "./frontend",
"install": "npm install",
"build": "npm run build",
"dev": "npm run dev"
},
"go": {
"ldflags": "-s -w -X main.version={{.Version}}"
}
}
```
## Asset Embedding
### How It Works
Wails uses Go's `embed` package:
```go
package main
import (
"embed"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed frontend/dist
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "My App",
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
})
app.NewWebviewWindow()
app.Run()
}
```
**At build time:**
1. Frontend built to `frontend/dist/`
2. `//go:embed` directive includes files
3. Files compiled into binary
4. Binary contains everything
**At runtime:**
1. App starts
2. Assets served from memory
3. No disk I/O for assets
4. Fast loading
### Custom Assets
Embed additional files:
```go
//go:embed frontend/dist
var frontendAssets embed.FS
//go:embed data/*.json
var dataAssets embed.FS
//go:embed templates/*.html
var templateAssets embed.FS
```
## Build Optimisations
### Frontend Optimisations
**Vite (default):**
```javascript
// vite.config.js
export default {
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.log
drop_debugger: true,
},
},
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'], // Separate vendor bundle
},
},
},
},
}
```
**Results:**
- JavaScript minified (~70% reduction)
- CSS minified (~60% reduction)
- Images optimised
- Tree-shaking applied
### Go Optimisations
**Compiler flags:**
```bash
-ldflags="-s -w"
```
- `-s`: Strip symbol table (~10% reduction)
- `-w`: Strip DWARF debug info (~20% reduction)
**Additional optimisations:**
```bash
-ldflags="-s -w -X main.version=1.0.0"
```
- `-X`: Set variable values at build time
- Useful for version numbers, build dates
### Binary Compression
**UPX (optional):**
```bash
# After building
upx --best build/bin/myapp.exe
```
**Results:**
- ~50% size reduction
- Slightly slower startup (~100ms)
- Not recommended for macOS (code signing issues)
## Platform-Specific Builds
### Windows
**Output:** `myapp.exe`
**Includes:**
- Application icon
- Version information
- Manifest (UAC settings)
**Icon:**
```bash
# Specify icon
wails3 build -icon build/appicon.png
```
Wails converts PNG to `.ico` automatically.
**Manifest:**
```xml
<!-- build/windows/manifest.xml -->
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" name="MyApp"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
```
### macOS
**Output:** `myapp.app` (application bundle)
**Structure:**
```
myapp.app/
├── Contents/
│ ├── Info.plist # App metadata
│ ├── MacOS/
│ │ └── myapp # Binary
│ ├── Resources/
│ │ └── icon.icns # Icon
│ └── _CodeSignature/ # Code signature (if signed)
```
**Info.plist:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>My App</string>
<key>CFBundleIdentifier</key>
<string>com.example.myapp</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
</dict>
</plist>
```
**Universal Binary:**
```bash
# Build for both architectures
wails3 build -platform darwin/amd64
wails3 build -platform darwin/arm64
# Combine into universal binary
lipo -create -output myapp-universal \
build/bin/myapp-amd64 \
build/bin/myapp-arm64
```
### Linux
**Output:** `myapp` (ELF binary)
**Dependencies:**
- GTK3
- WebKitGTK
**Desktop file:**
```ini
# myapp.desktop
[Desktop Entry]
Name=My App
Exec=/usr/bin/myapp
Icon=myapp
Type=Application
Categories=Utility;
```
**Installation:**
```bash
# Copy binary
sudo cp myapp /usr/bin/
# Copy desktop file
sudo cp myapp.desktop /usr/share/applications/
# Copy icon
sudo cp icon.png /usr/share/icons/hicolor/256x256/apps/myapp.png
```
## Build Performance
### Typical Build Times
| Phase | Time | Notes |
|-------|------|-------|
| Analysis | &lt;1s | Go code scanning |
| Binding Generation | &lt;1s | TypeScript generation |
| Frontend Build | 5-30s | Depends on project size |
| Go Compilation | 2-10s | Depends on code size |
| Asset Embedding | &lt;1s | Embedding frontend |
| **Total** | **10-45s** | First build |
| **Incremental** | **5-15s** | Subsequent builds |
### Speeding Up Builds
**1. Use build cache:**
```bash
# Go build cache is automatic
# Frontend cache (Vite)
npm run build # Uses cache by default
```
**2. Skip unchanged steps:**
```bash
# Skip frontend if unchanged
wails3 build -skipbindings
```
**3. Parallel builds:**
```bash
# Build multiple platforms in parallel
wails3 build -platform windows/amd64 &
wails3 build -platform darwin/amd64 &
wails3 build -platform linux/amd64 &
wait
```
**4. Use faster tools:**
```bash
# Use esbuild instead of webpack
# (Vite uses esbuild by default)
```
## Troubleshooting
### Build Fails
**Symptom:** `wails3 build` exits with error
**Common causes:**
1. **Go compilation error**
```bash
# Check Go code compiles
go build
```
2. **Frontend build error**
```bash
# Check frontend builds
cd frontend
npm run build
```
3. **Missing dependencies**
```bash
# Install dependencies
npm install
go mod download
```
### Binary Too Large
**Symptom:** Binary is >50MB
**Solutions:**
1. **Strip debug symbols** (should be automatic)
```bash
wails3 build # Already includes -ldflags="-s -w"
```
2. **Check embedded assets**
```bash
# Remove unnecessary files from frontend/dist/
# Check for large images, videos, etc.
```
3. **Use UPX compression**
```bash
upx --best build/bin/myapp.exe
```
### Slow Builds
**Symptom:** Builds take >1 minute
**Solutions:**
1. **Use build cache**
- Go cache is automatic
- Frontend cache (Vite) is automatic
2. **Skip unchanged steps**
```bash
wails3 build -skipbindings
```
3. **Optimise frontend build**
```javascript
// vite.config.js
export default {
build: {
minify: 'esbuild', // Faster than terser
},
}
```
## Best Practices
### ✅ Do
- **Use `wails3 dev` during development** - Fast iteration
- **Use `wails3 build` for releases** - Optimised output
- **Version your builds** - Use `-ldflags` to embed version
- **Test builds on target platforms** - Cross-compilation isn't perfect
- **Keep frontend builds fast** - Optimise bundler config
- **Use build cache** - Speeds up subsequent builds
### ❌ Don't
- **Don't commit `build/` directory** - Add to `.gitignore`
- **Don't skip testing builds** - Always test before release
- **Don't embed unnecessary assets** - Keep binaries small
- **Don't use debug builds for production** - Use optimised builds
- **Don't forget code signing** - Required for distribution
## Next Steps
**Building Applications** - Detailed guide to building and packaging
[Learn More →](/guides/building)
**Cross-Platform Builds** - Build for all platforms from one machine
[Learn More →](/guides/cross-platform)
**Creating Installers** - Create installers for end users
[Learn More →](/guides/installers)
---
**Questions about building?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [build examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/build).

View file

@ -0,0 +1,612 @@
---
title: Application Lifecycle
description: Understanding the Wails application lifecycle from startup to shutdown
sidebar:
order: 2
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
## Understanding Application Lifecycle
Desktop applications have a lifecycle from startup to shutdown. Wails provides hooks at each stage to **initialise resources**, **clean up properly**, **handle errors gracefully**, and **manage multiple windows** effectively.
## The Lifecycle Stages
```d2
direction: down
Start: "Application Start" {
shape: oval
style.fill: "#10B981"
}
PreInit: "Pre-Initialisation" {
Parse: "Parse Options" {
shape: rectangle
}
Register: "Register Services" {
shape: rectangle
}
Validate: "Validate Config" {
shape: rectangle
}
}
OnStartup: "OnStartup Hook" {
shape: rectangle
style.fill: "#3B82F6"
}
CreateWindows: "Create Windows" {
shape: rectangle
}
EventLoop: "Event Loop" {
Process: "Process Events" {
shape: rectangle
}
Handle: "Handle Messages" {
shape: rectangle
}
Update: "Update UI" {
shape: rectangle
}
}
QuitSignal: "Quit Signal" {
shape: diamond
style.fill: "#F59E0B"
}
OnBeforeClose: "OnBeforeClose Hook" {
shape: rectangle
style.fill: "#3B82F6"
}
OnShutdown: "OnShutdown Hook" {
shape: rectangle
style.fill: "#3B82F6"
}
Cleanup: "Cleanup" {
Close: "Close Windows" {
shape: rectangle
}
Release: "Release Resources" {
shape: rectangle
}
}
End: "Application End" {
shape: oval
style.fill: "#EF4444"
}
Start -> PreInit.Parse
PreInit.Parse -> PreInit.Register
PreInit.Register -> PreInit.Validate
PreInit.Validate -> OnStartup
OnStartup -> CreateWindows
CreateWindows -> EventLoop.Process
EventLoop.Process -> EventLoop.Handle
EventLoop.Handle -> EventLoop.Update
EventLoop.Update -> EventLoop.Process: "Loop"
EventLoop.Process -> QuitSignal: "User quits"
QuitSignal -> OnBeforeClose: "Can cancel?"
OnBeforeClose -> EventLoop.Process: "Cancelled"
OnBeforeClose -> OnShutdown: "Confirmed"
OnShutdown -> Cleanup.Close
Cleanup.Close -> Cleanup.Release
Cleanup.Release -> End
```
### 1. Pre-Initialisation
Before your code runs, Wails:
1. Parses `application.Options`
2. Registers services
3. Validates configuration
4. Sets up the runtime
**You don't control this phase** - it happens automatically.
### 2. OnStartup Hook
Your first opportunity to run code:
```go
app := application.New(application.Options{
Name: "My App",
OnStartup: func(ctx context.Context) {
// Initialise database
db, err := sql.Open("sqlite3", "app.db")
if err != nil {
log.Fatal(err)
}
// Load configuration
config, err := loadConfig()
if err != nil {
log.Fatal(err)
}
// Store in context for services to access
ctx = context.WithValue(ctx, "db", db)
ctx = context.WithValue(ctx, "config", config)
},
})
```
**When it runs:** After Wails initialisation, before windows are created
**Use it for:**
- Database connections
- Configuration loading
- Resource initialisation
- Authentication checks
**Context:** The `context.Context` is passed to all services and can store shared state.
### 3. Window Creation
After `OnStartup`, you create windows:
```go
window := app.NewWebviewWindow()
```
**What happens:**
1. Window is created (but not shown)
2. WebView is initialised
3. Frontend assets are loaded
4. Window is shown (unless `Hidden: true`)
### 4. Event Loop
The application enters the event loop:
```go
err := app.Run() // Blocks here until quit
```
**What happens in the loop:**
- OS events processed (mouse, keyboard, window events)
- Go-to-JS messages handled
- JS-to-Go calls executed
- UI updates rendered
**This is where your application spends most of its time.**
### 5. Quit Signal
User triggers quit via:
- Closing last window (default behaviour)
- Cmd+Q / Alt+F4 / File → Quit
- Your code calling `app.Quit()`
### 6. OnBeforeClose Hook
**Optional hook to prevent quit:**
```go
window := app.NewWebviewWindow(application.WebviewWindowOptions{
OnBeforeClose: func() bool {
// Return false to cancel quit
// Return true to allow quit
if hasUnsavedChanges() {
result := showConfirmDialog("Unsaved changes. Quit anyway?")
return result == "yes"
}
return true
},
})
```
**Use cases:**
- Confirm quit with unsaved changes
- Prevent accidental closure
- Save state before quitting
**Important:** Only works for window close events, not `app.Quit()`.
### 7. OnShutdown Hook
Your last opportunity to run code:
```go
app := application.New(application.Options{
OnShutdown: func() {
// Save application state
saveState()
// Close database
db.Close()
// Release resources
cleanup()
},
})
```
**When it runs:** After quit confirmed, before application exits
**Use it for:**
- Saving state
- Closing connections
- Releasing resources
- Final cleanup
**Important:** Keep it fast (&lt;1 second). OS may force-kill if too slow.
### 8. Cleanup & Exit
Wails automatically:
1. Closes all windows
2. Releases WebView resources
3. Exits the process
## Lifecycle Hooks Reference
| Hook | When | Can Cancel? | Use For |
|------|------|-------------|---------|
| `OnStartup` | Before windows created | No | Initialisation |
| `OnBeforeClose` | Window closing | Yes | Confirm quit |
| `OnShutdown` | After quit confirmed | No | Cleanup |
## Common Patterns
### Pattern 1: Database Lifecycle
```go
var db *sql.DB
app := application.New(application.Options{
OnStartup: func(ctx context.Context) {
var err error
db, err = sql.Open("sqlite3", "app.db")
if err != nil {
log.Fatal(err)
}
// Run migrations
if err := runMigrations(db); err != nil {
log.Fatal(err)
}
},
OnShutdown: func() {
if db != nil {
db.Close()
}
},
})
```
### Pattern 2: Configuration Management
```go
type Config struct {
Theme string
Language string
WindowPos Point
}
var config *Config
app := application.New(application.Options{
OnStartup: func(ctx context.Context) {
config = loadConfig() // Load from disk
},
OnShutdown: func() {
saveConfig(config) // Save to disk
},
})
```
### Pattern 3: Confirm Quit with Unsaved Changes
```go
type AppState struct {
hasUnsavedChanges bool
}
var state AppState
window := app.NewWebviewWindow(application.WebviewWindowOptions{
OnBeforeClose: func() bool {
if state.hasUnsavedChanges {
// Show dialog (blocks until user responds)
result := showConfirmdialog("Unsaved changes. Quit anyway?")
return result == "yes"
}
return true
},
})
```
### Pattern 4: Background Tasks
```go
app := application.New(application.Options{
OnStartup: func(ctx context.Context) {
// Start background task
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
performBackgroundSync()
case <-ctx.Done():
// Context cancelled, quit
return
}
}
}()
},
})
```
**Important:** Use `ctx.Done()` to know when to stop background tasks.
## Window Lifecycle
Each window has its own lifecycle:
```d2
direction: down
Create: "Create Window" {
shape: oval
style.fill: "#10B981"
}
Load: "Load Frontend" {
shape: rectangle
}
Show: "Show Window" {
shape: rectangle
}
Active: "Window Active" {
Events: "Handle Events" {
shape: rectangle
}
}
CloseRequest: "Close Request" {
shape: diamond
style.fill: "#F59E0B"
}
OnBeforeClose: "OnBeforeClose" {
shape: rectangle
style.fill: "#3B82F6"
}
Destroy: "Destroy Window" {
shape: rectangle
}
End: "Window Closed" {
shape: oval
style.fill: "#EF4444"
}
Create -> Load
Load -> Show
Show -> Active.Events
Active.Events -> Active.Events: "Loop"
Active.Events -> CloseRequest: "User closes"
CloseRequest -> OnBeforeClose
OnBeforeClose -> Active.Events: "Cancelled"
OnBeforeClose -> Destroy: "Confirmed"
Destroy -> End
```
**Key points:**
- Each window is independent
- Closing last window quits app (by default)
- Windows can prevent their own closure
## Multi-Window Lifecycle
With multiple windows:
```go
// Create main window
mainWindow := app.NewWebviewWindow()
// Create secondary window
secondaryWindow := app.NewWebviewWindow(application.WebviewWindowOptions{
Title: "Settings",
Width: 400,
Height: 600,
})
// Closing secondary window doesn't quit app
// Closing main window quits app (closes all windows)
```
**Default behaviour:**
- Closing any window closes just that window
- Closing the **last** window quits the application
**Custom behaviour:**
```go
// Prevent app quit when last window closes
app := application.New(application.Options{
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
})
// Now app stays running even with no windows
// Useful for menu bar / system tray apps
```
## Error Handling During Lifecycle
### Startup Errors
```go
app := application.New(application.Options{
OnStartup: func(ctx context.Context) {
if err := initialise(); err != nil {
// Show error dialog
showErrordialog("Initialisation failed: " + err.Error())
// Quit application
app.Quit()
}
},
})
```
### Shutdown Errors
```go
app := application.New(application.Options{
OnShutdown: func() {
if err := saveState(); err != nil {
// Log error (can't show dialog, app is quitting)
log.Printf("Failed to save state: %v", err)
}
},
})
```
**Important:** `OnShutdown` runs during quit - don't show dialogs or try to cancel.
## Platform Differences
### macOS
- **Application menu** persists even with no windows
- **Cmd+Q** always quits (can't be prevented)
- **Dock icon** remains unless hidden
### Windows
- **No application menu** without a window
- **Alt+F4** closes window (can be prevented)
- **System tray** can keep app running
### Linux
- **Behaviour varies** by desktop environment
- **Generally similar to Windows**
## Debugging Lifecycle Issues
### Problem: Resources Not Cleaned Up
**Symptom:** Database connections left open, files not closed
**Solution:** Use `OnShutdown`:
```go
app := application.New(application.Options{
OnShutdown: func() {
log.Println("Cleaning up...")
// Your cleanup code
},
})
```
### Problem: Application Won't Quit
**Symptom:** App hangs when trying to quit
**Causes:**
1. `OnBeforeClose` returning `false`
2. `OnShutdown` taking too long
3. Background goroutines not stopping
**Solution:**
```go
// 1. Check OnBeforeClose logic
OnBeforeClose: func() bool {
log.Println("OnBeforeClose called")
return true // Allow quit
}
// 2. Keep OnShutdown fast
OnShutdown: func() {
log.Println("OnShutdown started")
// Fast cleanup only
log.Println("OnShutdown finished")
}
// 3. Stop background tasks
OnStartup: func(ctx context.Context) {
go func() {
for {
select {
case <-ctx.Done():
log.Println("Background task stopped")
return
default:
// Work
}
}
}()
}
```
### Problem: Initialisation Fails Silently
**Symptom:** App starts but doesn't work correctly
**Solution:** Check errors in `OnStartup`:
```go
OnStartup: func(ctx context.Context) {
if err := initialise(); err != nil {
log.Fatal("Initialisation failed:", err)
// Or show dialog and quit
}
}
```
## Best Practices
### ✅ Do
- **Initialise in OnStartup** - Database, config, resources
- **Clean up in OnShutdown** - Close connections, save state
- **Keep OnShutdown fast** - &lt;1 second
- **Use context for cancellation** - Stop background tasks
- **Handle errors gracefully** - Show dialogs, log errors
- **Test quit scenarios** - Unsaved changes, background tasks
### ❌ Don't
- **Don't block OnStartup** - Keep it fast (&lt;2 seconds)
- **Don't show dialogs in OnShutdown** - App is quitting
- **Don't ignore errors** - Log or show them
- **Don't leak resources** - Always clean up
- **Don't forget background tasks** - Stop them on quit
## Next Steps
**Go-Frontend Bridge** - Understand how Go and JavaScript communicate
[Learn More →](/concepts/bridge)
**Build System** - Learn how Wails builds your application
[Learn More →](/concepts/build-system)
**Events System** - Use events for communication between components
[Learn More →](/features/events/system)
**Window Management** - Create and manage multiple windows
[Learn More →](/features/windows/basics)
---
**Questions about lifecycle?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).

View file

@ -1,7 +1,8 @@
---
title: Manager API
description: Organized API structure with focused manager interfaces
sidebar:
order: 25
order: 2
---
import { Tabs, TabItem } from "@astrojs/starlight/components";

View file

@ -0,0 +1,275 @@
---
title: Contributing
description: Contribute to Wails
sidebar:
order: 100
---
import { Card, CardGrid } from "@astrojs/starlight/components";
## Welcome Contributors!
We welcome contributions to Wails! Whether you're fixing bugs, adding features, or improving documentation, your help is appreciated.
## Ways to Contribute
### 1. Report Issues
Found a bug? [Open an issue](https://github.com/wailsapp/wails/issues/new) with:
- Clear description
- Steps to reproduce
- Expected vs actual behaviour
- System information
- Code samples
### 2. Improve Documentation
Documentation improvements are always welcome:
- Fix typos and errors
- Add examples
- Clarify explanations
- Translate content
### 3. Submit Code
Contribute code through pull requests:
- Bug fixes
- New features
- Performance improvements
- Tests
## Getting Started
### Fork and Clone
```bash
# Fork the repository on GitHub
# Then clone your fork
git clone https://github.com/YOUR_USERNAME/wails.git
cd wails
# Add upstream remote
git remote add upstream https://github.com/wailsapp/wails.git
```
### Build from Source
```bash
# Install dependencies
go mod download
# Build Wails CLI
cd v3/cmd/wails3
go build
# Test your build
./wails3 version
```
### Run Tests
```bash
# Run all tests
go test ./...
# Run specific package tests
go test ./v3/pkg/application
# Run with coverage
go test -cover ./...
```
## Making Changes
### Create a Branch
```bash
# Update main
git checkout main
git pull upstream main
# Create feature branch
git checkout -b feature/my-feature
```
### Make Your Changes
1. **Write code** following Go conventions
2. **Add tests** for new functionality
3. **Update documentation** if needed
4. **Run tests** to ensure nothing breaks
5. **Commit changes** with clear messages
### Commit Guidelines
```bash
# Good commit messages
git commit -m "fix: resolve window focus issue on macOS"
git commit -m "feat: add support for custom window chrome"
git commit -m "docs: improve bindings documentation"
# Use conventional commits:
# - feat: New feature
# - fix: Bug fix
# - docs: Documentation
# - test: Tests
# - refactor: Code refactoring
# - chore: Maintenance
```
### Submit Pull Request
```bash
# Push to your fork
git push origin feature/my-feature
# Open pull request on GitHub
# Provide clear description
# Reference related issues
```
## Pull Request Guidelines
### Good PR Description
```markdown
## Description
Brief description of changes
## Changes
- Added feature X
- Fixed bug Y
- Updated documentation
## Testing
- Tested on macOS 14
- Tested on Windows 11
- All tests passing
## Related Issues
Fixes #123
```
### PR Checklist
- [ ] Code follows Go conventions
- [ ] Tests added/updated
- [ ] Documentation updated
- [ ] All tests passing
- [ ] No breaking changes (or documented)
- [ ] Commit messages clear
## Code Guidelines
### Go Code Style
```go
// ✅ Good: Clear, documented, tested
// ProcessData processes the input data and returns the result.
// It returns an error if the data is invalid.
func ProcessData(data string) (string, error) {
if data == "" {
return "", errors.New("data cannot be empty")
}
result := process(data)
return result, nil
}
// ❌ Bad: No docs, no error handling
func ProcessData(data string) string {
return process(data)
}
```
### Testing
```go
func TestProcessData(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{"valid input", "test", "processed", false},
{"empty input", "", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ProcessData(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ProcessData() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ProcessData() = %v, want %v", got, tt.want)
}
})
}
}
```
## Documentation
### Writing Docs
Documentation uses Starlight (Astro):
```bash
cd docs
npm install
npm run dev
```
### Documentation Style
- Use International English spelling
- Start with the problem
- Provide working examples
- Include troubleshooting
- Cross-reference related content
## Community
### Get Help
- **Discord:** [Join our community](https://discord.gg/JDdSxwjhGf)
- **GitHub Discussions:** Ask questions
- **GitHub Issues:** Report bugs
### Code of Conduct
Be respectful, inclusive, and professional. We're all here to build great software together.
## Recognition
Contributors are recognised in:
- Release notes
- Contributors list
- GitHub insights
Thank you for contributing to Wails! 🎉
## Next Steps
<CardGrid>
<Card title="GitHub Repository" icon="github">
Visit the Wails repository.
[View on GitHub →](https://github.com/wailsapp/wails)
</Card>
<Card title="Discord Community" icon="discord">
Join the community.
[Join Discord →](https://discord.gg/JDdSxwjhGf)
</Card>
<Card title="Documentation" icon="open-book">
Read the docs.
[Browse Docs →](/quick-start/why-wails)
</Card>
</CardGrid>

View file

@ -5,7 +5,6 @@ sidebar:
order: 1
---
import Mermaid from "../../components/Mermaid.astro";
Wails v3 is a **full-stack desktop framework** consisting of a Go runtime,
a JavaScript bridge, a task-driven tool-chain and a collection of templates that
@ -22,59 +21,27 @@ This page presents the *big picture* in four diagrams:
## 1 · Overall Architecture
<Mermaid title="Wails v3 High-Level Stack">
flowchart TD
subgraph Developer
CLI[wails3 CLI]
end
subgraph Build["Build-time Tool-chain"]
GEN["Binding Generator\n(static analysis)"]
TMP["Template Engine"]
ASSETDEV["Asset Server (dev)"]
PKG["Cross-compilation & Packaging"]
end
subgraph Runtime["Native Runtime"]
RT["Desktop Runtime\n(window, dialogs, tray, …)"]
BRIDGE["Message Bridge\n(JSON channel)"]
end
subgraph App["Your Application"]
BACKEND["Go Backend"]
FRONTEND["Web Frontend\n(React/Vue/…)"]
end
**Wails v3 High-Level Stack**
CLI -->|init| TMP
CLI -->|generate| GEN
CLI -->|dev| ASSETDEV
CLI -->|build| PKG
{/*
TODO: Fix D2 diagram generation (triple quotes for multi-line strings) or embed as image.
The previous D2 code block was causing MDX parsing errors in the build pipeline.
*/}
GEN -->|Go & TS stubs| BACKEND
GEN -->|bindings.json| FRONTEND
ASSETDEV <-->|HTTP| FRONTEND
BACKEND <--> BRIDGE <--> FRONTEND
BRIDGE <--> RT
RT <-->|serve assets| ASSETDEV
</Mermaid>
**[High-Level Stack Diagram Placeholder]**
---
## 2 · Runtime Call Flow
<Mermaid title="Runtime JavaScript ⇄ Go Calling Path">
sequenceDiagram
participant JS as JavaScript (frontend)
participant Bridge as Bridge (WebView callback)
participant MP as Message Processor (Go)
participant Go as Bound Go Function
**Runtime JavaScript ⇄ Go Calling Path**
JS->>Bridge: invoke("Greet","Alice")
Bridge->>MP: JSON {t:c,id:42,...}
MP->>Go: call Greet("Alice")
Go-->>MP: "Hello Alice"
MP-->>Bridge: JSON {t:r,id:42,result:"Hello Alice"}
Bridge-->>JS: Promise.resolve("Hello Alice")
</Mermaid>
{/*
TODO: Fix D2 diagram generation (triple quotes for multi-line strings) or embed as image.
The previous D2 code block was causing MDX parsing errors in the build pipeline.
*/}
**[Runtime Call Flow Diagram Placeholder]**
Key points:
@ -86,25 +53,14 @@ Key points:
## 3 · Development vs Production Asset Flow
<Mermaid title="Dev ↔ Prod Asset Server">
flowchart LR
subgraph Dev["`wails3 dev`"]
VITE["Framework Dev Server\n(port 5173)"]
ASDEV["Asset Server (dev)\n(proxy + disk)"]
FRONTENDDEV[Browser]
end
subgraph Prod["`wails3 build`"]
EMBED["Embedded FS\n(go:embed)"]
ASPROD["Asset Server (prod)\n(read-only)"]
FRONTENDPROD[WebView Window]
end
**Dev ↔ Prod Asset Server**
VITE <-->|proxy / HMR| ASDEV
ASDEV <-->|http| FRONTENDDEV
{/*
TODO: Fix D2 diagram generation (triple quotes for multi-line strings) or embed as image.
The previous D2 code block was causing MDX parsing errors in the build pipeline.
*/}
EMBED --> ASPROD
ASPROD <-->|in-memory| FRONTENDPROD
</Mermaid>
**[Asset Flow Diagram Placeholder]**
* In **dev** the server proxies unknown paths to the frameworks live-reload
server and serves static assets from disk.
@ -115,36 +71,14 @@ flowchart LR
## 4 · Platform-Specific Runtime Split
<Mermaid title="Per-OS Runtime Files">
classDiagram
class runtime::Window {
+Show()
+Hide()
+Center()
}
**Per-OS Runtime Files**
runtime::Window <|-- Window_darwin
runtime::Window <|-- Window_linux
runtime::Window <|-- Window_windows
{/*
TODO: Fix D2 diagram generation (triple quotes for multi-line strings) or embed as image.
The previous D2 code block was causing MDX parsing errors in the build pipeline.
*/}
class Window_darwin {
//go:build darwin
+NSWindow* ptr
}
class Window_linux {
//go:build linux
+GtkWindow* ptr
}
class Window_windows {
//go:build windows
+HWND ptr
}
note for runtime::Window "Shared interface\nin pkg/application"
note for Window_darwin "Objective-C (Cgo)"
note for Window_linux "Pure Go GTK calls"
note for Window_windows "Win32 API via syscall"
</Mermaid>
**[Platform Split Diagram Placeholder]**
Every feature follows this pattern:

View file

@ -1,7 +1,8 @@
---
title: Binding System Internals
title: Binding System
description: How the binding system collects, processes, and generates JavaScript/TypeScript code
sidebar:
order: 21
order: 1
---
import { FileTree } from "@astrojs/starlight/components";

View file

@ -29,7 +29,7 @@ bytes over the WebView.
| Stage | Component | Output |
|-------|-----------|--------|
| **Analysis** | `internal/generator/analyse.go` | In-memory AST of exported Go structs, methods, params, return types |
| **Generation** | `internal/generator/render/*.tmpl` | • `pkg/application/bindings.go` (Go)<br>• `frontend/src/wailsjs/**.ts` (TS defs)<br>• `runtime/desktop/@wailsio/runtime/internal/bindings.json` |
| **Generation** | `internal/generator/render/*.tmpl` | • `pkg/application/bindings.go` (Go)<br />• `frontend/src/wailsjs/**.ts` (TS defs)<br />• `runtime/desktop/@wailsio/runtime/internal/bindings.json` |
| **Runtime** | `pkg/application/messageprocessor_call.go` + JS runtime (`invoke.ts`) | JSON messages over WebView native bridge |
The flow is orchestrated by the `wails3` CLI:

View file

@ -89,7 +89,7 @@ internal/
| Package | Responsibility | Where It Connects |
|---------|----------------|-------------------|
| `runtime` | Window/event loop, clipboard, dialogs, system tray. One file per OS with build-tags (`*_darwin.go`, `*_linux.go`, …). | Called by `pkg/application` and message processor. |
| `assetserver` | Dual-mode file server:<br>• Dev: serves from disk & proxies Vite<br>• Prod: embeds assets via `go:embed` | Initialized by `pkg/application` during startup. |
| `assetserver` | Dual-mode file server:<br />• Dev: serves from disk & proxies Vite<br />• Prod: embeds assets via `go:embed` | Initialized by `pkg/application` during startup. |
| `generator` | Parses Go source to build **binding metadata** which later produces TypeScript stub files and Go marshal/unmarshal code. | Triggered by `wails3 init` / `wails3 generate`. |
| `packager` | Wraps platform-specific packaging CLIs (eg. `electron-builder` equivalents) into Go for cross-platform automation. | Invoked by `wails3 package`. |
@ -172,19 +172,25 @@ so the same logic powers **CLI** and **CI**.
## How the Pieces Interact
```mermaid
flowchart TD
A[wails3 CLI] -- build/generate --> B[internal.generator]
A -- dev --> C[assetserver (dev)]
A -- package --> P[internal.packager]
subgraph App runtime
E[pkg.application] --> F[internal.runtime]
F --> G[OS APIs]
E --> C
end
B --> E %% generated bindings registered at init
```d2
direction: down
CLI: "wails3 CLI"
Generator: "internal/generator"
AssetDev: "assetserver (dev)"
Packager: "internal/packager"
AppRuntime: {
label: "App runtime"
ApplicationPkg: "pkg.application"
InternalRuntime: "internal.runtime"
OSAPIs: "OS APIs"
}
CLI -> Generator: "build / generate"
CLI -> AssetDev: "dev"
CLI -> Packager: "package"
Generator -> ApplicationPkg: "bindings"
ApplicationPkg -> InternalRuntime
InternalRuntime -> OSAPIs
ApplicationPkg -> AssetDev
```
*CLI → generator → runtime* forms the core path from **source** to **running

View file

@ -0,0 +1,369 @@
---
title: Getting Started
description: How to start contributing to Wails v3
---
import { Steps, Tabs, TabItem } from '@astrojs/starlight/components';
## Welcome, Contributor!
Thank you for your interest in contributing to Wails! This guide will help you make your first contribution.
## Prerequisites
Before you begin, ensure you have:
- **Go 1.25+** installed ([download](https://go.dev/dl/))
- **Node.js 20+** and **npm** ([download](https://nodejs.org/))
- **Git** configured with your GitHub account
- Basic familiarity with Go and JavaScript/TypeScript
### Platform-Specific Requirements
**macOS:**
- Xcode Command Line Tools: `xcode-select --install`
**Windows:**
- MSYS2 or similar Unix-like environment recommended
- WebView2 runtime (usually pre-installed on Windows 11)
**Linux:**
- `gcc`, `pkg-config`, `libgtk-3-dev`, `libwebkit2gtk-4.0-dev`
- Install via: `sudo apt install build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.0-dev` (Debian/Ubuntu)
## Contribution Process Overview
The typical contribution workflow follows these steps:
1. **Fork & Clone** - Create your own copy of the Wails repository
2. **Setup** - Build the Wails CLI and verify your environment
3. **Branch** - Create a feature branch for your changes
4. **Develop** - Make your changes following our coding standards
5. **Test** - Run tests to ensure everything works
6. **Commit** - Commit with clear, conventional commit messages
7. **Submit** - Open a pull request for review
8. **Iterate** - Respond to feedback and make adjustments
9. **Merge** - Once approved, your changes become part of Wails!
## Step-by-Step Guide
Choose your contribution type:
<Tabs>
<TabItem label="Bug Fix">
<Steps>
1. **Find or Report the Bug**
- Check if the bug is already reported in [GitHub Issues](https://github.com/wailsapp/wails/issues)
- If not, create a new issue with steps to reproduce
- Wait for confirmation before starting work
2. **Fork and Clone**
Fork the repository at [github.com/wailsapp/wails/fork](https://github.com/wailsapp/wails/fork)
Clone your fork:
```bash
git clone https://github.com/YOUR_USERNAME/wails.git
cd wails
git remote add upstream https://github.com/wailsapp/wails.git
```
3. **Build and Verify**
Build Wails and verify you can reproduce the bug:
```bash
cd v3
go build -o ../wails3 ./cmd/wails3
# Reproduce the bug to understand it
```
4. **Create a Bug Fix Branch**
Create a branch for your fix:
```bash
git checkout -b fix/issue-123-window-crash
```
5. **Fix the Bug**
- Make the minimal changes needed to fix the bug
- Don't refactor unrelated code
- Add or update tests to prevent regression
```bash
# Make your changes
# Add tests in *_test.go files
```
6. **Test Your Fix**
Run tests to ensure the fix works:
```bash
go test ./...
# Test the specific package
go test ./pkg/application -v
# Run with race detector
go test ./... -race
```
7. **Commit Your Fix**
Commit with a clear message:
```bash
git commit -m "fix: prevent window crash when closing during initialization
Fixes #123"
```
8. **Submit Pull Request**
Push and create PR:
```bash
git push origin fix/issue-123-window-crash
```
In your PR description:
- Explain the bug and root cause
- Describe your fix
- Reference the issue: "Fixes #123"
- Include before/after behavior
9. **Respond to Feedback**
Address review comments and update your PR as needed.
</Steps>
</TabItem>
<TabItem label="Enhancements">
<Steps>
1. **Discuss the Feature**
- Open a [GitHub Discussion](https://github.com/wailsapp/wails/discussions) or issue
- Describe what you want to add and why
- Wait for maintainer feedback before implementing
- Ensure it aligns with Wails' goals
2. **Fork and Clone**
Fork the repository at [github.com/wailsapp/wails/fork](https://github.com/wailsapp/wails/fork)
Clone your fork:
```bash
git clone https://github.com/YOUR_USERNAME/wails.git
cd wails
git remote add upstream https://github.com/wailsapp/wails.git
```
3. **Setup Development Environment**
Build Wails and verify your environment:
```bash
cd v3
go build -o ../wails3 ./cmd/wails3
# Run tests to ensure everything works
go test ./...
```
4. **Create a Feature Branch**
Create a descriptive branch:
```bash
git checkout -b feat/window-transparency-support
```
5. **Implement the Feature**
- Follow our [Coding Standards](/contributing/standards)
- Keep changes focused on the feature
- Write clean, documented code
- Add comprehensive tests
```bash
# Example: Adding a new window method
# 1. Add to window.go interface
# 2. Implement in platform files (darwin, windows, linux)
# 3. Add tests
# 4. Update documentation
```
6. **Test Thoroughly**
Test your feature:
```bash
# Unit tests
go test ./pkg/application -v
# Integration test - create a test app
cd ..
./wails3 init -n feature-test
cd feature-test
# Add code using your new feature
../wails3 dev
```
7. **Document Your Feature**
- Add docstrings to all public APIs
- Update relevant documentation in `/docs`
- Add examples if applicable
8. **Commit with Convention**
Use conventional commits:
```bash
git commit -m "feat: add window transparency support
- Add SetTransparent() method to Window API
- Implement for macOS, Windows, and Linux
- Add tests and documentation
Closes #456"
```
9. **Submit Pull Request**
Push and create PR:
```bash
git push origin feat/window-transparency-support
```
In your PR:
- Describe the feature and use cases
- Show examples or screenshots
- List any breaking changes
- Reference the discussion/issue
10. **Iterate Based on Review**
Maintainers may request changes. Be patient and collaborative.
</Steps>
</TabItem>
<TabItem label="Documentation">
<Steps>
1. **Identify Documentation Needs**
- Found outdated docs while using Wails?
- Notice missing examples or explanations?
- Want to fix typos or improve clarity?
- Check [documentation issues](https://github.com/wailsapp/wails/labels/documentation)
2. **Fork and Clone**
Fork the repository at [github.com/wailsapp/wails/fork](https://github.com/wailsapp/wails/fork)
Clone your fork:
```bash
git clone https://github.com/YOUR_USERNAME/wails.git
cd wails
git remote add upstream https://github.com/wailsapp/wails.git
```
3. **Setup Documentation Environment**
The docs are in `/docs` and built with Astro:
```bash
cd docs
npm install
npm run dev
```
Open http://localhost:4321/ to preview changes live.
4. **Create a Documentation Branch**
Create a branch for your changes:
```bash
git checkout -b docs/improve-window-api-examples
```
5. **Make Your Changes**
Documentation files are in `/docs/src/content/docs/`:
```bash
# Edit MDX files
# Check the preview in your browser
# Ensure formatting is correct
```
**Best Practices:**
- Use clear, concise language
- Include practical code examples
- Add links to related sections
- Check spelling and grammar
- Test all code examples
6. **Verify Your Changes**
Check the live preview and ensure:
- Links work correctly
- Code examples are accurate
- Formatting renders properly
- No broken images or references
7. **Commit Documentation Changes**
Commit with clear message:
```bash
git commit -m "docs: add practical examples to Window API guide
- Add window positioning examples
- Include common patterns section
- Fix broken links to Event API"
```
8. **Submit Pull Request**
Push and create PR:
```bash
git push origin docs/improve-window-api-examples
```
In your PR:
- Describe what docs you improved
- Explain why the change helps users
- Include screenshots if visual changes
9. **Address Review Feedback**
Documentation PRs are usually quick to review and merge!
</Steps>
</TabItem>
</Tabs>
## Finding Issues to Work On
- Look for [`good first issue`](https://github.com/wailsapp/wails/labels/good%20first%20issue) labels
- Check [`help wanted`](https://github.com/wailsapp/wails/labels/help%20wanted) issues
- Browse [open issues](https://github.com/wailsapp/wails/issues) and ask to be assigned
## Getting Help
- **Discord:** Join [Wails Discord](https://discord.gg/JDdSxwjhGf)
- **Discussions:** Post in [GitHub Discussions](https://github.com/wailsapp/wails/discussions)
- **Issues:** Open an issue if you find a bug or have a question
## Code of Conduct
Be respectful, constructive, and welcoming. We're building a friendly community focused on creating great software together.
## Next Steps
- Set up your [Development Environment](/contributing/setup)
- Review our [Coding Standards](/contributing/standards)
- Explore the [Technical Documentation](/contributing)

View file

@ -6,7 +6,6 @@ sidebar:
---
import { Card, CardGrid } from "@astrojs/starlight/components";
import Mermaid from "../../components/Mermaid.astro";
## Welcome to the Wails v3 Technical Documentation
@ -50,47 +49,15 @@ context you need.
## Architectural Overview
<Mermaid title="Wails v3 End-to-End Flow">
```mermaid
flowchart TD
subgraph Developer Environment
CLI[wails3 CLI<br/>Init · Dev · Build · Package]
end
**Wails v3 End-to-End Flow**
subgraph Build-Time
GEN[Binding System<br/>(Static Analysis & Codegen)]
ASSET[Asset Server<br/>(Dev Proxy · Embed FS)]
PKG[Build & Packaging<br/>Pipeline]
end
{/*
TODO: Fix D2 diagram generation (triple quotes for multi-line strings) or embed as image.
The previous D2 code block was causing MDX parsing errors in the build pipeline.
*/}
subgraph Runtime
RUNTIME[Desktop Runtime<br/>(Window · Events · Dialogs)]
BIND[Bridge<br/>(Message Processor)]
end
**[End-to-End Flow Diagram Placeholder]**
subgraph Application
GO[Go Backend<br/>(App Logic)]
WEB[Web Frontend<br/>(React/Vue/...)]
end
%% Relationships
CLI --> |"generate"| GEN
CLI --> |"dev / build"| ASSET
CLI --> |"compile & package"| PKG
GEN --> |"Code Stubs + TS"| GO
GEN --> |"Bindings JSON"| WEB
PKG --> |"Final Binary + Installer"| GO
GO --> |"Function Calls"| BIND
WEB --> |"Invoke / Events"| BIND
RUNTIME <-->|native messages| BIND
RUNTIME --> |"Display Assets"| ASSET
WEB <-->|HTTP / In-Memory| ASSET
```
</Mermaid>
The diagram shows the **end-to-end flow**:
@ -120,7 +87,7 @@ diagrams, and references to the relevant source files.
---
:::note
Prerequisites: You should be comfortable with **Go 1.23+**, basic TypeScript,
Prerequisites: You should be comfortable with **Go 1.25+**, basic TypeScript,
and modern frontend build tools. If you are new to Go, consider skimming the
official tour first.
:::

View file

@ -18,7 +18,7 @@ source code.
|-------|-----------|--------------|
| **Bootstrap** | `pkg/application/application.go:init()` | Registers build-time data, creates a global `application` singleton. |
| **New()** | `application.New(...)` | Validates `Options`, spins up the **AssetServer**, initialises logging. |
| **Run()** | `application.(*App).Run()` | 1. Calls platform `mainthread.X()` to enter the OS UI thread.<br>2. Boots the **runtime** (`internal/runtime`).<br>3. Blocks until the last window closes or `Quit()` is called. |
| **Run()** | `application.(*App).Run()` | 1. Calls platform `mainthread.X()` to enter the OS UI thread.<br />2. Boots the **runtime** (`internal/runtime`).<br />3. Blocks until the last window closes or `Quit()` is called. |
| **Shutdown** | `application.(*App).Quit()` | Broadcasts `application:shutdown` event, flushes log, tears down windows & services. |
The lifecycle is strictly **single-entry**: you may create many windows, but the

View file

@ -0,0 +1,297 @@
---
title: Development Setup
description: Set up your development environment for Wails v3 development
---
## Development Environment Setup
This guide walks you through setting up a complete development environment for working on Wails v3.
## Required Tools
### Go Development
1. **Install Go 1.25 or later:**
```bash
# Download from https://go.dev/dl/
go version # Verify installation
```
2. **Configure Go environment:**
```bash
# Add to your shell profile (.bashrc, .zshrc, etc.)
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
```
3. **Install useful Go tools:**
```bash
go install golang.org/x/tools/cmd/goimports@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
```
### Node.js and npm
Required for building documentation and testing frontend integrations.
```bash
# Install Node.js 20+ and npm
node --version # Should be 20+
npm --version
```
### Platform-Specific Dependencies
**macOS:**
```bash
# Install Xcode Command Line Tools
xcode-select --install
# Verify installation
xcode-select -p # Should output a path
```
**Windows:**
1. Install [MSYS2](https://www.msys2.org/) for a Unix-like environment
2. WebView2 Runtime (pre-installed on Windows 11, [download](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) for Windows 10)
3. Optional: Install [Git for Windows](https://git-scm.com/download/win)
**Linux (Debian/Ubuntu):**
```bash
sudo apt update
sudo apt install build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.0-dev
```
**Linux (Fedora/RHEL):**
```bash
sudo dnf install gcc pkg-config gtk3-devel webkit2gtk3-devel
```
**Linux (Arch):**
```bash
sudo pacman -S base-devel gtk3 webkit2gtk
```
## Repository Setup
### Clone and Configure
```bash
# Clone your fork
git clone https://github.com/YOUR_USERNAME/wails.git
cd wails
# Add upstream remote
git remote add upstream https://github.com/wailsapp/wails.git
# Verify remotes
git remote -v
```
### Build the Wails CLI
```bash
# Navigate to v3 directory
cd v3
# Build the CLI
go build -o ../wails3 ./cmd/wails3
# Test the build
cd ..
./wails3 version
```
### Add to PATH (Optional)
**Linux/macOS:**
```bash
# Add to ~/.bashrc or ~/.zshrc
export PATH=$PATH:/path/to/wails
```
**Windows:**
Add the Wails directory to your PATH environment variable through System Properties.
## IDE Setup
### VS Code (Recommended)
1. **Install VS Code:** [Download](https://code.visualstudio.com/)
2. **Install extensions:**
- Go (by Go Team at Google)
- ESLint
- Prettier
- MDX (for documentation)
3. **Configure workspace settings** (`.vscode/settings.json`):
```json
{
"go.useLanguageServer": true,
"go.lintTool": "golangci-lint",
"go.lintOnSave": "workspace",
"editor.formatOnSave": true,
"go.formatTool": "goimports"
}
```
### GoLand
1. **Install GoLand:** [Download](https://www.jetbrains.com/go/)
2. **Configure:**
- Enable Go modules support
- Set up file watchers for `goimports`
- Configure code style to match project conventions
## Verify Your Setup
Run these commands to verify everything is working:
```bash
# Go version check
go version
# Build Wails
cd v3
go build ./cmd/wails3
# Run tests
go test ./pkg/...
# Create a test app
cd ..
./wails3 init -n mytest -t vanilla
cd mytest
../wails3 dev
```
If the test app builds and runs, your environment is ready!
## Running Tests
### Unit Tests
```bash
cd v3
go test ./...
```
### Specific Package Tests
```bash
go test ./pkg/application
go test ./pkg/events -v # Verbose output
```
### Run with Coverage
```bash
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out
```
### Run with Race Detector
```bash
go test ./... -race
```
## Working with Documentation
The Wails documentation is built with Astro and Starlight.
```bash
cd docs
# Install dependencies
npm install
# Start dev server
npm run dev
# Build for production
npm run build
```
Documentation will be available at `http://localhost:4321/`
## Debugging
### Debugging Go Code
**VS Code:**
Create `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Wails CLI",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/v3/cmd/wails3",
"args": ["dev"]
}
]
}
```
**Command Line:**
```bash
# Use Delve debugger
go install github.com/go-delve/delve/cmd/dlv@latest
dlv debug ./cmd/wails3 -- dev
```
### Debugging Platform Code
Platform-specific debugging requires platform tools:
- **macOS:** Xcode Instruments
- **Windows:** Visual Studio Debugger
- **Linux:** GDB
## Common Issues
### "command not found: wails3"
Add the Wails directory to your PATH or use `./wails3` from the project root.
### "webkit2gtk not found" (Linux)
Install WebKit2GTK development packages:
```bash
sudo apt install libwebkit2gtk-4.0-dev # Debian/Ubuntu
```
### Build fails with Go module errors
```bash
cd v3
go mod tidy
go mod download
```
### "CGO_ENABLED" errors on Windows
Ensure you have a C compiler (MinGW-w64 via MSYS2) in your PATH.
## Next Steps
- Review [Coding Standards](/contributing/standards)
- Explore the [Technical Documentation](/contributing)
- Find an issue to work on: [Good First Issues](https://github.com/wailsapp/wails/labels/good%20first%20issue)

View file

@ -0,0 +1,465 @@
---
title: Coding Standards
description: Code style, conventions, and best practices for Wails v3
---
## Code Style and Conventions
Following consistent coding standards makes the codebase easier to read, maintain, and contribute to.
## Go Code Standards
### Code Formatting
Use standard Go formatting tools:
```bash
# Format all code
gofmt -w .
# Use goimports for import organization
goimports -w .
```
**Required:** All Go code must pass `gofmt` and `goimports` before committing.
### Naming Conventions
**Packages:**
- Lowercase, single word when possible
- `package application`, `package events`
- Avoid underscores or mixed caps
**Exported Names:**
- PascalCase for types, functions, constants
- `type WebviewWindow struct`, `func NewApplication()`
**Unexported Names:**
- camelCase for internal types, functions, variables
- `type windowImpl struct`, `func createWindow()`
**Interfaces:**
- Name by behavior: `Reader`, `Writer`, `Handler`
- Single-method interfaces: name with `-er` suffix
```go
// Good
type Closer interface {
Close() error
}
// Avoid
type CloseInterface interface {
Close() error
}
```
### Error Handling
**Always check errors:**
```go
// Good
result, err := doSomething()
if err != nil {
return fmt.Errorf("failed to do something: %w", err)
}
// Bad - ignoring errors
result, _ := doSomething()
```
**Use error wrapping:**
```go
// Wrap errors to provide context
if err := validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
```
**Create custom error types when needed:**
```go
type ValidationError struct {
Field string
Value string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid value %q for field %q", e.Value, e.Field)
}
```
### Comments and Documentation
**Package comments:**
```go
// Package application provides the core Wails application runtime.
//
// It handles window management, event dispatching, and service lifecycle.
package application
```
**Exported declarations:**
```go
// NewApplication creates a new Wails application with the given options.
//
// The application must be started with Run() or RunWithContext().
func NewApplication(opts Options) *Application {
// ...
}
```
**Implementation comments:**
```go
// processEvent handles incoming events from the runtime.
// It dispatches to registered handlers and manages event lifecycle.
func (a *Application) processEvent(event *Event) {
// Validate event before processing
if event == nil {
return
}
// Find and invoke handlers
// ...
}
```
### Function and Method Structure
**Keep functions focused:**
```go
// Good - single responsibility
func (w *Window) setTitle(title string) {
w.title = title
w.updateNativeTitle()
}
// Bad - doing too much
func (w *Window) updateEverything() {
w.setTitle(w.title)
w.setSize(w.width, w.height)
w.setPosition(w.x, w.y)
// ... 20 more operations
}
```
**Use early returns:**
```go
// Good
func validate(input string) error {
if input == "" {
return errors.New("empty input")
}
if len(input) > 100 {
return errors.New("input too long")
}
return nil
}
// Avoid deep nesting
```
### Concurrency
**Use context for cancellation:**
```go
func (a *Application) RunWithContext(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-a.done:
return nil
}
}
```
**Protect shared state with mutexes:**
```go
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
```
**Avoid goroutine leaks:**
```go
// Good - goroutine has exit condition
func (a *Application) startWorker(ctx context.Context) {
go func() {
for {
select {
case <-ctx.Done():
return // Clean exit
case work := <-a.workChan:
a.process(work)
}
}
}()
}
```
### Testing
**Test file naming:**
```go
// Implementation: window.go
// Tests: window_test.go
```
**Table-driven tests:**
```go
func TestValidate(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"empty input", "", true},
{"valid input", "hello", false},
{"too long", strings.Repeat("a", 101), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validate(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
```
## JavaScript/TypeScript Standards
### Code Formatting
Use Prettier for consistent formatting:
```json
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}
```
### Naming Conventions
**Variables and functions:**
- camelCase: `const userName = "John"`
**Classes and types:**
- PascalCase: `class WindowManager`
**Constants:**
- UPPER_SNAKE_CASE: `const MAX_RETRIES = 3`
### TypeScript
**Use explicit types:**
```typescript
// Good
function greet(name: string): string {
return `Hello, ${name}`
}
// Avoid implicit any
function process(data) { // Bad
return data
}
```
**Define interfaces:**
```typescript
interface WindowOptions {
title: string
width: number
height: number
}
function createWindow(options: WindowOptions): void {
// ...
}
```
## Commit Message Format
Use [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>(<scope>): <subject>
<body>
<footer>
```
**Types:**
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `refactor`: Code refactoring
- `test`: Adding or updating tests
- `chore`: Maintenance tasks
**Examples:**
```
feat(window): add SetAlwaysOnTop method
Implement SetAlwaysOnTop for keeping windows above others.
Adds platform implementations for macOS, Windows, and Linux.
Closes #123
```
```
fix(events): prevent event handler memory leak
Event listeners were not being properly cleaned up when
windows were closed. This adds explicit cleanup in the
window destructor.
```
## Pull Request Guidelines
### Before Submitting
- [ ] Code passes `gofmt` and `goimports`
- [ ] All tests pass (`go test ./...`)
- [ ] New code has tests
- [ ] Documentation updated if needed
- [ ] Commit messages follow conventions
- [ ] No merge conflicts with `master`
### PR Description Template
```markdown
## Description
Brief description of what this PR does.
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
How was this tested?
## Checklist
- [ ] Tests pass
- [ ] Documentation updated
- [ ] No breaking changes (or documented)
```
## Code Review Process
### As a Reviewer
- Be constructive and respectful
- Focus on code quality, not personal preferences
- Explain why changes are suggested
- Approve once satisfied
### As an Author
- Respond to all comments
- Ask for clarification if needed
- Make requested changes or explain why not
- Be open to feedback
## Best Practices
### Performance
- Avoid premature optimization
- Profile before optimizing
- Use benchmarks for performance-critical code
```go
func BenchmarkProcess(b *testing.B) {
for i := 0; i < b.N; i++ {
process(testData)
}
}
```
### Security
- Validate all user input
- Sanitize data before display
- Use `crypto/rand` for random data
- Never log sensitive information
### Documentation
- Document exported APIs
- Include examples in documentation
- Update docs when changing APIs
- Keep README files current
## Platform-Specific Code
### File Naming
```
window.go // Common interface
window_darwin.go // macOS implementation
window_windows.go // Windows implementation
window_linux.go // Linux implementation
```
### Build Tags
```go
//go:build darwin
package application
// macOS-specific code
```
## Linting
Run linters before committing:
```bash
# golangci-lint (recommended)
golangci-lint run
# Individual linters
go vet ./...
staticcheck ./...
```
## Questions?
If you're unsure about any standards:
- Check existing code for examples
- Ask in [Discord](https://discord.gg/JDdSxwjhGf)
- Open a discussion on GitHub

View file

@ -133,7 +133,7 @@ Key features:
* **Matrix** over `os: [ubuntu-latest, windows-latest, macos-latest]`
* Caches Go & npm deps for speed
* Steps:
1. `setup-go` @ 1.23
1. `setup-go` @ 1.25
2. `go vet ./...`
3. `go test ./... -v -coverprofile=cover.out`
4. Build CLI + sample app (`wails3 build -skip-package`)

View file

@ -0,0 +1,260 @@
---
title: Frequently Asked Questions
description: Common questions about Wails
sidebar:
order: 99
---
## General
### What is Wails?
Wails is a framework for building desktop applications using Go and web technologies. It provides native OS integration whilst allowing you to build your UI with HTML, CSS, and JavaScript.
### How does Wails compare to Electron?
**Wails:**
- ~10MB memory usage
- ~15MB binary size
- Native performance
- Go backend
**Electron:**
- ~100MB+ memory usage
- ~150MB+ binary size
- Chromium overhead
- Node.js backend
### Is Wails production-ready?
Yes! Wails v3 is suitable for production applications. Many companies use Wails for their desktop applications.
### What platforms does Wails support?
- Windows (7+)
- macOS (10.13+)
- Linux (GTK3)
## Development
### Do I need to know Go?
Basic Go knowledge is helpful but not required. You can start with simple services and learn as you go.
### Can I use my favourite frontend framework?
Yes! Wails works with:
- Vanilla JavaScript
- React
- Vue
- Svelte
- Any framework that builds to HTML/CSS/JS
### How do I call Go functions from JavaScript?
Use bindings:
```go
// Go
func (s *Service) Greet(name string) string {
return "Hello " + name
}
```
```javascript
// JavaScript
import { Greet } from './bindings/myapp/service'
const message = await Greet("World")
```
### Can I use TypeScript?
Yes! Wails generates TypeScript definitions automatically.
### How do I debug my application?
Use your browser's dev tools:
- Right-click → Inspect Element
- Or enable dev tools in window options
## Building & Distribution
### How do I build for production?
```bash
wails3 build
```
Your application will be in `build/bin/`.
### Can I cross-compile?
Yes! Build for other platforms:
```bash
wails3 build -platform windows/amd64
wails3 build -platform darwin/universal
wails3 build -platform linux/amd64
```
### How do I create an installer?
Use platform-specific tools:
- Windows: NSIS or WiX
- macOS: Create DMG
- Linux: DEB/RPM packages
See the [Installers Guide](/guides/installers).
### How do I code sign my application?
**macOS:**
```bash
codesign --deep --force --sign "Developer ID" MyApp.app
```
**Windows:**
Use SignTool with your certificate.
## Features
### Can I create multiple windows?
Yes! Wails v3 has native multi-window support:
```go
window1 := app.NewWebviewWindow()
window2 := app.NewWebviewWindow()
```
### Does Wails support system tray?
Yes! Create system tray applications:
```go
tray := app.NewSystemTray()
tray.SetIcon(iconBytes)
tray.SetMenu(menu)
```
### Can I use native dialogs?
Yes! Wails provides native dialogs:
```go
path, _ := app.OpenFileDialog().
SetTitle("Select File").
PromptForSingleSelection()
```
### Does Wails support auto-updates?
Wails doesn't include auto-update functionality, but you can implement it yourself or use third-party libraries.
## Performance
### Why is my binary large?
Go binaries include the runtime. Reduce size:
```bash
wails3 build -ldflags "-s -w"
```
### How do I improve performance?
- Paginate large datasets
- Cache expensive operations
- Use events for updates
- Optimise frontend bundle
- Profile your code
See the [Performance Guide](/guides/performance).
### Does Wails support hot reload?
Yes! Use dev mode:
```bash
wails3 dev
```
## Troubleshooting
### My bindings aren't working
Regenerate bindings:
```bash
wails3 generate bindings
```
### Window doesn't appear
Check if you called `Show()`:
```go
window := app.NewWebviewWindow()
window.Show() // Don't forget this!
```
### Events not firing
Ensure event names match exactly:
```go
// Go
app.EmitEvent("my-event", data)
// JavaScript
OnEvent("my-event", handler) // Must match
```
### Build fails
Common fixes:
- Run `go mod tidy`
- Check `wails.json` configuration
- Verify frontend builds: `cd frontend && npm run build`
- Update Wails: `go install github.com/wailsapp/wails/v3/cmd/wails3@latest`
## Migration
### Should I migrate from v2 to v3?
v3 offers:
- Better performance
- Multi-window support
- Improved API
- Better developer experience
See the [Migration Guide](/migration/v2-to-v3).
### Will v2 be maintained?
Yes, v2 will receive critical updates.
### Can I run v2 and v3 side by side?
Yes, they use different import paths.
## Community
### How do I get help?
- [Discord Community](https://discord.gg/JDdSxwjhGf)
- [GitHub Discussions](https://github.com/wailsapp/wails/discussions)
- [GitHub Issues](https://github.com/wailsapp/wails/issues)
### How do I contribute?
See the [Contributing Guide](/contributing).
### Where can I find examples?
- [Official Examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples)
- [Community Examples](https://github.com/topics/wails)
## Still Have Questions?
Ask in [Discord](https://discord.gg/JDdSxwjhGf) or [open a discussion](https://github.com/wailsapp/wails/discussions).

View file

@ -1,7 +1,8 @@
---
title: Advanced Binding Techniques
title: Advanced Binding
description: Advanced binding techniques including directives, code injection, and custom IDs
sidebar:
order: 22
order: 3
---
import { FileTree } from "@astrojs/starlight/components";

View file

@ -0,0 +1,688 @@
---
title: Bindings Best Practices
description: Design patterns and best practices for Go-JavaScript bindings
sidebar:
order: 4
---
## Bindings Best Practices
Follow **proven patterns** for binding design to create clean, performant, and secure bindings. This guide covers API design principles, performance optimisation, security patterns, error handling, and testing strategies for maintainable applications.
## API Design Principles
### 1. Single Responsibility
Each service should have one clear purpose:
```go
// ❌ Bad: God object
type AppService struct {
// Does everything
}
func (a *AppService) SaveFile(path string, data []byte) error
func (a *AppService) GetUser(id int) (*User, error)
func (a *AppService) SendEmail(to, subject, body string) error
func (a *AppService) ProcessPayment(amount float64) error
// ✅ Good: Focused services
type FileService struct{}
func (f *FileService) Save(path string, data []byte) error
type UserService struct{}
func (u *UserService) GetByID(id int) (*User, error)
type EmailService struct{}
func (e *EmailService) Send(to, subject, body string) error
type PaymentService struct{}
func (p *PaymentService) Process(amount float64) error
```
### 2. Clear Method Names
Use descriptive, action-oriented names:
```go
// ❌ Bad: Unclear names
func (s *Service) Do(x string) error
func (s *Service) Handle(data interface{}) interface{}
func (s *Service) Process(input map[string]interface{}) bool
// ✅ Good: Clear names
func (s *FileService) SaveDocument(path string, content string) error
func (s *UserService) AuthenticateUser(email, password string) (*User, error)
func (s *OrderService) CreateOrder(items []Item) (*Order, error)
```
### 3. Consistent Return Types
Always return errors explicitly:
```go
// ❌ Bad: Inconsistent error handling
func (s *Service) GetData() interface{} // How to handle errors?
func (s *Service) SaveData(data string) // Silent failures?
// ✅ Good: Explicit errors
func (s *Service) GetData() (Data, error)
func (s *Service) SaveData(data string) error
```
### 4. Input Validation
Validate all input on the Go side:
```go
// ❌ Bad: No validation
func (s *UserService) CreateUser(email, password string) (*User, error) {
user := &User{Email: email, Password: password}
return s.db.Create(user)
}
// ✅ Good: Validate first
func (s *UserService) CreateUser(email, password string) (*User, error) {
// Validate email
if !isValidEmail(email) {
return nil, errors.New("invalid email address")
}
// Validate password
if len(password) < 8 {
return nil, errors.New("password must be at least 8 characters")
}
// Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := &User{
Email: email,
PasswordHash: string(hash),
}
return s.db.Create(user)
}
```
## Performance Patterns
### 1. Batch Operations
Reduce bridge calls by batching:
```go
// ❌ Bad: N calls
// JavaScript
for (const item of items) {
await ProcessItem(item) // N bridge calls
}
// ✅ Good: 1 call
// Go
func (s *Service) ProcessItems(items []Item) ([]Result, error) {
results := make([]Result, len(items))
for i, item := range items {
results[i] = s.processItem(item)
}
return results, nil
}
// JavaScript
const results = await ProcessItems(items) // 1 bridge call
```
### 2. Pagination
Don't return huge datasets:
```go
// ❌ Bad: Returns everything
func (s *Service) GetAllUsers() ([]User, error) {
return s.db.FindAll() // Could be millions
}
// ✅ Good: Paginated
type PageRequest struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type PageResponse struct {
Items []User `json:"items"`
TotalItems int `json:"totalItems"`
TotalPages int `json:"totalPages"`
Page int `json:"page"`
}
func (s *Service) GetUsers(req PageRequest) (*PageResponse, error) {
// Validate
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 20
}
// Get total
total, err := s.db.Count()
if err != nil {
return nil, err
}
// Get page
offset := (req.Page - 1) * req.PageSize
users, err := s.db.Find(offset, req.PageSize)
if err != nil {
return nil, err
}
return &PageResponse{
Items: users,
TotalItems: total,
TotalPages: (total + req.PageSize - 1) / req.PageSize,
Page: req.Page,
}, nil
}
```
### 3. Caching
Cache expensive operations:
```go
type CachedService struct {
cache map[string]interface{}
mu sync.RWMutex
ttl time.Duration
}
func (s *CachedService) GetData(key string) (interface{}, error) {
// Check cache
s.mu.RLock()
if data, ok := s.cache[key]; ok {
s.mu.RUnlock()
return data, nil
}
s.mu.RUnlock()
// Fetch data
data, err := s.fetchData(key)
if err != nil {
return nil, err
}
// Cache it
s.mu.Lock()
s.cache[key] = data
s.mu.Unlock()
// Schedule expiry
go func() {
time.Sleep(s.ttl)
s.mu.Lock()
delete(s.cache, key)
s.mu.Unlock()
}()
return data, nil
}
```
### 4. Streaming with Events
Use events for streaming data:
```go
// ❌ Bad: Polling
func (s *Service) GetProgress() int {
return s.progress
}
// JavaScript polls
setInterval(async () => {
const progress = await GetProgress()
updateUI(progress)
}, 100)
// ✅ Good: Events
func (s *Service) ProcessLargeFile(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
total := 0
processed := 0
// Count lines
for scanner.Scan() {
total++
}
// Process
file.Seek(0, 0)
scanner = bufio.NewScanner(file)
for scanner.Scan() {
s.processLine(scanner.Text())
processed++
// Emit progress
s.app.EmitEvent("progress", map[string]interface{}{
"processed": processed,
"total": total,
"percent": int(float64(processed) / float64(total) * 100),
})
}
return scanner.Err()
}
// JavaScript listens
OnEvent("progress", (data) => {
updateProgress(data.percent)
})
```
## Security Patterns
### 1. Input Sanitisation
Always sanitise user input:
```go
import (
"html"
"strings"
)
func (s *Service) SaveComment(text string) error {
// Sanitise
text = strings.TrimSpace(text)
text = html.EscapeString(text)
// Validate length
if len(text) == 0 {
return errors.New("comment cannot be empty")
}
if len(text) > 1000 {
return errors.New("comment too long")
}
return s.db.SaveComment(text)
}
```
### 2. Authentication
Protect sensitive operations:
```go
type AuthService struct {
sessions map[string]*Session
mu sync.RWMutex
}
func (a *AuthService) Login(email, password string) (string, error) {
user, err := a.db.FindByEmail(email)
if err != nil {
return "", errors.New("invalid credentials")
}
if !a.verifyPassword(user.PasswordHash, password) {
return "", errors.New("invalid credentials")
}
// Create session
token := generateToken()
a.mu.Lock()
a.sessions[token] = &Session{
UserID: user.ID,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
a.mu.Unlock()
return token, nil
}
func (a *AuthService) requireAuth(token string) (*Session, error) {
a.mu.RLock()
session, ok := a.sessions[token]
a.mu.RUnlock()
if !ok {
return nil, errors.New("not authenticated")
}
if time.Now().After(session.ExpiresAt) {
return nil, errors.New("session expired")
}
return session, nil
}
// Protected method
func (a *AuthService) DeleteAccount(token string) error {
session, err := a.requireAuth(token)
if err != nil {
return err
}
return a.db.DeleteUser(session.UserID)
}
```
### 3. Rate Limiting
Prevent abuse:
```go
type RateLimiter struct {
requests map[string][]time.Time
mu sync.Mutex
limit int
window time.Duration
}
func (r *RateLimiter) Allow(key string) bool {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
// Clean old requests
if requests, ok := r.requests[key]; ok {
var recent []time.Time
for _, t := range requests {
if now.Sub(t) < r.window {
recent = append(recent, t)
}
}
r.requests[key] = recent
}
// Check limit
if len(r.requests[key]) >= r.limit {
return false
}
// Add request
r.requests[key] = append(r.requests[key], now)
return true
}
// Usage
func (s *Service) SendEmail(to, subject, body string) error {
if !s.rateLimiter.Allow(to) {
return errors.New("rate limit exceeded")
}
return s.emailer.Send(to, subject, body)
}
```
## Error Handling Patterns
### 1. Descriptive Errors
Provide context in errors:
```go
// ❌ Bad: Generic errors
func (s *Service) LoadFile(path string) ([]byte, error) {
return os.ReadFile(path) // "no such file or directory"
}
// ✅ Good: Contextual errors
func (s *Service) LoadFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to load file %s: %w", path, err)
}
return data, nil
}
```
### 2. Error Types
Use typed errors for specific handling:
```go
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
type NotFoundError struct {
Resource string
ID interface{}
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s not found: %v", e.Resource, e.ID)
}
// Usage
func (s *UserService) GetUser(id int) (*User, error) {
if id <= 0 {
return nil, &ValidationError{
Field: "id",
Message: "must be positive",
}
}
user, err := s.db.Find(id)
if err == sql.ErrNoRows {
return nil, &NotFoundError{
Resource: "User",
ID: id,
}
}
if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}
return user, nil
}
```
### 3. Error Recovery
Handle errors gracefully:
```go
func (s *Service) ProcessWithRetry(data string) error {
maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ {
err := s.process(data)
if err == nil {
return nil
}
// Log attempt
s.app.Logger.Warn("Process failed",
"attempt", attempt,
"error", err)
// Don't retry on validation errors
if _, ok := err.(*ValidationError); ok {
return err
}
// Wait before retry
if attempt < maxRetries {
time.Sleep(time.Duration(attempt) * time.Second)
}
}
return fmt.Errorf("failed after %d attempts", maxRetries)
}
```
## Testing Patterns
### 1. Unit Testing
Test services in isolation:
```go
func TestUserService_CreateUser(t *testing.T) {
// Setup
db := &MockDB{}
service := &UserService{db: db}
// Test valid input
user, err := service.CreateUser("test@example.com", "password123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Email != "test@example.com" {
t.Errorf("expected email test@example.com, got %s", user.Email)
}
// Test invalid email
_, err = service.CreateUser("invalid", "password123")
if err == nil {
t.Error("expected error for invalid email")
}
// Test short password
_, err = service.CreateUser("test@example.com", "short")
if err == nil {
t.Error("expected error for short password")
}
}
```
### 2. Integration Testing
Test with real dependencies:
```go
func TestUserService_Integration(t *testing.T) {
// Setup real database
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
// Create schema
_, err = db.Exec(`CREATE TABLE users (...)`)
if err != nil {
t.Fatal(err)
}
// Test service
service := &UserService{db: db}
user, err := service.CreateUser("test@example.com", "password123")
if err != nil {
t.Fatal(err)
}
// Verify in database
var count int
db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?",
user.Email).Scan(&count)
if count != 1 {
t.Errorf("expected 1 user, got %d", count)
}
}
```
### 3. Mock Services
Create testable interfaces:
```go
type UserRepository interface {
Create(user *User) error
FindByEmail(email string) (*User, error)
Update(user *User) error
Delete(id int) error
}
type UserService struct {
repo UserRepository
}
// Mock for testing
type MockUserRepository struct {
users map[string]*User
}
func (m *MockUserRepository) Create(user *User) error {
m.users[user.Email] = user
return nil
}
// Test with mock
func TestUserService_WithMock(t *testing.T) {
mock := &MockUserRepository{
users: make(map[string]*User),
}
service := &UserService{repo: mock}
// Test
user, err := service.CreateUser("test@example.com", "password123")
if err != nil {
t.Fatal(err)
}
// Verify mock was called
if len(mock.users) != 1 {
t.Error("expected 1 user in mock")
}
}
```
## Best Practices Summary
### ✅ Do
- **Single responsibility** - One service, one purpose
- **Clear naming** - Descriptive method names
- **Validate input** - Always on Go side
- **Return errors** - Explicit error handling
- **Batch operations** - Reduce bridge calls
- **Use events** - For streaming data
- **Sanitise input** - Prevent injection
- **Test thoroughly** - Unit and integration tests
- **Document methods** - Comments become JSDoc
- **Version your API** - Plan for changes
### ❌ Don't
- **Don't create god objects** - Keep services focused
- **Don't trust frontend** - Validate everything
- **Don't return huge datasets** - Use pagination
- **Don't block** - Use goroutines for long operations
- **Don't ignore errors** - Handle all error cases
- **Don't skip testing** - Test early and often
- **Don't hardcode** - Use configuration
- **Don't expose internals** - Keep implementation private
## Next Steps
- [Methods](/features/bindings/methods) - Learn method binding basics
- [Services](/features/bindings/services) - Understand service architecture
- [Models](/features/bindings/models) - Bind complex data structures
- [Events](/features/events/system) - Use events for pub/sub
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [binding examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/binding).

View file

@ -0,0 +1,643 @@
---
title: Method Bindings
description: Call Go methods from JavaScript with type safety
sidebar:
order: 1
---
import { FileTree, Card, CardGrid } from "@astrojs/starlight/components";
## Type-Safe Go-JavaScript Bindings
Wails **automatically generates type-safe JavaScript/TypeScript bindings** for your Go methods. Write Go code, run one command, and get fully-typed frontend functions with no HTTP overhead, no manual work, and zero boilerplate.
## Quick Start
**1. Write Go service:**
```go
type GreetService struct{}
func (g *GreetService) Greet(name string) string {
return "Hello, " + name + "!"
}
```
**2. Register service:**
```go
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&GreetService{}),
},
})
```
**3. Generate bindings:**
```bash
wails3 generate bindings
```
**4. Use in JavaScript:**
```javascript
import { Greet } from './bindings/myapp/greetservice'
const message = await Greet("World")
console.log(message) // "Hello, World!"
```
**That's it!** Type-safe Go-to-JavaScript calls.
## Creating Services
### Basic Service
```go
package main
import "github.com/wailsapp/wails/v3/pkg/application"
type CalculatorService struct{}
func (c *CalculatorService) Add(a, b int) int {
return a + b
}
func (c *CalculatorService) Subtract(a, b int) int {
return a - b
}
func (c *CalculatorService) Multiply(a, b int) int {
return a * b
}
func (c *CalculatorService) Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
```
**Register:**
```go
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&CalculatorService{}),
},
})
```
**Key points:**
- Only **exported methods** (PascalCase) are bound
- Methods can return values or `(value, error)`
- Services are **singletons** (one instance per application)
### Service with State
```go
type CounterService struct {
count int
mu sync.Mutex
}
func (c *CounterService) Increment() int {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
return c.count
}
func (c *CounterService) Decrement() int {
c.mu.Lock()
defer c.mu.Unlock()
c.count--
return c.count
}
func (c *CounterService) GetCount() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.count
}
func (c *CounterService) Reset() {
c.mu.Lock()
defer c.mu.Unlock()
c.count = 0
}
```
**Important:** Services are shared across all windows. Use mutexes for thread safety.
### Service with Dependencies
```go
type DatabaseService struct {
db *sql.DB
}
func NewDatabaseService(db *sql.DB) *DatabaseService {
return &DatabaseService{db: db}
}
func (d *DatabaseService) GetUser(id int) (*User, error) {
var user User
err := d.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
return &user, err
}
```
**Register with dependencies:**
```go
db, _ := sql.Open("sqlite3", "app.db")
app := application.New(application.Options{
Services: []application.Service{
application.NewService(NewDatabaseService(db)),
},
})
```
## Generating Bindings
### Basic Generation
```bash
wails3 generate bindings
```
**Output:**
```
INFO 347 Packages, 3 Services, 12 Methods, 0 Enums, 0 Models in 1.98s
INFO Output directory: /myproject/frontend/bindings
```
**Generated structure:**
<FileTree>
- frontend/bindings
- myapp
- calculatorservice.js
- counterservice.js
- databaseservice.js
- index.js
</FileTree>
### TypeScript Generation
```bash
wails3 generate bindings -ts
```
**Generates `.ts` files** with full TypeScript types.
### Custom Output Directory
```bash
wails3 generate bindings -o ./src/bindings
```
### Watch Mode (Development)
```bash
wails3 dev
```
**Automatically regenerates bindings** when Go code changes.
## Using Bindings
### JavaScript
**Generated binding:**
```javascript
// frontend/bindings/myapp/calculatorservice.js
/**
* @param {number} a
* @param {number} b
* @returns {Promise<number>}
*/
export function Add(a, b) {
return window.wails.Call('CalculatorService.Add', a, b)
}
```
**Usage:**
```javascript
import { Add, Subtract, Multiply, Divide } from './bindings/myapp/calculatorservice'
// Simple calls
const sum = await Add(5, 3) // 8
const diff = await Subtract(10, 4) // 6
const product = await Multiply(7, 6) // 42
// Error handling
try {
const result = await Divide(10, 0)
} catch (error) {
console.error("Error:", error) // "division by zero"
}
```
### TypeScript
**Generated binding:**
```typescript
// frontend/bindings/myapp/calculatorservice.ts
export function Add(a: number, b: number): Promise<number>
export function Subtract(a: number, b: number): Promise<number>
export function Multiply(a: number, b: number): Promise<number>
export function Divide(a: number, b: number): Promise<number>
```
**Usage:**
```typescript
import { Add, Divide } from './bindings/myapp/calculatorservice'
const sum: number = await Add(5, 3)
try {
const result = await Divide(10, 0)
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message)
}
}
```
**Benefits:**
- Full type checking
- IDE autocomplete
- Compile-time errors
- Better refactoring
### Index Files
**Generated index:**
```javascript
// frontend/bindings/myapp/index.js
export * as CalculatorService from './calculatorservice.js'
export * as CounterService from './counterservice.js'
export * as DatabaseService from './databaseservice.js'
```
**Simplified imports:**
```javascript
import { CalculatorService } from './bindings/myapp'
const sum = await CalculatorService.Add(5, 3)
```
## Type Mapping
### Primitive Types
| Go Type | JavaScript/TypeScript |
|---------|----------------------|
| `string` | `string` |
| `bool` | `boolean` |
| `int`, `int8`, `int16`, `int32`, `int64` | `number` |
| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | `number` |
| `float32`, `float64` | `number` |
| `byte` | `number` |
| `rune` | `number` |
### Complex Types
| Go Type | JavaScript/TypeScript |
|---------|----------------------|
| `[]T` | `T[]` |
| `[N]T` | `T[]` |
| `map[string]T` | `Record<string, T>` |
| `map[K]V` | `Map<K, V>` |
| `struct` | `class` (with fields) |
| `time.Time` | `Date` |
| `*T` | `T` (pointers transparent) |
| `interface{}` | `any` |
| `error` | Exception (thrown) |
### Unsupported Types
These types **cannot** be passed across the bridge:
- `chan T` (channels)
- `func()` (functions)
- Complex interfaces (except `interface{}`)
- Unexported fields (lowercase)
**Workaround:** Use IDs or handles:
```go
// ❌ Can't return file handle
func OpenFile(path string) (*os.File, error)
// ✅ Return file ID instead
var files = make(map[string]*os.File)
func OpenFile(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
id := generateID()
files[id] = file
return id, nil
}
func ReadFile(id string) ([]byte, error) {
file := files[id]
return io.ReadAll(file)
}
func CloseFile(id string) error {
file := files[id]
delete(files, id)
return file.Close()
}
```
## Error Handling
### Go Side
```go
func (d *DatabaseService) GetUser(id int) (*User, error) {
if id <= 0 {
return nil, errors.New("invalid user ID")
}
var user User
err := d.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("user %d not found", id)
}
if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}
return &user, nil
}
```
### JavaScript Side
```javascript
import { GetUser } from './bindings/myapp/databaseservice'
try {
const user = await GetUser(123)
console.log("User:", user)
} catch (error) {
console.error("Error:", error)
// Error: "user 123 not found"
}
```
**Error types:**
- Go `error` → JavaScript exception
- Error message preserved
- Stack trace available
## Performance
### Call Overhead
**Typical call:** &lt;1ms
```
JavaScript → Bridge → Go → Bridge → JavaScript
↓ ↓ ↓ ↓ ↓
&lt;0.1ms &lt;0.1ms [varies] &lt;0.1ms &lt;0.1ms
```
**Compared to alternatives:**
- HTTP/REST: 5-50ms
- IPC: 1-10ms
- Wails: &lt;1ms
### Optimisation Tips
**✅ Batch operations:**
```javascript
// ❌ Slow: N calls
for (const item of items) {
await ProcessItem(item)
}
// ✅ Fast: 1 call
await ProcessItems(items)
```
**✅ Cache results:**
```javascript
// ❌ Repeated calls
const config1 = await GetConfig()
const config2 = await GetConfig()
// ✅ Cache
const config = await GetConfig()
// Use config multiple times
```
**✅ Use events for streaming:**
```go
func ProcessLargeFile(path string) error {
// Emit progress events
for line := range lines {
app.EmitEvent("progress", line)
}
return nil
}
```
## Complete Example
**Go:**
```go
package main
import (
"fmt"
"github.com/wailsapp/wails/v3/pkg/application"
)
type TodoService struct {
todos []Todo
}
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
func (t *TodoService) GetAll() []Todo {
return t.todos
}
func (t *TodoService) Add(title string) Todo {
todo := Todo{
ID: len(t.todos) + 1,
Title: title,
Completed: false,
}
t.todos = append(t.todos, todo)
return todo
}
func (t *TodoService) Toggle(id int) error {
for i := range t.todos {
if t.todos[i].ID == id {
t.todos[i].Completed = !t.todos[i].Completed
return nil
}
}
return fmt.Errorf("todo %d not found", id)
}
func (t *TodoService) Delete(id int) error {
for i := range t.todos {
if t.todos[i].ID == id {
t.todos = append(t.todos[:i], t.todos[i+1:]...)
return nil
}
}
return fmt.Errorf("todo %d not found", id)
}
func main() {
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&TodoService{}),
},
})
app.NewWebviewWindow()
app.Run()
}
```
**JavaScript:**
```javascript
import { GetAll, Add, Toggle, Delete } from './bindings/myapp/todoservice'
class TodoApp {
async loadTodos() {
const todos = await GetAll()
this.renderTodos(todos)
}
async addTodo(title) {
try {
const todo = await Add(title)
this.loadTodos()
} catch (error) {
console.error("Failed to add todo:", error)
}
}
async toggleTodo(id) {
try {
await Toggle(id)
this.loadTodos()
} catch (error) {
console.error("Failed to toggle todo:", error)
}
}
async deleteTodo(id) {
try {
await Delete(id)
this.loadTodos()
} catch (error) {
console.error("Failed to delete todo:", error)
}
}
renderTodos(todos) {
const list = document.getElementById('todo-list')
list.innerHTML = todos.map(todo => `
<div class="todo ${todo.Completed ? 'completed' : ''}">
<input type="checkbox"
${todo.Completed ? 'checked' : ''}
onchange="app.toggleTodo(${todo.ID})">
<span>${todo.Title}</span>
<button onclick="app.deleteTodo(${todo.ID})">Delete</button>
</div>
`).join('')
}
}
const app = new TodoApp()
app.loadTodos()
```
## Best Practices
### ✅ Do
- **Keep methods simple** - Single responsibility
- **Return errors** - Don't panic
- **Use thread-safe state** - Mutexes for shared data
- **Batch operations** - Reduce bridge calls
- **Cache on Go side** - Avoid repeated work
- **Document methods** - Comments become JSDoc
### ❌ Don't
- **Don't block** - Use goroutines for long operations
- **Don't return channels** - Use events instead
- **Don't return functions** - Not supported
- **Don't ignore errors** - Always handle them
- **Don't use unexported fields** - Won't be bound
## Next Steps
<CardGrid>
<Card title="Services" icon="puzzle">
Deep dive into the service system.
[Learn More →](/features/bindings/services)
</Card>
<Card title="Models" icon="document">
Bind complex data structures.
[Learn More →](/features/bindings/models)
</Card>
<Card title="Go-Frontend Bridge" icon="rocket">
Understand the bridge mechanism.
[Learn More →](/concepts/bridge)
</Card>
<Card title="Events" icon="star">
Use events for pub/sub communication.
[Learn More →](/features/events/system)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [binding examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/binding).

View file

@ -0,0 +1,741 @@
---
title: Data Models
description: Bind complex data structures between Go and JavaScript
sidebar:
order: 3
---
import { Card, CardGrid } from "@astrojs/starlight/components";
## Data Model Bindings
Wails **automatically generates JavaScript/TypeScript classes** from Go structs, providing full type safety when passing complex data between backend and frontend. Write Go structs, generate bindings, and get fully-typed frontend models complete with constructors, type annotations, and JSDoc comments.
## Quick Start
**Go struct:**
```go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"createdAt"`
}
func (s *UserService) GetUser(id int) (*User, error) {
// Return user
}
```
**Generate:**
```bash
wails3 generate bindings
```
**JavaScript:**
```javascript
import { GetUser } from './bindings/myapp/userservice'
import { User } from './bindings/myapp/models'
const user = await GetUser(1)
console.log(user.Name) // Type-safe!
```
**That's it!** Full type safety across the bridge.
## Defining Models
### Basic Struct
```go
type Person struct {
Name string
Age int
}
```
**Generated JavaScript:**
```javascript
export class Person {
/** @type {string} */
Name = ""
/** @type {number} */
Age = 0
constructor(source = {}) {
Object.assign(this, source)
}
static createFrom(source = {}) {
return new Person(source)
}
}
```
### With JSON Tags
```go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"createdAt"`
}
```
**Generated JavaScript:**
```javascript
export class User {
/** @type {number} */
id = 0
/** @type {string} */
name = ""
/** @type {string} */
email = ""
/** @type {Date} */
createdAt = new Date()
constructor(source = {}) {
Object.assign(this, source)
}
}
```
**JSON tags control field names** in JavaScript.
### With Comments
```go
// User represents an application user
type User struct {
// Unique identifier
ID int `json:"id"`
// Full name of the user
Name string `json:"name"`
// Email address (must be unique)
Email string `json:"email"`
}
```
**Generated JavaScript:**
```javascript
/**
* User represents an application user
*/
export class User {
/**
* Unique identifier
* @type {number}
*/
id = 0
/**
* Full name of the user
* @type {string}
*/
name = ""
/**
* Email address (must be unique)
* @type {string}
*/
email = ""
}
```
**Comments become JSDoc!** Your IDE shows them.
### Nested Structs
```go
type Address struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Address Address `json:"address"`
}
```
**Generated JavaScript:**
```javascript
export class Address {
/** @type {string} */
street = ""
/** @type {string} */
city = ""
/** @type {string} */
country = ""
}
export class User {
/** @type {number} */
id = 0
/** @type {string} */
name = ""
/** @type {Address} */
address = new Address()
}
```
**Usage:**
```javascript
const user = new User({
id: 1,
name: "Alice",
address: new Address({
street: "123 Main St",
city: "Springfield",
country: "USA"
})
})
```
### Arrays and Slices
```go
type Team struct {
Name string `json:"name"`
Members []string `json:"members"`
}
```
**Generated JavaScript:**
```javascript
export class Team {
/** @type {string} */
name = ""
/** @type {string[]} */
members = []
}
```
**Usage:**
```javascript
const team = new Team({
name: "Engineering",
members: ["Alice", "Bob", "Charlie"]
})
```
### Maps
```go
type Config struct {
Settings map[string]string `json:"settings"`
}
```
**Generated JavaScript:**
```javascript
export class Config {
/** @type {Record<string, string>} */
settings = {}
}
```
**Usage:**
```javascript
const config = new Config({
settings: {
theme: "dark",
language: "en"
}
})
```
## Type Mapping
### Primitive Types
| Go Type | JavaScript Type |
|---------|----------------|
| `string` | `string` |
| `bool` | `boolean` |
| `int`, `int8`, `int16`, `int32`, `int64` | `number` |
| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | `number` |
| `float32`, `float64` | `number` |
| `byte` | `number` |
| `rune` | `number` |
### Special Types
| Go Type | JavaScript Type |
|---------|----------------|
| `time.Time` | `Date` |
| `[]byte` | `Uint8Array` |
| `*T` | `T` (pointers transparent) |
| `interface{}` | `any` |
### Collections
| Go Type | JavaScript Type |
|---------|----------------|
| `[]T` | `T[]` |
| `[N]T` | `T[]` |
| `map[string]T` | `Record<string, T>` |
| `map[K]V` | `Map<K, V>` |
### Unsupported Types
- `chan T` (channels)
- `func()` (functions)
- Complex interfaces (except `interface{}`)
- Unexported fields (lowercase)
## Using Models
### Creating Instances
```javascript
import { User } from './bindings/myapp/models'
// Empty instance
const user1 = new User()
// With data
const user2 = new User({
id: 1,
name: "Alice",
email: "alice@example.com"
})
// From JSON string
const user3 = User.createFrom('{"id":1,"name":"Alice"}')
// From object
const user4 = User.createFrom({ id: 1, name: "Alice" })
```
### Passing to Go
```javascript
import { CreateUser } from './bindings/myapp/userservice'
import { User } from './bindings/myapp/models'
const user = new User({
name: "Bob",
email: "bob@example.com"
})
const created = await CreateUser(user)
console.log("Created user:", created.id)
```
### Receiving from Go
```javascript
import { GetUser } from './bindings/myapp/userservice'
const user = await GetUser(1)
// user is already a User instance
console.log(user.name)
console.log(user.email)
console.log(user.createdAt.toISOString())
```
### Updating Models
```javascript
import { GetUser, UpdateUser } from './bindings/myapp/userservice'
// Get user
const user = await GetUser(1)
// Modify
user.name = "Alice Smith"
user.email = "alice.smith@example.com"
// Save
await UpdateUser(user)
```
## TypeScript Support
### Generated TypeScript
```bash
wails3 generate bindings -ts
```
**Generated:**
```typescript
/**
* User represents an application user
*/
export class User {
/**
* Unique identifier
*/
id: number = 0
/**
* Full name of the user
*/
name: string = ""
/**
* Email address (must be unique)
*/
email: string = ""
/**
* Account creation date
*/
createdAt: Date = new Date()
constructor(source: Partial<User> = {}) {
Object.assign(this, source)
}
static createFrom(source: string | Partial<User> = {}): User {
const parsedSource = typeof source === 'string'
? JSON.parse(source)
: source
return new User(parsedSource)
}
}
```
### Usage in TypeScript
```typescript
import { GetUser, CreateUser } from './bindings/myapp/userservice'
import { User } from './bindings/myapp/models'
async function example() {
// Create user
const newUser = new User({
name: "Alice",
email: "alice@example.com"
})
const created: User = await CreateUser(newUser)
// Get user
const user: User = await GetUser(created.id)
// Type-safe access
console.log(user.name.toUpperCase()) // ✅ string method
console.log(user.id + 1) // ✅ number operation
console.log(user.createdAt.getTime()) // ✅ Date method
}
```
## Advanced Patterns
### Optional Fields
```go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Nickname *string `json:"nickname,omitempty"`
}
```
**JavaScript:**
```javascript
const user = new User({
id: 1,
name: "Alice",
nickname: "Ally" // Optional
})
// Check if set
if (user.nickname) {
console.log("Nickname:", user.nickname)
}
```
### Enums
```go
type UserRole string
const (
RoleAdmin UserRole = "admin"
RoleUser UserRole = "user"
RoleGuest UserRole = "guest"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Role UserRole `json:"role"`
}
```
**Generated:**
```javascript
export const UserRole = {
Admin: "admin",
User: "user",
Guest: "guest"
}
export class User {
/** @type {string} */
role = UserRole.User
}
```
**Usage:**
```javascript
import { User, UserRole } from './bindings/myapp/models'
const admin = new User({
name: "Admin",
role: UserRole.Admin
})
```
### Validation
```javascript
class User {
validate() {
if (!this.name) {
throw new Error("Name is required")
}
if (!this.email.includes('@')) {
throw new Error("Invalid email")
}
return true
}
}
// Use
const user = new User({ name: "Alice", email: "alice@example.com" })
user.validate() // ✅
const invalid = new User({ name: "", email: "invalid" })
invalid.validate() // ❌ Throws
```
### Serialisation
```javascript
// To JSON
const json = JSON.stringify(user)
// From JSON
const user = User.createFrom(json)
// To plain object
const obj = { ...user }
// From plain object
const user2 = new User(obj)
```
## Complete Example
**Go:**
```go
package main
import (
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
type Address struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Address Address `json:"address"`
CreatedAt time.Time `json:"createdAt"`
}
type UserService struct {
users []User
}
func (s *UserService) GetAll() []User {
return s.users
}
func (s *UserService) GetByID(id int) (*User, error) {
for _, user := range s.users {
if user.ID == id {
return &user, nil
}
}
return nil, fmt.Errorf("user %d not found", id)
}
func (s *UserService) Create(user User) User {
user.ID = len(s.users) + 1
user.CreatedAt = time.Now()
s.users = append(s.users, user)
return user
}
func (s *UserService) Update(user User) error {
for i, u := range s.users {
if u.ID == user.ID {
s.users[i] = user
return nil
}
}
return fmt.Errorf("user %d not found", user.ID)
}
func main() {
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&UserService{}),
},
})
app.NewWebviewWindow()
app.Run()
}
```
**JavaScript:**
```javascript
import { GetAll, GetByID, Create, Update } from './bindings/myapp/userservice'
import { User, Address } from './bindings/myapp/models'
class UserManager {
async loadUsers() {
const users = await GetAll()
this.renderUsers(users)
}
async createUser(name, email, address) {
const user = new User({
name,
email,
address: new Address(address)
})
try {
const created = await Create(user)
console.log("Created user:", created.id)
this.loadUsers()
} catch (error) {
console.error("Failed to create user:", error)
}
}
async updateUser(id, updates) {
try {
const user = await GetByID(id)
Object.assign(user, updates)
await Update(user)
this.loadUsers()
} catch (error) {
console.error("Failed to update user:", error)
}
}
renderUsers(users) {
const list = document.getElementById('users')
list.innerHTML = users.map(user => `
<div class="user">
<h3>${user.name}</h3>
<p>${user.email}</p>
<p>${user.address.city}, ${user.address.country}</p>
<small>Created: ${user.createdAt.toLocaleDateString()}</small>
</div>
`).join('')
}
}
const manager = new UserManager()
manager.loadUsers()
```
## Best Practices
### ✅ Do
- **Use JSON tags** - Control field names
- **Add comments** - They become JSDoc
- **Use time.Time** - Converts to Date
- **Validate on Go side** - Don't trust frontend
- **Keep models simple** - Data containers only
- **Use pointers for optional** - `*string` for nullable
### ❌ Don't
- **Don't add methods to Go structs** - Keep them as data
- **Don't use unexported fields** - Won't be bound
- **Don't use complex interfaces** - Not supported
- **Don't forget JSON tags** - Field names matter
- **Don't nest too deeply** - Keep it simple
## Next Steps
<CardGrid>
<Card title="Method Bindings" icon="rocket">
Learn how to bind Go methods.
[Learn More →](/features/bindings/methods)
</Card>
<Card title="Services" icon="puzzle">
Organise code with services.
[Learn More →](/features/bindings/services)
</Card>
<Card title="Best Practices" icon="approve-check">
Binding design patterns.
[Learn More →](/features/bindings/best-practices)
</Card>
<Card title="Go-Frontend Bridge" icon="star">
Understand the bridge mechanism.
[Learn More →](/concepts/bridge)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [binding examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/binding).

View file

@ -0,0 +1,790 @@
---
title: Services
description: Build modular, reusable application components with services
sidebar:
order: 2
---
## Service Architecture
Wails **services** provide a structured way to organise application logic with modular, self-contained components. Services are lifecycle-aware with startup and shutdown hooks, automatically bound to the frontend, dependency-injectable, and fully testable in isolation.
## Quick Start
```go
type GreetService struct {
prefix string
}
func NewGreetService(prefix string) *GreetService {
return &GreetService{prefix: prefix}
}
func (g *GreetService) Greet(name string) string {
return g.prefix + name + "!"
}
// Register
app := application.New(application.Options{
Services: []application.Service{
application.NewService(NewGreetService("Hello, ")),
},
})
```
**That's it!** `Greet` is now callable from JavaScript.
## Creating Services
### Basic Service
```go
type CalculatorService struct{}
func (c *CalculatorService) Add(a, b int) int {
return a + b
}
func (c *CalculatorService) Subtract(a, b int) int {
return a - b
}
```
**Register:**
```go
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&CalculatorService{}),
},
})
```
**Key points:**
- Only **exported methods** (PascalCase) are bound
- Services are **singletons** (one instance)
- Methods can return `(value, error)`
### Service with State
```go
type CounterService struct {
count int
mu sync.RWMutex
}
func (c *CounterService) Increment() int {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
return c.count
}
func (c *CounterService) GetCount() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.count
}
func (c *CounterService) Reset() {
c.mu.Lock()
defer c.mu.Unlock()
c.count = 0
}
```
**Important:** Services are shared across all windows. Always use mutexes for thread safety.
### Service with Dependencies
```go
type UserService struct {
db *sql.DB
logger *slog.Logger
}
func NewUserService(db *sql.DB, logger *slog.Logger) *UserService {
return &UserService{
db: db,
logger: logger,
}
}
func (u *UserService) GetUser(id int) (*User, error) {
u.logger.Info("Getting user", "id", id)
var user User
err := u.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
return &user, nil
}
```
**Register with dependencies:**
```go
db, _ := sql.Open("sqlite3", "app.db")
logger := slog.Default()
app := application.New(application.Options{
Services: []application.Service{
application.NewService(NewUserService(db, logger)),
},
})
```
## Service Lifecycle
### ServiceStartup
Called when application starts:
```go
func (u *UserService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
u.logger.Info("UserService starting up")
// Initialise resources
if err := u.db.Ping(); err != nil {
return fmt.Errorf("database not available: %w", err)
}
// Run migrations
if err := u.runMigrations(); err != nil {
return fmt.Errorf("migrations failed: %w", err)
}
// Start background tasks
go u.backgroundSync(ctx)
return nil
}
```
**Use cases:**
- Initialise resources
- Validate configuration
- Run migrations
- Start background tasks
- Connect to external services
**Important:**
- Services start in **registration order**
- Return error to **prevent app startup**
- Use `ctx` for cancellation
### ServiceShutdown
Called when application shuts down:
```go
func (u *UserService) ServiceShutdown() error {
u.logger.Info("UserService shutting down")
// Close database
if err := u.db.Close(); err != nil {
return fmt.Errorf("failed to close database: %w", err)
}
// Cleanup resources
u.cleanup()
return nil
}
```
**Use cases:**
- Close connections
- Save state
- Cleanup resources
- Flush buffers
- Cancel background tasks
**Important:**
- Services shutdown in **reverse order**
- Application context already cancelled
- Return error to **log warning** (doesn't prevent shutdown)
### Complete Lifecycle Example
```go
type DatabaseService struct {
db *sql.DB
logger *slog.Logger
cancel context.CancelFunc
}
func NewDatabaseService(logger *slog.Logger) *DatabaseService {
return &DatabaseService{logger: logger}
}
func (d *DatabaseService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
d.logger.Info("Starting database service")
// Open database
db, err := sql.Open("sqlite3", "app.db")
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
d.db = db
// Test connection
if err := db.Ping(); err != nil {
return fmt.Errorf("database not available: %w", err)
}
// Start background cleanup
ctx, cancel := context.WithCancel(ctx)
d.cancel = cancel
go d.periodicCleanup(ctx)
return nil
}
func (d *DatabaseService) ServiceShutdown() error {
d.logger.Info("Shutting down database service")
// Cancel background tasks
if d.cancel != nil {
d.cancel()
}
// Close database
if d.db != nil {
if err := d.db.Close(); err != nil {
return fmt.Errorf("failed to close database: %w", err)
}
}
return nil
}
func (d *DatabaseService) periodicCleanup(ctx context.Context) {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
d.cleanup()
}
}
}
```
## Service Options
### Custom Name
```go
app := application.New(application.Options{
Services: []application.Service{
application.NewServiceWithOptions(&MyService{}, application.ServiceOptions{
Name: "CustomServiceName",
}),
},
})
```
**Use cases:**
- Multiple instances of same service type
- Clearer logging
- Better debugging
### HTTP Routes
Services can handle HTTP requests:
```go
type FileService struct {
root string
}
func (f *FileService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Serve files from root directory
http.FileServer(http.Dir(f.root)).ServeHTTP(w, r)
}
// Register with route
app := application.New(application.Options{
Services: []application.Service{
application.NewServiceWithOptions(&FileService{root: "./files"}, application.ServiceOptions{
Route: "/files",
}),
},
})
```
**Access:** `http://wails.localhost/files/...`
**Use cases:**
- File serving
- Custom APIs
- WebSocket endpoints
- Media streaming
## Service Patterns
### Repository Pattern
```go
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) GetByID(id int) (*User, error) {
// Database query
}
func (r *UserRepository) Create(user *User) error {
// Insert user
}
func (r *UserRepository) Update(user *User) error {
// Update user
}
func (r *UserRepository) Delete(id int) error {
// Delete user
}
```
### Service Layer Pattern
```go
type UserService struct {
repo *UserRepository
logger *slog.Logger
}
func (s *UserService) RegisterUser(email, password string) (*User, error) {
// Validate
if !isValidEmail(email) {
return nil, errors.New("invalid email")
}
// Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// Create user
user := &User{
Email: email,
PasswordHash: string(hash),
CreatedAt: time.Now(),
}
if err := s.repo.Create(user); err != nil {
return nil, err
}
s.logger.Info("User registered", "email", email)
return user, nil
}
```
### Factory Pattern
```go
type ServiceFactory struct {
db *sql.DB
logger *slog.Logger
}
func (f *ServiceFactory) CreateUserService() *UserService {
return &UserService{
repo: &UserRepository{db: f.db},
logger: f.logger,
}
}
func (f *ServiceFactory) CreateOrderService() *OrderService {
return &OrderService{
repo: &OrderRepository{db: f.db},
logger: f.logger,
}
}
```
### Event-Driven Pattern
```go
type OrderService struct {
app *application.Application
}
func (o *OrderService) CreateOrder(items []Item) (*Order, error) {
order := &Order{
Items: items,
CreatedAt: time.Now(),
}
// Save order
if err := o.saveOrder(order); err != nil {
return nil, err
}
// Emit event
o.app.EmitEvent("order-created", order)
return order, nil
}
```
## Dependency Injection
### Constructor Injection
```go
type EmailService struct {
smtp *smtp.Client
logger *slog.Logger
}
func NewEmailService(smtp *smtp.Client, logger *slog.Logger) *EmailService {
return &EmailService{
smtp: smtp,
logger: logger,
}
}
// Register
smtpClient := createSMTPClient()
logger := slog.Default()
app := application.New(application.Options{
Services: []application.Service{
application.NewService(NewEmailService(smtpClient, logger)),
},
})
```
### Application Injection
```go
type NotificationService struct {
app *application.Application
}
func NewNotificationService(app *application.Application) *NotificationService {
return &NotificationService{app: app}
}
func (n *NotificationService) Notify(message string) {
// Use application to emit events
n.app.EmitEvent("notification", message)
// Or show system notification
n.app.ShowNotification(message)
}
// Register after app creation
app := application.New(application.Options{})
app.RegisterService(application.NewService(NewNotificationService(app)))
```
### Service-to-Service Dependencies
```go
type OrderService struct {
userService *UserService
emailService *EmailService
}
func NewOrderService(userService *UserService, emailService *EmailService) *OrderService {
return &OrderService{
userService: userService,
emailService: emailService,
}
}
// Register in order
userService := &UserService{}
emailService := &EmailService{}
orderService := NewOrderService(userService, emailService)
app := application.New(application.Options{
Services: []application.Service{
application.NewService(userService),
application.NewService(emailService),
application.NewService(orderService),
},
})
```
## Testing Services
### Unit Testing
```go
func TestCalculatorService_Add(t *testing.T) {
calc := &CalculatorService{}
result := calc.Add(2, 3)
if result != 5 {
t.Errorf("expected 5, got %d", result)
}
}
```
### Testing with Dependencies
```go
func TestUserService_GetUser(t *testing.T) {
// Create mock database
db, mock, _ := sqlmock.New()
defer db.Close()
// Set expectations
rows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "Alice")
mock.ExpectQuery("SELECT").WillReturnRows(rows)
// Create service
service := NewUserService(db, slog.Default())
// Test
user, err := service.GetUser(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("expected Alice, got %s", user.Name)
}
}
```
### Testing Lifecycle
```go
func TestDatabaseService_Lifecycle(t *testing.T) {
service := NewDatabaseService(slog.Default())
// Test startup
ctx := context.Background()
err := service.ServiceStartup(ctx, application.ServiceOptions{})
if err != nil {
t.Fatalf("startup failed: %v", err)
}
// Test functionality
// ...
// Test shutdown
err = service.ServiceShutdown()
if err != nil {
t.Fatalf("shutdown failed: %v", err)
}
}
```
## Best Practices
### ✅ Do
- **Single responsibility** - One service, one purpose
- **Constructor injection** - Pass dependencies explicitly
- **Thread-safe state** - Use mutexes
- **Return errors** - Don't panic
- **Log important events** - Use structured logging
- **Test in isolation** - Mock dependencies
### ❌ Don't
- **Don't use global state** - Pass dependencies
- **Don't block startup** - Keep ServiceStartup fast
- **Don't ignore shutdown** - Always cleanup
- **Don't create circular dependencies** - Design carefully
- **Don't expose internal methods** - Keep them private
- **Don't forget thread safety** - Services are shared
## Complete Example
```go
package main
import (
"context"
"database/sql"
"fmt"
"log/slog"
"sync"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"createdAt"`
}
type UserService struct {
db *sql.DB
logger *slog.Logger
cache map[int]*User
mu sync.RWMutex
}
func NewUserService(logger *slog.Logger) *UserService {
return &UserService{
logger: logger,
cache: make(map[int]*User),
}
}
func (u *UserService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
u.logger.Info("Starting UserService")
// Open database
db, err := sql.Open("sqlite3", "users.db")
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
u.db = db
// Create table
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
// Preload cache
if err := u.loadCache(); err != nil {
return fmt.Errorf("failed to load cache: %w", err)
}
return nil
}
func (u *UserService) ServiceShutdown() error {
u.logger.Info("Shutting down UserService")
if u.db != nil {
return u.db.Close()
}
return nil
}
func (u *UserService) GetUser(id int) (*User, error) {
// Check cache first
u.mu.RLock()
if user, ok := u.cache[id]; ok {
u.mu.RUnlock()
return user, nil
}
u.mu.RUnlock()
// Query database
var user User
err := u.db.QueryRow(
"SELECT id, name, email, created_at FROM users WHERE id = ?",
id,
).Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("user %d not found", id)
}
if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}
// Update cache
u.mu.Lock()
u.cache[id] = &user
u.mu.Unlock()
return &user, nil
}
func (u *UserService) CreateUser(name, email string) (*User, error) {
result, err := u.db.Exec(
"INSERT INTO users (name, email) VALUES (?, ?)",
name, email,
)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
id, _ := result.LastInsertId()
user := &User{
ID: int(id),
Name: name,
Email: email,
CreatedAt: time.Now(),
}
// Update cache
u.mu.Lock()
u.cache[int(id)] = user
u.mu.Unlock()
u.logger.Info("User created", "id", id, "email", email)
return user, nil
}
func (u *UserService) loadCache() error {
rows, err := u.db.Query("SELECT id, name, email, created_at FROM users")
if err != nil {
return err
}
defer rows.Close()
u.mu.Lock()
defer u.mu.Unlock()
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt); err != nil {
return err
}
u.cache[user.ID] = &user
}
return rows.Err()
}
func main() {
app := application.New(application.Options{
Name: "User Management",
Services: []application.Service{
application.NewService(NewUserService(slog.Default())),
},
})
app.NewWebviewWindow()
app.Run()
}
```
## Next Steps
- [Method Bindings](/features/bindings/methods) - Learn how to bind Go methods to JavaScript
- [Models](/features/bindings/models) - Bind complex data structures
- [Events](/features/events/system) - Use events for pub/sub communication
- [Best Practices](/features/bindings/best-practices) - Service design patterns and best practices
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [service examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).

View file

@ -1,7 +1,8 @@
---
title: Browser Integration
description: Open URLs and files in the user's default web browser
sidebar:
order: 58
order: 1
---
import { Tabs, TabItem } from "@astrojs/starlight/components";

View file

@ -0,0 +1,493 @@
---
title: Clipboard Operations
description: Copy and paste text with the system clipboard
sidebar:
order: 1
---
import { Card, CardGrid } from "@astrojs/starlight/components";
## Clipboard Operations
Wails provides a **unified clipboard API** that works across all platforms. Copy and paste text with simple, consistent methods on Windows, macOS, and Linux.
## Quick Start
```go
// Copy text to clipboard
app.Clipboard.SetText("Hello, World!")
// Get text from clipboard
text, ok := app.Clipboard.Text()
if ok {
fmt.Println("Clipboard:", text)
}
```
**That's it!** Cross-platform clipboard access.
## Copying Text
### Basic Copy
```go
success := app.Clipboard.SetText("Text to copy")
if !success {
app.Logger.Error("Failed to copy to clipboard")
}
```
**Returns:** `bool` - `true` if successful, `false` otherwise
### Copy from Service
```go
type ClipboardService struct {
app *application.Application
}
func (c *ClipboardService) CopyToClipboard(text string) bool {
return c.app.Clipboard.SetText(text)
}
```
**Call from JavaScript:**
```javascript
import { CopyToClipboard } from './bindings/myapp/clipboardservice'
await CopyToClipboard("Text to copy")
```
### Copy with Feedback
```go
func copyWithFeedback(text string) {
if app.Clipboard.SetText(text) {
app.InfoDialog().
SetTitle("Copied").
SetMessage("Text copied to clipboard!").
Show()
} else {
app.ErrorDialog().
SetTitle("Copy Failed").
SetMessage("Failed to copy to clipboard.").
Show()
}
}
```
## Pasting Text
### Basic Paste
```go
text, ok := app.Clipboard.Text()
if !ok {
app.Logger.Error("Failed to read clipboard")
return
}
fmt.Println("Clipboard text:", text)
```
**Returns:** `(string, bool)` - Text and success flag
### Paste from Service
```go
func (c *ClipboardService) PasteFromClipboard() string {
text, ok := c.app.Clipboard.Text()
if !ok {
return ""
}
return text
}
```
**Call from JavaScript:**
```javascript
import { PasteFromClipboard } from './bindings/myapp/clipboardservice'
const text = await PasteFromClipboard()
console.log("Pasted:", text)
```
### Paste with Validation
```go
func pasteText() (string, error) {
text, ok := app.Clipboard.Text()
if !ok {
return "", errors.New("clipboard empty or unavailable")
}
// Validate
if len(text) == 0 {
return "", errors.New("clipboard is empty")
}
if len(text) > 10000 {
return "", errors.New("clipboard text too large")
}
return text, nil
}
```
## Complete Examples
### Copy Button
**Go:**
```go
type TextService struct {
app *application.Application
}
func (t *TextService) CopyText(text string) error {
if !t.app.Clipboard.SetText(text) {
return errors.New("failed to copy")
}
return nil
}
```
**JavaScript:**
```javascript
import { CopyText } from './bindings/myapp/textservice'
async function copyToClipboard(text) {
try {
await CopyText(text)
showNotification("Copied to clipboard!")
} catch (error) {
showError("Failed to copy: " + error)
}
}
// Usage
document.getElementById('copy-btn').addEventListener('click', () => {
const text = document.getElementById('text').value
copyToClipboard(text)
})
```
### Paste and Process
**Go:**
```go
type DataService struct {
app *application.Application
}
func (d *DataService) PasteAndProcess() (string, error) {
// Get clipboard text
text, ok := d.app.Clipboard.Text()
if !ok {
return "", errors.New("clipboard unavailable")
}
// Process text
processed := strings.TrimSpace(text)
processed = strings.ToUpper(processed)
return processed, nil
}
```
**JavaScript:**
```javascript
import { PasteAndProcess } from './bindings/myapp/dataservice'
async function pasteAndProcess() {
try {
const result = await PasteAndProcess()
document.getElementById('output').value = result
} catch (error) {
showError("Failed to paste: " + error)
}
}
```
### Copy Multiple Formats
```go
type CopyService struct {
app *application.Application
}
func (c *CopyService) CopyAsPlainText(text string) bool {
return c.app.Clipboard.SetText(text)
}
func (c *CopyService) CopyAsJSON(data interface{}) bool {
jsonBytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
return false
}
return c.app.Clipboard.SetText(string(jsonBytes))
}
func (c *CopyService) CopyAsCSV(rows [][]string) bool {
var buf bytes.Buffer
writer := csv.NewWriter(&buf)
for _, row := range rows {
if err := writer.Write(row); err != nil {
return false
}
}
writer.Flush()
return c.app.Clipboard.SetText(buf.String())
}
```
### Clipboard Monitor
```go
type ClipboardMonitor struct {
app *application.Application
lastText string
ticker *time.Ticker
stopChan chan bool
}
func NewClipboardMonitor(app *application.Application) *ClipboardMonitor {
return &ClipboardMonitor{
app: app,
stopChan: make(chan bool),
}
}
func (cm *ClipboardMonitor) Start() {
cm.ticker = time.NewTicker(1 * time.Second)
go func() {
for {
select {
case <-cm.ticker.C:
cm.checkClipboard()
case <-cm.stopChan:
return
}
}
}()
}
func (cm *ClipboardMonitor) Stop() {
if cm.ticker != nil {
cm.ticker.Stop()
}
cm.stopChan <- true
}
func (cm *ClipboardMonitor) checkClipboard() {
text, ok := cm.app.Clipboard.Text()
if !ok {
return
}
if text != cm.lastText {
cm.lastText = text
cm.app.EmitEvent("clipboard-changed", text)
}
}
```
### Copy with History
```go
type ClipboardHistory struct {
app *application.Application
history []string
maxSize int
}
func NewClipboardHistory(app *application.Application) *ClipboardHistory {
return &ClipboardHistory{
app: app,
history: make([]string, 0),
maxSize: 10,
}
}
func (ch *ClipboardHistory) Copy(text string) bool {
if !ch.app.Clipboard.SetText(text) {
return false
}
// Add to history
ch.history = append([]string{text}, ch.history...)
// Limit size
if len(ch.history) > ch.maxSize {
ch.history = ch.history[:ch.maxSize]
}
return true
}
func (ch *ClipboardHistory) GetHistory() []string {
return ch.history
}
func (ch *ClipboardHistory) RestoreFromHistory(index int) bool {
if index < 0 || index >= len(ch.history) {
return false
}
return ch.app.Clipboard.SetText(ch.history[index])
}
```
## Frontend Integration
### Using Browser Clipboard API
For simple text, you can use the browser's clipboard API:
```javascript
// Copy
async function copyText(text) {
try {
await navigator.clipboard.writeText(text)
console.log("Copied!")
} catch (error) {
console.error("Copy failed:", error)
}
}
// Paste
async function pasteText() {
try {
const text = await navigator.clipboard.readText()
return text
} catch (error) {
console.error("Paste failed:", error)
return ""
}
}
```
**Note:** Browser clipboard API requires HTTPS or localhost, and user permission.
### Using Wails Clipboard
For system-wide clipboard access:
```javascript
import { CopyToClipboard, PasteFromClipboard } from './bindings/myapp/clipboardservice'
// Copy
async function copy(text) {
const success = await CopyToClipboard(text)
if (success) {
console.log("Copied!")
}
}
// Paste
async function paste() {
const text = await PasteFromClipboard()
return text
}
```
## Best Practices
### ✅ Do
- **Check return values** - Handle failures gracefully
- **Provide feedback** - Let users know copy succeeded
- **Validate pasted text** - Check format and size
- **Use appropriate method** - Browser API vs Wails API
- **Handle empty clipboard** - Check before using
- **Trim whitespace** - Clean pasted text
### ❌ Don't
- **Don't ignore failures** - Always check success
- **Don't copy sensitive data** - Clipboard is shared
- **Don't assume format** - Validate pasted data
- **Don't poll too frequently** - If monitoring clipboard
- **Don't copy large data** - Use files instead
- **Don't forget security** - Sanitise pasted content
## Platform Differences
### macOS
- Uses NSPasteboard
- Supports rich text (future)
- System-wide clipboard
- Clipboard history (system feature)
### Windows
- Uses Windows Clipboard API
- Supports multiple formats (future)
- System-wide clipboard
- Clipboard history (Windows 10+)
### Linux
- Uses X11/Wayland clipboard
- Primary and clipboard selections
- Varies by desktop environment
- May require clipboard manager
## Limitations
### Current Version
- **Text only** - Images not yet supported
- **No format detection** - Plain text only
- **No clipboard events** - Must poll for changes
- **No clipboard history** - Implement yourself
### Future Features
- Image support
- Rich text support
- Multiple formats
- Clipboard change events
- Clipboard history API
## Next Steps
<CardGrid>
<Card title="Bindings" icon="rocket">
Call Go functions from JavaScript.
[Learn More →](/features/bindings/methods)
</Card>
<Card title="Events" icon="star">
Use events for clipboard notifications.
[Learn More →](/features/events/system)
</Card>
<Card title="dialogs" icon="information">
Show copy/paste feedback.
[Learn More →](/features/dialogs/message)
</Card>
<Card title="Services" icon="puzzle">
Organise clipboard code.
[Learn More →](/features/bindings/services)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [clipboard examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).

View file

@ -0,0 +1,626 @@
---
title: Custom dialogs
sidebar:
order: 4
---
import { Card, CardGrid } from "@astrojs/starlight/components";
## Custom dialogs
Create **custom dialog windows** using regular Wails windows with dialog-like behaviour. Build custom forms, complex input validation, branded appearance, and rich content (images, videos) whilst maintaining familiar dialog patterns.
## Quick Start
```go
// Create custom dialog window
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Custom dialog",
Width: 400,
Height: 300,
AlwaysOnTop: true,
Frameless: true,
Hidden: true,
})
// Load custom UI
dialog.SetURL("http://wails.localhost/dialog.html")
// Show as modal
dialog.Show()
dialog.SetFocus()
```
**That's it!** Custom UI with dialog behaviour.
## Creating Custom dialogs
### Basic Custom dialog
```go
type Customdialog struct {
window *application.WebviewWindow
result chan string
}
func NewCustomdialog(app *application.Application) *Customdialog {
dialog := &Customdialog{
result: make(chan string, 1),
}
dialog.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Custom dialog",
Width: 400,
Height: 300,
AlwaysOnTop: true,
Resizable: false,
Hidden: true,
})
return dialog
}
func (d *Customdialog) Show() string {
d.window.Show()
d.window.SetFocus()
// Wait for result
return <-d.result
}
func (d *Customdialog) Close(result string) {
d.result <- result
d.window.Close()
}
```
### Modal dialog
```go
func ShowModaldialog(parent *application.WebviewWindow, title string) string {
// Create dialog
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: title,
Width: 400,
Height: 200,
Parent: parent,
AlwaysOnTop: true,
Resizable: false,
})
// Disable parent
parent.SetEnabled(false)
// Re-enable parent on close
dialog.OnClose(func() bool {
parent.SetEnabled(true)
parent.SetFocus()
return true
})
dialog.Show()
return waitForResult(dialog)
}
```
### Form dialog
```go
type Formdialog struct {
window *application.WebviewWindow
data map[string]interface{}
done chan bool
}
func NewFormdialog(app *application.Application) *Formdialog {
fd := &Formdialog{
data: make(map[string]interface{}),
done: make(chan bool, 1),
}
fd.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Enter Information",
Width: 500,
Height: 400,
Frameless: true,
Hidden: true,
})
return fd
}
func (fd *Formdialog) Show() (map[string]interface{}, bool) {
fd.window.Show()
fd.window.SetFocus()
ok := <-fd.done
return fd.data, ok
}
func (fd *Formdialog) Submit(data map[string]interface{}) {
fd.data = data
fd.done <- true
fd.window.Close()
}
func (fd *Formdialog) Cancel() {
fd.done <- false
fd.window.Close()
}
```
## dialog Patterns
### Confirmation dialog
```go
func ShowConfirmdialog(message string) bool {
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Confirm",
Width: 400,
Height: 150,
AlwaysOnTop: true,
Frameless: true,
})
// Pass message to dialog
dialog.OnReady(func() {
dialog.EmitEvent("set-message", message)
})
result := make(chan bool, 1)
// Handle responses
app.OnEvent("confirm-yes", func(e *application.CustomEvent) {
result <- true
dialog.Close()
})
app.OnEvent("confirm-no", func(e *application.CustomEvent) {
result <- false
dialog.Close()
})
dialog.Show()
return <-result
}
```
**Frontend (HTML/JS):**
```html
<div class="dialog">
<h2 id="message"></h2>
<div class="buttons">
<button onclick="confirm(true)">Yes</button>
<button onclick="confirm(false)">No</button>
</div>
</div>
<script>
import { OnEvent, Emit } from '@wailsio/runtime'
OnEvent("set-message", (message) => {
document.getElementById("message").textContent = message
})
function confirm(result) {
Emit(result ? "confirm-yes" : "confirm-no")
}
</script>
```
### Input dialog
```go
func ShowInputdialog(prompt string, defaultValue string) (string, bool) {
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Input",
Width: 400,
Height: 150,
Frameless: true,
})
result := make(chan struct {
value string
ok bool
}, 1)
dialog.OnReady(func() {
dialog.EmitEvent("set-prompt", map[string]string{
"prompt": prompt,
"default": defaultValue,
})
})
app.OnEvent("input-submit", func(e *application.CustomEvent) {
result <- struct {
value string
ok bool
}{e.Data.(string), true}
dialog.Close()
})
app.OnEvent("input-cancel", func(e *application.CustomEvent) {
result <- struct {
value string
ok bool
}{"", false}
dialog.Close()
})
dialog.Show()
r := <-result
return r.value, r.ok
}
```
### Progress dialog
```go
type Progressdialog struct {
window *application.WebviewWindow
}
func NewProgressdialog(title string) *Progressdialog {
pd := &Progressdialog{}
pd.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: title,
Width: 400,
Height: 150,
Frameless: true,
})
return pd
}
func (pd *Progressdialog) Show() {
pd.window.Show()
}
func (pd *Progressdialog) UpdateProgress(current, total int, message string) {
pd.window.EmitEvent("progress-update", map[string]interface{}{
"current": current,
"total": total,
"message": message,
})
}
func (pd *Progressdialog) Close() {
pd.window.Close()
}
```
**Usage:**
```go
func processFiles(files []string) {
progress := NewProgressdialog("Processing Files")
progress.Show()
for i, file := range files {
progress.UpdateProgress(i+1, len(files),
fmt.Sprintf("Processing %s...", filepath.Base(file)))
processFile(file)
}
progress.Close()
}
```
## Complete Examples
### Login dialog
**Go:**
```go
type Logindialog struct {
window *application.WebviewWindow
result chan struct {
username string
password string
ok bool
}
}
func NewLogindialog(app *application.Application) *Logindialog {
ld := &Logindialog{
result: make(chan struct {
username string
password string
ok bool
}, 1),
}
ld.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Login",
Width: 400,
Height: 250,
Frameless: true,
})
return ld
}
func (ld *Logindialog) Show() (string, string, bool) {
ld.window.Show()
ld.window.SetFocus()
r := <-ld.result
return r.username, r.password, r.ok
}
func (ld *Logindialog) Submit(username, password string) {
ld.result <- struct {
username string
password string
ok bool
}{username, password, true}
ld.window.Close()
}
func (ld *Logindialog) Cancel() {
ld.result <- struct {
username string
password string
ok bool
}{"", "", false}
ld.window.Close()
}
```
**Frontend:**
```html
<div class="login-dialog">
<h2>Login</h2>
<form id="login-form">
<input type="text" id="username" placeholder="Username" required>
<input type="password" id="password" placeholder="Password" required>
<div class="buttons">
<button type="submit">Login</button>
<button type="button" onclick="cancel()">Cancel</button>
</div>
</form>
</div>
<script>
import { Emit } from '@wailsio/runtime'
document.getElementById('login-form').addEventListener('submit', (e) => {
e.preventDefault()
const username = document.getElementById('username').value
const password = document.getElementById('password').value
Emit('login-submit', { username, password })
})
function cancel() {
Emit('login-cancel')
}
</script>
```
### Settings dialog
**Go:**
```go
type Settingsdialog struct {
window *application.WebviewWindow
settings map[string]interface{}
done chan bool
}
func NewSettingsdialog(app *application.Application, current map[string]interface{}) *Settingsdialog {
sd := &Settingsdialog{
settings: current,
done: make(chan bool, 1),
}
sd.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Settings",
Width: 600,
Height: 500,
})
sd.window.OnReady(func() {
sd.window.EmitEvent("load-settings", current)
})
return sd
}
func (sd *Settingsdialog) Show() (map[string]interface{}, bool) {
sd.window.Show()
ok := <-sd.done
return sd.settings, ok
}
func (sd *Settingsdialog) Save(settings map[string]interface{}) {
sd.settings = settings
sd.done <- true
sd.window.Close()
}
func (sd *Settingsdialog) Cancel() {
sd.done <- false
sd.window.Close()
}
```
### Wizard dialog
```go
type Wizarddialog struct {
window *application.WebviewWindow
currentStep int
data map[string]interface{}
done chan bool
}
func NewWizarddialog(app *application.Application) *Wizarddialog {
wd := &Wizarddialog{
currentStep: 0,
data: make(map[string]interface{}),
done: make(chan bool, 1),
}
wd.window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Setup Wizard",
Width: 600,
Height: 400,
Resizable: false,
})
return wd
}
func (wd *Wizarddialog) Show() (map[string]interface{}, bool) {
wd.window.Show()
ok := <-wd.done
return wd.data, ok
}
func (wd *Wizarddialog) NextStep(stepData map[string]interface{}) {
// Merge step data
for k, v := range stepData {
wd.data[k] = v
}
wd.currentStep++
wd.window.EmitEvent("next-step", wd.currentStep)
}
func (wd *Wizarddialog) PreviousStep() {
if wd.currentStep > 0 {
wd.currentStep--
wd.window.EmitEvent("previous-step", wd.currentStep)
}
}
func (wd *Wizarddialog) Finish(finalData map[string]interface{}) {
for k, v := range finalData {
wd.data[k] = v
}
wd.done <- true
wd.window.Close()
}
func (wd *Wizarddialog) Cancel() {
wd.done <- false
wd.window.Close()
}
```
## Best Practices
### ✅ Do
- **Use appropriate window options** - AlwaysOnTop, Frameless, etc.
- **Handle cancellation** - Always provide a way to cancel
- **Validate input** - Check data before accepting
- **Provide feedback** - Loading states, errors
- **Use events for communication** - Clean separation
- **Clean up resources** - Close windows, remove listeners
### ❌ Don't
- **Don't block the main thread** - Use channels for results
- **Don't forget to close** - Memory leaks
- **Don't skip validation** - Always validate input
- **Don't ignore errors** - Handle all error cases
- **Don't make it too complex** - Keep dialogs simple
- **Don't forget accessibility** - Keyboard navigation
## Styling Custom dialogs
### Modern dialog Style
```css
.dialog {
display: flex;
flex-direction: column;
height: 100vh;
background: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.dialog-header {
--wails-draggable: drag;
padding: 16px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.dialog-content {
flex: 1;
padding: 24px;
overflow: auto;
}
.dialog-footer {
padding: 16px;
background: #f5f5f5;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
gap: 8px;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button.primary {
background: #007aff;
color: white;
}
button.secondary {
background: #e0e0e0;
color: #333;
}
```
## Next Steps
<CardGrid>
<Card title="Message dialogs" icon="information">
Standard info, warning, error dialogs.
[Learn More →](/features/dialogs/message)
</Card>
<Card title="File dialogs" icon="document">
Open, save, folder selection.
[Learn More →](/features/dialogs/file)
</Card>
<Card title="Windows" icon="laptop">
Learn about window management.
[Learn More →](/features/windows/basics)
</Card>
<Card title="Events" icon="star">
Use events for dialog communication.
[Learn More →](/features/events/system)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [custom dialog examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/dialogs).

View file

@ -0,0 +1,585 @@
---
title: File dialogs
description: Open, save, and folder selection dialogs
sidebar:
order: 3
---
import { Card, CardGrid } from "@astrojs/starlight/components";
## File dialogs
Wails provides **native file dialogs** with platform-appropriate appearance for opening files, saving files, and selecting folders. Simple API with file type filtering, multiple selection support, and default locations.
## Open File dialog
Select files to open:
```go
path, err := app.OpenFileDialog().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.gif"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
PromptForSingleSelection()
if err != nil {
return
}
openFile(path)
```
**Use cases:**
- Open documents
- Import files
- Load images
- Select configuration files
### Single File Selection
```go
path, err := app.OpenFileDialog().
SetTitle("Open Document").
SetFilters([]application.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
PromptForSingleSelection()
if err != nil {
// User cancelled or error occurred
return
}
// Use selected file
data, _ := os.ReadFile(path)
```
### Multiple File Selection
```go
paths, err := app.OpenFileDialog().
SetTitle("Select Images").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.jpeg;*.gif"},
}).
PromptForMultipleSelection()
if err != nil {
return
}
// Process all selected files
for _, path := range paths {
processFile(path)
}
```
### With Default Directory
```go
path, err := app.OpenFileDialog().
SetTitle("Open File").
SetDirectory("/Users/me/Documents").
PromptForSingleSelection()
```
## Save File dialog
Choose where to save:
```go
path, err := app.SaveFileDialog().
SetTitle("Save Document").
SetDefaultFilename("document.txt").
SetFilters([]application.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
PromptForSingleSelection()
if err != nil {
return
}
saveFile(path, data)
```
**Use cases:**
- Save documents
- Export data
- Create new files
- Save as...
### With Default Filename
```go
path, err := app.SaveFileDialog().
SetTitle("Export Data").
SetDefaultFilename("export.csv").
SetFilters([]application.FileFilter{
{DisplayName: "CSV Files", Pattern: "*.csv"},
}).
PromptForSingleSelection()
```
### With Default Directory
```go
path, err := app.SaveFileDialog().
SetTitle("Save File").
SetDirectory("/Users/me/Documents").
SetDefaultFilename("untitled.txt").
PromptForSingleSelection()
```
### Overwrite Confirmation
```go
path, err := app.SaveFileDialog().
SetTitle("Save File").
SetDefaultFilename("document.txt").
PromptForSingleSelection()
if err != nil {
return
}
// Check if file exists
if _, err := os.Stat(path); err == nil {
result, _ := app.QuestionDialog().
SetTitle("Confirm Overwrite").
SetMessage("File already exists. Overwrite?").
SetButtons("Overwrite", "Cancel").
Show()
if result != "Overwrite" {
return
}
}
saveFile(path, data)
```
## Select Folder dialog
Choose a directory:
```go
path, err := app.SelectFolderDialog().
SetTitle("Select Output Folder").
PromptForSingleSelection()
if err != nil {
return
}
exportToFolder(path)
```
**Use cases:**
- Choose output directory
- Select workspace
- Pick backup location
- Choose installation directory
### With Default Directory
```go
path, err := app.SelectFolderDialog().
SetTitle("Select Folder").
SetDirectory("/Users/me/Documents").
PromptForSingleSelection()
```
## File Filters
### Basic Filters
```go
filters := []application.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt"},
{DisplayName: "All Files", Pattern: "*.*"},
}
path, _ := app.OpenFileDialog().
SetFilters(filters).
PromptForSingleSelection()
```
### Multiple Extensions
```go
filters := []application.FileFilter{
{
DisplayName: "Images",
Pattern: "*.png;*.jpg;*.jpeg;*.gif;*.bmp",
},
{
DisplayName: "Documents",
Pattern: "*.txt;*.doc;*.docx;*.pdf",
},
{
DisplayName: "All Files",
Pattern: "*.*",
},
}
```
### Platform-Specific Patterns
**Windows:**
```go
// Semicolon-separated
Pattern: "*.png;*.jpg;*.gif"
```
**macOS/Linux:**
```go
// Space or comma-separated (both work)
Pattern: "*.png *.jpg *.gif"
// or
Pattern: "*.png,*.jpg,*.gif"
```
**Cross-platform (recommended):**
```go
// Use semicolons (works on all platforms)
Pattern: "*.png;*.jpg;*.gif"
```
## Complete Examples
### Open Image File
```go
func openImage() (image.Image, error) {
path, err := app.OpenFileDialog().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{
DisplayName: "Images",
Pattern: "*.png;*.jpg;*.jpeg;*.gif;*.bmp",
},
}).
PromptForSingleSelection()
if err != nil {
return nil, err
}
// Open and decode image
file, err := os.Open(path)
if err != nil {
app.ErrorDialog().
SetTitle("Open Failed").
SetMessage(err.Error()).
Show()
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
app.ErrorDialog().
SetTitle("Invalid Image").
SetMessage("Could not decode image file.").
Show()
return nil, err
}
return img, nil
}
```
### Save Document with Validation
```go
func saveDocument(content string) error {
path, err := app.SaveFileDialog().
SetTitle("Save Document").
SetDefaultFilename("document.txt").
SetFilters([]application.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt"},
{DisplayName: "Markdown Files", Pattern: "*.md"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
PromptForSingleSelection()
if err != nil {
return err
}
// Validate extension
ext := filepath.Ext(path)
if ext != ".txt" && ext != ".md" {
result, _ := app.QuestionDialog().
SetTitle("Confirm Extension").
SetMessage(fmt.Sprintf("Save as %s file?", ext)).
SetButtons("Save", "Cancel").
Show()
if result != "Save" {
return errors.New("cancelled")
}
}
// Save file
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
app.ErrorDialog().
SetTitle("Save Failed").
SetMessage(err.Error()).
Show()
return err
}
app.InfoDialog().
SetTitle("Saved").
SetMessage("Document saved successfully!").
Show()
return nil
}
```
### Batch File Processing
```go
func processMultipleFiles() {
paths, err := app.OpenFileDialog().
SetTitle("Select Files to Process").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg"},
}).
PromptForMultipleSelection()
if err != nil || len(paths) == 0 {
return
}
// Confirm processing
result, _ := app.QuestionDialog().
SetTitle("Confirm Processing").
SetMessage(fmt.Sprintf("Process %d file(s)?", len(paths))).
SetButtons("Process", "Cancel").
Show()
if result != "Process" {
return
}
// Process files
var errors []error
for i, path := range paths {
if err := processFile(path); err != nil {
errors = append(errors, err)
}
// Update progress
app.EmitEvent("progress", map[string]interface{}{
"current": i + 1,
"total": len(paths),
})
}
// Show results
if len(errors) > 0 {
app.WarningDialog().
SetTitle("Processing Complete").
SetMessage(fmt.Sprintf("Processed %d files with %d errors.",
len(paths), len(errors))).
Show()
} else {
app.InfoDialog().
SetTitle("Success").
SetMessage(fmt.Sprintf("Processed %d files successfully!", len(paths))).
Show()
}
}
```
### Export with Folder Selection
```go
func exportData(data []byte) error {
// Select output folder
folder, err := app.SelectFolderDialog().
SetTitle("Select Export Folder").
SetDirectory(getDefaultExportFolder()).
PromptForSingleSelection()
if err != nil {
return err
}
// Generate filename
filename := fmt.Sprintf("export_%s.csv",
time.Now().Format("2006-01-02_15-04-05"))
path := filepath.Join(folder, filename)
// Save file
if err := os.WriteFile(path, data, 0644); err != nil {
app.ErrorDialog().
SetTitle("Export Failed").
SetMessage(err.Error()).
Show()
return err
}
// Show success with option to open folder
result, _ := app.QuestionDialog().
SetTitle("Export Complete").
SetMessage(fmt.Sprintf("Exported to %s", filename)).
SetButtons("Open Folder", "OK").
Show()
if result == "Open Folder" {
openFolder(folder)
}
return nil
}
```
### Import with Validation
```go
func importConfiguration() error {
path, err := app.OpenFileDialog().
SetTitle("Import Configuration").
SetFilters([]application.FileFilter{
{DisplayName: "JSON Files", Pattern: "*.json"},
{DisplayName: "YAML Files", Pattern: "*.yaml;*.yml"},
}).
PromptForSingleSelection()
if err != nil {
return err
}
// Read file
data, err := os.ReadFile(path)
if err != nil {
app.ErrorDialog().
SetTitle("Read Failed").
SetMessage(err.Error()).
Show()
return err
}
// Validate configuration
config, err := parseConfig(data)
if err != nil {
app.ErrorDialog().
SetTitle("Invalid Configuration").
SetMessage("File is not a valid configuration.").
Show()
return err
}
// Confirm import
result, _ := app.QuestionDialog().
SetTitle("Confirm Import").
SetMessage("Import this configuration?").
SetButtons("Import", "Cancel").
Show()
if result != "Import" {
return errors.New("cancelled")
}
// Apply configuration
if err := applyConfig(config); err != nil {
app.ErrorDialog().
SetTitle("Import Failed").
SetMessage(err.Error()).
Show()
return err
}
app.InfoDialog().
SetTitle("Success").
SetMessage("Configuration imported successfully!").
Show()
return nil
}
```
## Best Practices
### ✅ Do
- **Provide file filters** - Help users find files
- **Set appropriate titles** - Clear context
- **Use default directories** - Start in logical location
- **Validate selections** - Check file types
- **Handle cancellation** - User might cancel
- **Show confirmation** - For destructive actions
- **Provide feedback** - Success/error messages
### ❌ Don't
- **Don't skip validation** - Check file types
- **Don't ignore errors** - Handle cancellation
- **Don't use generic filters** - Be specific
- **Don't forget "All Files"** - Always include as option
- **Don't hardcode paths** - Use user's home directory
- **Don't assume file exists** - Check before opening
## Platform Differences
### macOS
- Native NSOpenPanel/NSSavePanel
- Sheet-style when attached to window
- Follows system theme
- Supports Quick Look preview
- Tags and favourites integration
### Windows
- Native File Open/Save dialogs
- Follows system theme
- Recent files integration
- Network location support
### Linux
- GTK file chooser
- Varies by desktop environment
- Follows desktop theme
- Recent files support
## Next Steps
<CardGrid>
<Card title="Message dialogs" icon="information">
Info, warning, and error dialogs.
[Learn More →](/features/dialogs/message)
</Card>
<Card title="Custom dialogs" icon="puzzle">
Create custom dialog windows.
[Learn More →](/features/dialogs/custom)
</Card>
<Card title="Bindings" icon="rocket">
Call Go functions from JavaScript.
[Learn More →](/features/bindings/methods)
</Card>
<Card title="Events" icon="star">
Use events for progress updates.
[Learn More →](/features/events/system)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [file dialog examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/dialogs).

View file

@ -0,0 +1,477 @@
---
title: Message dialogs
description: Display information, warnings, errors, and questions
sidebar:
order: 2
---
import { Card, CardGrid } from "@astrojs/starlight/components";
## Message dialogs
Wails provides **native message dialogs** with platform-appropriate appearance: info, warning, error, and question dialogs with customisable titles, messages, and buttons. Simple API, native behaviour, accessible by default.
## Information dialog
Display informational messages:
```go
app.InfoDialog().
SetTitle("Success").
SetMessage("File saved successfully!").
Show()
```
**Use cases:**
- Success confirmations
- Completion notices
- Informational messages
- Status updates
**Example - Save confirmation:**
```go
func saveFile(path string, data []byte) error {
if err := os.WriteFile(path, data, 0644); err != nil {
return err
}
app.InfoDialog().
SetTitle("File Saved").
SetMessage(fmt.Sprintf("Saved to %s", filepath.Base(path))).
Show()
return nil
}
```
## Warning dialog
Show warnings:
```go
app.WarningDialog().
SetTitle("Warning").
SetMessage("This action cannot be undone.").
Show()
```
**Use cases:**
- Non-critical warnings
- Deprecation notices
- Caution messages
- Potential issues
**Example - Disk space warning:**
```go
func checkDiskSpace() {
available := getDiskSpace()
if available < 100*1024*1024 { // Less than 100MB
app.WarningDialog().
SetTitle("Low Disk Space").
SetMessage(fmt.Sprintf("Only %d MB available.", available/(1024*1024))).
Show()
}
}
```
## Error dialog
Display errors:
```go
app.ErrorDialog().
SetTitle("Error").
SetMessage("Failed to connect to server.").
Show()
```
**Use cases:**
- Error messages
- Failure notifications
- Exception handling
- Critical issues
**Example - Network error:**
```go
func fetchData(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
app.ErrorDialog().
SetTitle("Network Error").
SetMessage(fmt.Sprintf("Failed to connect: %v", err)).
Show()
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
```
## Question dialog
Ask users questions:
```go
result, err := app.QuestionDialog().
SetTitle("Confirm").
SetMessage("Save changes before closing?").
SetButtons("Save", "Don't Save", "Cancel").
SetDefaultButton("Save").
Show()
switch result {
case "Save":
saveChanges()
case "Don't Save":
// Continue without saving
case "Cancel":
// Don't close
}
```
**Use cases:**
- Confirm actions
- Yes/No questions
- Multiple choice
- User decisions
**Example - Unsaved changes:**
```go
func closeDocument() bool {
if !hasUnsavedChanges() {
return true
}
result, err := app.QuestionDialog().
SetTitle("Unsaved Changes").
SetMessage("Do you want to save your changes?").
SetButtons("Save", "Don't Save", "Cancel").
SetDefaultButton("Save").
Show()
if err != nil {
return false
}
switch result {
case "Save":
return saveDocument()
case "Don't Save":
return true
case "Cancel":
return false
}
return false
}
```
## dialog Options
### Title and Message
```go
dialog := app.InfoDialog().
SetTitle("Operation Complete").
SetMessage("All files have been processed successfully.")
```
**Best practices:**
- **Title:** Short, descriptive (2-5 words)
- **Message:** Clear, specific, actionable
- **Avoid jargon:** Use plain language
### Buttons
**Single button (Info/Warning/Error):**
```go
// Default "OK" button
app.InfoDialog().
SetMessage("Done!").
Show()
```
**Multiple buttons (Question):**
```go
result, _ := app.QuestionDialog().
SetMessage("Choose an action").
SetButtons("Option 1", "Option 2", "Option 3").
Show()
```
**Default button:**
```go
app.QuestionDialog().
SetMessage("Delete file?").
SetButtons("Delete", "Cancel").
SetDefaultButton("Cancel"). // Safe option
Show()
```
**Best practices:**
- **1-3 buttons:** Don't overwhelm users
- **Clear labels:** "Save" not "OK"
- **Safe default:** Non-destructive action
- **Order matters:** Most likely action first (except Cancel)
### Window Attachment
Attach to specific window:
```go
dialog := app.QuestionDialog().
SetMessage("Window-specific question").
AttachToWindow(window)
result, _ := dialog.Show()
```
**Benefits:**
- dialog appears on correct window
- Parent window disabled whilst shown
- Better multi-window UX
## Complete Examples
### Confirm Destructive Action
```go
func deleteFiles(paths []string) error {
// Confirm deletion
message := fmt.Sprintf("Delete %d file(s)?", len(paths))
if len(paths) == 1 {
message = fmt.Sprintf("Delete %s?", filepath.Base(paths[0]))
}
result, err := app.QuestionDialog().
SetTitle("Confirm Delete").
SetMessage(message).
SetButtons("Delete", "Cancel").
SetDefaultButton("Cancel").
Show()
if err != nil || result != "Delete" {
return errors.New("cancelled")
}
// Perform deletion
var errors []error
for _, path := range paths {
if err := os.Remove(path); err != nil {
errors = append(errors, err)
}
}
// Show result
if len(errors) > 0 {
app.ErrorDialog().
SetTitle("Delete Failed").
SetMessage(fmt.Sprintf("Failed to delete %d file(s)", len(errors))).
Show()
return fmt.Errorf("%d errors", len(errors))
}
app.InfoDialog().
SetTitle("Delete Complete").
SetMessage(fmt.Sprintf("Deleted %d file(s)", len(paths))).
Show()
return nil
}
```
### Error Handling with Retry
```go
func connectToServer(url string) error {
for attempts := 0; attempts < 3; attempts++ {
if err := tryConnect(url); err == nil {
return nil
}
if attempts < 2 {
result, _ := app.QuestionDialog().
SetTitle("Connection Failed").
SetMessage("Failed to connect. Retry?").
SetButtons("Retry", "Cancel").
Show()
if result != "Retry" {
return errors.New("cancelled")
}
}
}
app.ErrorDialog().
SetTitle("Connection Failed").
SetMessage("Could not connect after 3 attempts.").
Show()
return errors.New("connection failed")
}
```
### Multi-Step Workflow
```go
func exportAndEmail() {
// Step 1: Confirm export
result, _ := app.QuestionDialog().
SetTitle("Export and Email").
SetMessage("Export data and send via email?").
SetButtons("Continue", "Cancel").
Show()
if result != "Continue" {
return
}
// Step 2: Export data
data, err := exportData()
if err != nil {
app.ErrorDialog().
SetTitle("Export Failed").
SetMessage(err.Error()).
Show()
return
}
// Step 3: Send email
if err := sendEmail(data); err != nil {
app.ErrorDialog().
SetTitle("Email Failed").
SetMessage(err.Error()).
Show()
return
}
// Step 4: Success
app.InfoDialog().
SetTitle("Success").
SetMessage("Data exported and emailed successfully!").
Show()
}
```
### Validation with Feedback
```go
func validateAndSave(data string) error {
// Validate
if err := validate(data); err != nil {
app.WarningDialog().
SetTitle("Validation Warning").
SetMessage(err.Error()).
Show()
result, _ := app.QuestionDialog().
SetMessage("Save anyway?").
SetButtons("Save", "Cancel").
SetDefaultButton("Cancel").
Show()
if result != "Save" {
return errors.New("cancelled")
}
}
// Save
if err := save(data); err != nil {
app.ErrorDialog().
SetTitle("Save Failed").
SetMessage(err.Error()).
Show()
return err
}
app.InfoDialog().
SetTitle("Saved").
SetMessage("Data saved successfully!").
Show()
return nil
}
```
## Best Practices
### ✅ Do
- **Be specific** - "File saved to Documents" not "Success"
- **Use appropriate type** - Error for errors, Warning for warnings
- **Provide context** - Include relevant details
- **Use clear button labels** - "Delete" not "OK"
- **Set safe defaults** - Non-destructive action
- **Handle cancellation** - User might close dialog
### ❌ Don't
- **Don't overuse** - Interrupts workflow
- **Don't use for frequent updates** - Use notifications instead
- **Don't use generic messages** - "Error" tells nothing
- **Don't ignore errors** - Handle dialog.Show() errors
- **Don't block unnecessarily** - Consider async alternatives
- **Don't use technical jargon** - Plain language
## Platform Differences
### macOS
- Sheet-style when attached to window
- Standard keyboard shortcuts (⌘. for Cancel)
- Follows system theme automatically
- Accessibility built-in
### Windows
- Modal dialogs
- TaskDialog appearance
- Esc for Cancel
- Follows system theme
### Linux
- GTK dialogs
- Varies by desktop environment
- Follows desktop theme
- Standard keyboard navigation
## Next Steps
<CardGrid>
<Card title="File dialogs" icon="document">
Open, save, and folder selection.
[Learn More →](/features/dialogs/file)
</Card>
<Card title="Custom dialogs" icon="puzzle">
Create custom dialog windows.
[Learn More →](/features/dialogs/custom)
</Card>
<Card title="Notifications" icon="bell">
Non-intrusive notifications.
[Learn More →](/features/notifications)
</Card>
<Card title="Events" icon="star">
Use events for non-blocking communication.
[Learn More →](/features/events/system)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [dialog examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/dialogs).

View file

@ -0,0 +1,470 @@
---
title: dialogs Overview
description: Display native system dialogs in your application
sidebar:
order: 1
---
import { Tabs, TabItem, Card, CardGrid } from "@astrojs/starlight/components";
## Native dialogs
Wails provides **native system dialogs** that work across all platforms: message dialogs (info, warning, error, question), file dialogs (open, save, folder), and custom dialog windows with platform-native appearance and behaviour.
## Quick Start
```go
// Information dialog
app.InfoDialog().
SetTitle("Success").
SetMessage("File saved successfully!").
Show()
// Question dialog
result, _ := app.QuestionDialog().
SetTitle("Confirm").
SetMessage("Delete this file?").
SetButtons("Delete", "Cancel").
Show()
if result == "Delete" {
deleteFile()
}
// File open dialog
path, _ := app.OpenFileDialog().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg"},
}).
PromptForSingleSelection()
```
**That's it!** Native dialogs with minimal code.
## dialog Types
### Information dialog
Display simple messages:
```go
app.InfoDialog().
SetTitle("Welcome").
SetMessage("Welcome to our application!").
Show()
```
**Use cases:**
- Success messages
- Informational notices
- Completion confirmations
### Warning dialog
Show warnings:
```go
app.WarningDialog().
SetTitle("Warning").
SetMessage("This action cannot be undone.").
Show()
```
**Use cases:**
- Non-critical warnings
- Deprecation notices
- Caution messages
### Error dialog
Display errors:
```go
app.ErrorDialog().
SetTitle("Error").
SetMessage("Failed to save file: " + err.Error()).
Show()
```
**Use cases:**
- Error messages
- Failure notifications
- Exception handling
### Question dialog
Ask users questions:
```go
result, err := app.QuestionDialog().
SetTitle("Confirm Delete").
SetMessage("Are you sure you want to delete this file?").
SetButtons("Delete", "Cancel").
SetDefaultButton("Cancel").
Show()
if result == "Delete" {
deleteFile()
}
```
**Use cases:**
- Confirm actions
- Yes/No questions
- Multiple choice
## File dialogs
### Open File dialog
Select files to open:
```go
path, err := app.OpenFileDialog().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.gif"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
PromptForSingleSelection()
if err == nil {
openFile(path)
}
```
**Multiple selection:**
```go
paths, err := app.OpenFileDialog().
SetTitle("Select Images").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg"},
}).
PromptForMultipleSelection()
for _, path := range paths {
processFile(path)
}
```
### Save File dialog
Choose where to save:
```go
path, err := app.SaveFileDialog().
SetTitle("Save Document").
SetDefaultFilename("document.txt").
SetFilters([]application.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
PromptForSingleSelection()
if err == nil {
saveFile(path)
}
```
### Select Folder dialog
Choose a directory:
```go
path, err := app.SelectFolderDialog().
SetTitle("Select Output Folder").
PromptForSingleSelection()
if err == nil {
exportToFolder(path)
}
```
## dialog Options
### Title and Message
```go
dialog := app.InfoDialog().
SetTitle("Success").
SetMessage("Operation completed successfully!")
```
### Buttons
**Predefined buttons:**
```go
// OK button
app.InfoDialog().
SetMessage("Done!").
Show()
// Yes/No buttons
result, _ := app.QuestionDialog().
SetMessage("Continue?").
SetButtons("Yes", "No").
Show()
// Custom buttons
result, _ := app.QuestionDialog().
SetMessage("Choose action").
SetButtons("Save", "Don't Save", "Cancel").
Show()
```
**Default button:**
```go
app.QuestionDialog().
SetMessage("Delete file?").
SetButtons("Delete", "Cancel").
SetDefaultButton("Cancel"). // Highlighted by default
Show()
```
### Window Attachment
Attach dialog to specific window:
```go
dialog := app.InfoDialog().
SetMessage("Window-specific message").
AttachToWindow(window)
dialog.Show()
```
**Behaviour:**
- dialog appears centred on parent window
- Parent window disabled whilst dialog shown
- dialog moves with parent window (macOS)
## Platform Behaviour
<Tabs syncKey="platform">
<TabItem label="macOS" icon="apple">
**macOS dialogs:**
- Native NSAlert appearance
- Follow system theme (light/dark)
- Support keyboard navigation
- Standard shortcuts (⌘. for Cancel)
- Accessibility features built-in
- Sheet-style when attached to window
**Example:**
```go
// Appears as sheet on macOS
app.QuestionDialog().
SetMessage("Save changes?").
AttachToWindow(window).
Show()
```
</TabItem>
<TabItem label="Windows" icon="seti:windows">
**Windows dialogs:**
- Native TaskDialog appearance
- Follow system theme
- Support keyboard navigation
- Standard shortcuts (Esc for Cancel)
- Accessibility features built-in
- Modal to parent window
**Example:**
```go
// Modal dialog on Windows
app.ErrorDialog().
SetTitle("Error").
SetMessage("Operation failed").
Show()
```
</TabItem>
<TabItem label="Linux" icon="linux">
**Linux dialogs:**
- GTK dialog appearance
- Follow desktop theme
- Support keyboard navigation
- Desktop environment integration
- Varies by DE (GNOME, KDE, etc.)
**Example:**
```go
// GTK dialog on Linux
app.InfoDialog().
SetMessage("Update complete").
Show()
```
</TabItem>
</Tabs>
## Common Patterns
### Confirm Before Destructive Action
```go
func deleteFile(path string) error {
result, err := app.QuestionDialog().
SetTitle("Confirm Delete").
SetMessage(fmt.Sprintf("Delete %s?", filepath.Base(path))).
SetButtons("Delete", "Cancel").
SetDefaultButton("Cancel").
Show()
if err != nil || result != "Delete" {
return errors.New("cancelled")
}
return os.Remove(path)
}
```
### Error Handling with dialog
```go
func saveDocument(path string, data []byte) {
if err := os.WriteFile(path, data, 0644); err != nil {
app.ErrorDialog().
SetTitle("Save Failed").
SetMessage(fmt.Sprintf("Could not save file: %v", err)).
Show()
return
}
app.InfoDialog().
SetTitle("Success").
SetMessage("File saved successfully!").
Show()
}
```
### File Selection with Validation
```go
func selectImageFile() (string, error) {
path, err := app.OpenFileDialog().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.jpeg;*.gif"},
}).
PromptForSingleSelection()
if err != nil {
return "", err
}
// Validate file
if !isValidImage(path) {
app.ErrorDialog().
SetTitle("Invalid File").
SetMessage("Selected file is not a valid image.").
Show()
return "", errors.New("invalid image")
}
return path, nil
}
```
### Multi-Step dialog Flow
```go
func exportData() {
// Step 1: Confirm export
result, _ := app.QuestionDialog().
SetTitle("Export Data").
SetMessage("Export all data to CSV?").
SetButtons("Export", "Cancel").
Show()
if result != "Export" {
return
}
// Step 2: Select destination
path, err := app.SaveFileDialog().
SetTitle("Save Export").
SetDefaultFilename("export.csv").
SetFilters([]application.FileFilter{
{DisplayName: "CSV Files", Pattern: "*.csv"},
}).
PromptForSingleSelection()
if err != nil {
return
}
// Step 3: Perform export
if err := performExport(path); err != nil {
app.ErrorDialog().
SetTitle("Export Failed").
SetMessage(err.Error()).
Show()
return
}
// Step 4: Success
app.InfoDialog().
SetTitle("Export Complete").
SetMessage("Data exported successfully!").
Show()
}
```
## Best Practices
### ✅ Do
- **Use native dialogs** - Better UX than custom
- **Provide clear messages** - Be specific
- **Set appropriate titles** - Context matters
- **Use default buttons wisely** - Safe option as default
- **Handle cancellation** - User might cancel
- **Validate file selections** - Check file types
### ❌ Don't
- **Don't overuse dialogs** - Interrupts workflow
- **Don't use for frequent messages** - Use notifications
- **Don't forget error handling** - User might cancel
- **Don't block unnecessarily** - Consider alternatives
- **Don't use generic messages** - Be specific
- **Don't ignore platform differences** - Test on all platforms
## Next Steps
<CardGrid>
<Card title="Message dialogs" icon="information">
Info, warning, and error dialogs.
[Learn More →](/features/dialogs/message)
</Card>
<Card title="File dialogs" icon="document">
Open, save, and folder selection.
[Learn More →](/features/dialogs/file)
</Card>
<Card title="Custom dialogs" icon="puzzle">
Create custom dialog windows.
[Learn More →](/features/dialogs/custom)
</Card>
<Card title="Windows" icon="laptop">
Learn about window management.
[Learn More →](/features/windows/basics)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [dialog examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/dialogs).

View file

@ -0,0 +1,579 @@
---
title: Event System
description: Communicate between components with the event system
sidebar:
order: 1
---
import { Tabs, TabItem, Card, CardGrid } from "@astrojs/starlight/components";
## Event System
Wails provides a **unified event system** for pub/sub communication. Emit events from anywhere, listen from anywhere—Go to JavaScript, JavaScript to Go, window to window—enabling decoupled architecture with typed events and lifecycle hooks.
## Quick Start
**Go (emit):**
```go
app.EmitEvent("user-logged-in", map[string]interface{}{
"userId": 123,
"name": "Alice",
})
```
**JavaScript (listen):**
```javascript
import { OnEvent } from '@wailsio/runtime'
OnEvent("user-logged-in", (data) => {
console.log(`User ${data.name} logged in`)
})
```
**That's it!** Cross-language pub/sub.
## Event Types
### Custom Events
Your application-specific events:
```go
// Emit from Go
app.EmitEvent("order-created", order)
app.EmitEvent("payment-processed", payment)
app.EmitEvent("notification", message)
```
```javascript
// Listen in JavaScript
OnEvent("order-created", handleOrder)
OnEvent("payment-processed", handlePayment)
OnEvent("notification", showNotification)
```
### System Events
Built-in OS and application events:
```go
import "github.com/wailsapp/wails/v3/pkg/events"
// Theme changes
app.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
if e.Context().IsDarkMode() {
app.Logger.Info("Dark mode enabled")
}
})
// Application lifecycle
app.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) {
app.Logger.Info("Application started")
})
```
### Window Events
Window-specific events:
```go
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
app.Logger.Info("Window focused")
})
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) {
app.Logger.Info("Window closing")
})
```
## Emitting Events
### From Go
**Basic emit:**
```go
app.EmitEvent("event-name", data)
```
**With different data types:**
```go
// String
app.EmitEvent("message", "Hello")
// Number
app.EmitEvent("count", 42)
// Struct
app.EmitEvent("user", User{ID: 1, Name: "Alice"})
// Map
app.EmitEvent("config", map[string]interface{}{
"theme": "dark",
"fontSize": 14,
})
// Array
app.EmitEvent("items", []string{"a", "b", "c"})
```
**To specific window:**
```go
window.EmitEvent("window-specific-event", data)
```
### From JavaScript
```javascript
import { Emit } from '@wailsio/runtime'
// Emit to Go
Emit("button-clicked", { buttonId: "submit" })
// Emit to all windows
Emit("broadcast-message", "Hello everyone")
```
## Listening to Events
### In Go
**Application events:**
```go
app.OnEvent("custom-event", func(e *application.CustomEvent) {
data := e.Data
// Handle event
})
```
**With type assertion:**
```go
app.OnEvent("user-updated", func(e *application.CustomEvent) {
user := e.Data.(User)
app.Logger.Info("User updated", "name", user.Name)
})
```
**Multiple handlers:**
```go
// All handlers will be called
app.OnEvent("order-created", logOrder)
app.OnEvent("order-created", sendEmail)
app.OnEvent("order-created", updateInventory)
```
### In JavaScript
**Basic listener:**
```javascript
import { OnEvent } from '@wailsio/runtime'
OnEvent("event-name", (data) => {
console.log("Event received:", data)
})
```
**With cleanup:**
```javascript
const unsubscribe = OnEvent("event-name", handleEvent)
// Later, stop listening
unsubscribe()
```
**Multiple handlers:**
```javascript
OnEvent("data-updated", updateUI)
OnEvent("data-updated", saveToCache)
OnEvent("data-updated", logChange)
```
## System Events
### Application Events
**Common events (cross-platform):**
```go
import "github.com/wailsapp/wails/v3/pkg/events"
// Application started
app.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) {
app.Logger.Info("App started")
})
// Theme changed
app.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
isDark := e.Context().IsDarkMode()
app.EmitEvent("theme-changed", isDark)
})
// File opened
app.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *application.ApplicationEvent) {
filePath := e.Context().OpenedFile()
openFile(filePath)
})
```
**Platform-specific events:**
<Tabs syncKey="platform">
<TabItem label="macOS" icon="apple">
```go
// Application became active
app.OnApplicationEvent(events.Mac.ApplicationDidBecomeActive, func(e *application.ApplicationEvent) {
app.Logger.Info("App became active")
})
// Application will terminate
app.OnApplicationEvent(events.Mac.ApplicationWillTerminate, func(e *application.ApplicationEvent) {
cleanup()
})
```
</TabItem>
<TabItem label="Windows" icon="seti:windows">
```go
// Power status changed
app.OnApplicationEvent(events.Windows.APMPowerStatusChange, func(e *application.ApplicationEvent) {
app.Logger.Info("Power status changed")
})
// System suspending
app.OnApplicationEvent(events.Windows.APMSuspend, func(e *application.ApplicationEvent) {
saveState()
})
```
</TabItem>
<TabItem label="Linux" icon="linux">
```go
// Application startup
app.OnApplicationEvent(events.Linux.ApplicationStartup, func(e *application.ApplicationEvent) {
app.Logger.Info("App starting")
})
// Theme changed
app.OnApplicationEvent(events.Linux.SystemThemeChanged, func(e *application.ApplicationEvent) {
updateTheme()
})
```
</TabItem>
</Tabs>
### Window Events
**Common window events:**
```go
// Window focus
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
app.Logger.Info("Window focused")
})
// Window blur
window.OnWindowEvent(events.Common.WindowBlur, func(e *application.WindowEvent) {
app.Logger.Info("Window blurred")
})
// Window closing
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) {
if hasUnsavedChanges() {
e.Cancel() // Prevent close
}
})
// Window closed
window.OnWindowEvent(events.Common.WindowClosed, func(e *application.WindowEvent) {
cleanup()
})
```
## Event Hooks
Hooks run **before** standard listeners and can **cancel** events:
```go
// Hook - runs first, can cancel
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
if hasUnsavedChanges() {
result := showConfirmdialog("Unsaved changes. Close anyway?")
if result != "yes" {
e.Cancel() // Prevent window close
}
}
})
// Standard listener - runs after hooks
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) {
app.Logger.Info("Window closing")
})
```
**Key differences:**
| Feature | Hooks | Standard Listeners |
|---------|-------|-------------------|
| Execution order | First, in registration order | After hooks, no guaranteed order |
| Blocking | Synchronous, blocks next hook | Asynchronous, non-blocking |
| Can cancel | Yes | No (already propagated) |
| Use case | Control flow, validation | Logging, side effects |
## Event Patterns
### Pub/Sub Pattern
```go
// Publisher (service)
type OrderService struct {
app *application.Application
}
func (o *OrderService) CreateOrder(items []Item) (*Order, error) {
order := &Order{Items: items}
if err := o.saveOrder(order); err != nil {
return nil, err
}
// Publish event
o.app.EmitEvent("order-created", order)
return order, nil
}
// Subscribers
app.OnEvent("order-created", func(e *application.CustomEvent) {
order := e.Data.(*Order)
sendConfirmationEmail(order)
})
app.OnEvent("order-created", func(e *application.CustomEvent) {
order := e.Data.(*Order)
updateInventory(order)
})
app.OnEvent("order-created", func(e *application.CustomEvent) {
order := e.Data.(*Order)
logOrder(order)
})
```
### Request/Response Pattern
```go
// Frontend requests data
Emit("get-user-data", { userId: 123 })
// Backend responds
app.OnEvent("get-user-data", func(e *application.CustomEvent) {
data := e.Data.(map[string]interface{})
userId := int(data["userId"].(float64))
user := getUserFromDB(userId)
// Send response
app.EmitEvent("user-data-response", user)
})
// Frontend receives response
OnEvent("user-data-response", (user) => {
displayUser(user)
})
```
**Note:** For request/response, **bindings are better**. Use events for notifications.
### Broadcast Pattern
```go
// Broadcast to all windows
app.EmitEvent("global-notification", "System update available")
// Each window handles it
OnEvent("global-notification", (message) => {
showNotification(message)
})
```
### Event Aggregation
```go
type EventAggregator struct {
events []Event
mu sync.Mutex
}
func (ea *EventAggregator) Add(event Event) {
ea.mu.Lock()
defer ea.mu.Unlock()
ea.events = append(ea.events, event)
// Emit batch every 100 events
if len(ea.events) >= 100 {
app.EmitEvent("event-batch", ea.events)
ea.events = nil
}
}
```
## Complete Example
**Go:**
```go
package main
import (
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
type NotificationService struct {
app *application.Application
}
func (n *NotificationService) Notify(message string) {
// Emit to all windows
n.app.EmitEvent("notification", map[string]interface{}{
"message": message,
"timestamp": time.Now(),
})
}
func main() {
app := application.New(application.Options{
Name: "Event Demo",
})
notifService := &NotificationService{app: app}
// System events
app.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) {
isDark := e.Context().IsDarkMode()
app.EmitEvent("theme-changed", isDark)
})
// Custom events from frontend
app.OnEvent("user-action", func(e *application.CustomEvent) {
data := e.Data.(map[string]interface{})
action := data["action"].(string)
app.Logger.Info("User action", "action", action)
// Respond
notifService.Notify("Action completed: " + action)
})
// Window events
window := app.NewWebviewWindow()
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
app.EmitEvent("window-focused", window.Name())
})
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
// Confirm before close
app.EmitEvent("confirm-close", nil)
e.Cancel() // Wait for confirmation
})
app.Run()
}
```
**JavaScript:**
```javascript
import { OnEvent, Emit } from '@wailsio/runtime'
// Listen for notifications
OnEvent("notification", (data) => {
showNotification(data.message)
})
// Listen for theme changes
OnEvent("theme-changed", (isDark) => {
document.body.classList.toggle('dark', isDark)
})
// Listen for window focus
OnEvent("window-focused", (windowName) => {
console.log(`Window ${windowName} focused`)
})
// Handle close confirmation
OnEvent("confirm-close", () => {
if (confirm("Close window?")) {
Emit("close-confirmed", true)
}
})
// Emit user actions
document.getElementById('button').addEventListener('click', () => {
Emit("user-action", { action: "button-clicked" })
})
```
## Best Practices
### ✅ Do
- **Use events for notifications** - One-way communication
- **Use bindings for requests** - Two-way communication
- **Keep event names consistent** - Use kebab-case
- **Document event data** - What fields are included?
- **Unsubscribe when done** - Prevent memory leaks
- **Use hooks for validation** - Control event flow
### ❌ Don't
- **Don't use events for RPC** - Use bindings instead
- **Don't emit too frequently** - Batch if needed
- **Don't block in handlers** - Keep them fast
- **Don't forget to unsubscribe** - Memory leaks
- **Don't use events for large data** - Use bindings
- **Don't create event loops** - A emits B, B emits A
## Next Steps
<CardGrid>
<Card title="Custom Events" icon="star">
Create your own event types.
[Learn More →](/features/events/custom)
</Card>
<Card title="Event Patterns" icon="puzzle">
Common event patterns and best practices.
[Learn More →](/features/events/patterns)
</Card>
<Card title="Bindings" icon="rocket">
Use bindings for request/response.
[Learn More →](/features/bindings/methods)
</Card>
<Card title="Window Events" icon="laptop">
Handle window lifecycle events.
[Learn More →](/features/windows/events)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [event examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/events).

View file

@ -1,7 +1,8 @@
---
title: Key Bindings
title: Keyboard Shortcuts
description: Register global keyboard shortcuts for quick access to functionality
sidebar:
order: 56
order: 1
---
import { Tabs, TabItem } from "@astrojs/starlight/components";

View file

@ -0,0 +1,640 @@
---
title: Application Menus
description: Create native menu bars for your desktop application
sidebar:
order: 1
---
import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components";
## The Problem
Professional desktop applications need menu bars—File, Edit, View, Help. But menus work differently on each platform:
- **macOS**: Global menu bar at top of screen
- **Windows**: Menu bar in window title bar
- **Linux**: Varies by desktop environment
Building platform-appropriate menus manually is tedious and error-prone.
## The Wails Solution
Wails provides a **unified API** that creates platform-native menus automatically. Write once, get native behaviour on all platforms.
{/* VISUAL PLACEHOLDER: Menu Bar Comparison
Description: Three screenshots side-by-side showing the same Wails menu on:
1. macOS - Global menu bar at top of screen with app name
2. Windows - Menu bar in window title bar
3. Linux (GNOME) - Menu bar in window
All showing identical menu structure: File, Edit, View, Tools, Help
Style: Clean screenshots with subtle borders, labels indicating platform
*/}
## Quick Start
```go
package main
import (
"runtime"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(application.Options{
Name: "My App",
})
// Create menu
menu := app.NewMenu()
// Add standard menus (platform-appropriate)
if runtime.GOOS == "darwin" {
menu.AddRole(application.AppMenu) // macOS only
}
menu.AddRole(application.FileMenu)
menu.AddRole(application.EditMenu)
menu.AddRole(application.WindowMenu)
menu.AddRole(application.HelpMenu)
// Set the menu
app.SetMenu(menu)
// Create window and run
app.NewWebviewWindow()
app.Run()
}
```
**That's it!** You now have platform-native menus with standard items.
## Creating Menus
### Basic Menu Creation
```go
// Create a new menu
menu := app.NewMenu()
// Add a top-level menu
fileMenu := menu.AddSubmenu("File")
// Add menu items
fileMenu.Add("New").OnClick(func(ctx *application.Context) {
// Handle New
})
fileMenu.Add("Open").OnClick(func(ctx *application.Context) {
// Handle Open
})
fileMenu.AddSeparator()
fileMenu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
```
### Setting the Menu
Platform-specific API:
<Tabs syncKey="platform">
<TabItem label="macOS" icon="apple">
**Global menu bar** (one per application):
```go
app.SetMenu(menu)
```
The menu appears at the top of the screen and persists even when all windows are closed.
</TabItem>
<TabItem label="Windows" icon="seti:windows">
**Per-window menu bar**:
```go
window.SetMenu(menu)
```
Each window can have its own menu. The menu appears in the window's title bar.
</TabItem>
<TabItem label="Linux" icon="linux">
**Per-window menu bar** (usually):
```go
window.SetMenu(menu)
```
Behaviour varies by desktop environment. Some (like Unity) support global menus.
</TabItem>
</Tabs>
**Cross-platform helper:**
```go
func setMenuForPlatform(app *application.Application, window *application.WebviewWindow, menu *application.Menu) {
if runtime.GOOS == "darwin" {
app.SetMenu(menu)
} else {
window.SetMenu(menu)
}
}
```
## Menu Roles
Wails provides **predefined menu roles** that create platform-appropriate menu structures automatically.
### Available Roles
| Role | Description | Platform Notes |
|------|-------------|----------------|
| `AppMenu` | Application menu with About, Preferences, Quit | **macOS only** |
| `FileMenu` | File operations (New, Open, Save, etc.) | All platforms |
| `EditMenu` | Text editing (Undo, Redo, Cut, Copy, Paste) | All platforms |
| `WindowMenu` | Window management (Minimise, Zoom, etc.) | All platforms |
| `HelpMenu` | Help and information | All platforms |
### Using Roles
```go
menu := app.NewMenu()
// macOS: Add application menu
if runtime.GOOS == "darwin" {
menu.AddRole(application.AppMenu)
}
// All platforms: Add standard menus
menu.AddRole(application.FileMenu)
menu.AddRole(application.EditMenu)
menu.AddRole(application.WindowMenu)
menu.AddRole(application.HelpMenu)
```
**What you get:**
<Tabs syncKey="platform">
<TabItem label="macOS" icon="apple">
**AppMenu** (with app name):
- About [App Name]
- Preferences... (⌘,)
- ---
- Services
- ---
- Hide [App Name] (⌘H)
- Hide Others (⌥⌘H)
- Show All
- ---
- Quit [App Name] (⌘Q)
**FileMenu**:
- New (⌘N)
- Open... (⌘O)
- ---
- Close Window (⌘W)
**EditMenu**:
- Undo (⌘Z)
- Redo (⇧⌘Z)
- ---
- Cut (⌘X)
- Copy (⌘C)
- Paste (⌘V)
- Select All (⌘A)
**WindowMenu**:
- Minimise (⌘M)
- Zoom
- ---
- Bring All to Front
**HelpMenu**:
- [App Name] Help
</TabItem>
<TabItem label="Windows" icon="seti:windows">
**FileMenu**:
- New (Ctrl+N)
- Open... (Ctrl+O)
- ---
- Exit (Alt+F4)
**EditMenu**:
- Undo (Ctrl+Z)
- Redo (Ctrl+Y)
- ---
- Cut (Ctrl+X)
- Copy (Ctrl+C)
- Paste (Ctrl+V)
- Select All (Ctrl+A)
**WindowMenu**:
- Minimise
- Maximise
**HelpMenu**:
- About [App Name]
</TabItem>
<TabItem label="Linux" icon="linux">
Similar to Windows, but keyboard shortcuts may vary by desktop environment.
</TabItem>
</Tabs>
### Customising Role Menus
Add items to role menus:
```go
fileMenu := menu.AddRole(application.FileMenu)
// Add custom items
fileMenu.Add("Import...").OnClick(handleImport)
fileMenu.Add("Export...").OnClick(handleExport)
```
## Custom Menus
Create your own menus for application-specific features:
```go
// Add a custom top-level menu
toolsMenu := menu.AddSubmenu("Tools")
// Add items
toolsMenu.Add("Settings").OnClick(func(ctx *application.Context) {
showSettingsWindow()
})
toolsMenu.AddSeparator()
// Add checkbox
toolsMenu.AddCheckbox("Dark Mode", false).OnClick(func(ctx *application.Context) {
isDark := ctx.ClickedMenuItem().Checked()
setTheme(isDark)
})
// Add radio group
toolsMenu.AddRadio("Small", true).OnClick(handleFontSize)
toolsMenu.AddRadio("Medium", false).OnClick(handleFontSize)
toolsMenu.AddRadio("Large", false).OnClick(handleFontSize)
// Add submenu
advancedMenu := toolsMenu.AddSubmenu("Advanced")
advancedMenu.Add("Configure...").OnClick(showAdvancedSettings)
```
**For more menu item types**, see [Menu Reference](/features/menus/reference).
## Dynamic Menus
Update menus based on application state:
### Enable/Disable Items
```go
var saveMenuItem *application.MenuItem
func createMenu() {
menu := app.NewMenu()
fileMenu := menu.AddSubmenu("File")
saveMenuItem = fileMenu.Add("Save")
saveMenuItem.SetEnabled(false) // Initially disabled
saveMenuItem.OnClick(handleSave)
app.SetMenu(menu)
}
func onDocumentChanged() {
saveMenuItem.SetEnabled(hasUnsavedChanges())
menu.Update() // Important!
}
```
:::caution[Always Call menu.Update()]
After changing menu state (enable/disable, label, checked), **always call `menu.Update()`**. This is especially critical on Windows where menus are reconstructed.
See [Menu Reference](/features/menus/reference#enabled-state) for details.
:::
### Change Labels
```go
updateMenuItem := menu.Add("Check for Updates")
updateMenuItem.OnClick(func(ctx *application.Context) {
updateMenuItem.SetLabel("Checking...")
menu.Update()
checkForUpdates()
updateMenuItem.SetLabel("Check for Updates")
menu.Update()
})
```
### Rebuild Menus
For major changes, rebuild the entire menu:
```go
func rebuildFileMenu() {
menu := app.NewMenu()
fileMenu := menu.AddSubmenu("File")
fileMenu.Add("New").OnClick(handleNew)
fileMenu.Add("Open").OnClick(handleOpen)
// Add recent files dynamically
if hasRecentFiles() {
recentMenu := fileMenu.AddSubmenu("Open Recent")
for _, file := range getRecentFiles() {
filePath := file // Capture for closure
recentMenu.Add(filepath.Base(file)).OnClick(func(ctx *application.Context) {
openFile(filePath)
})
}
recentMenu.AddSeparator()
recentMenu.Add("Clear Recent").OnClick(clearRecentFiles)
}
fileMenu.AddSeparator()
fileMenu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
app.SetMenu(menu)
}
```
## Window Control from Menus
Menu items can control windows:
```go
viewMenu := menu.AddSubmenu("View")
// Toggle fullscreen
viewMenu.Add("Toggle Fullscreen").OnClick(func(ctx *application.Context) {
window := app.GetWindowByName("main")
window.SetFullscreen(!window.IsFullscreen())
})
// Zoom controls
viewMenu.Add("Zoom In").SetAccelerator("CmdOrCtrl++").OnClick(func(ctx *application.Context) {
// Increase zoom
})
viewMenu.Add("Zoom Out").SetAccelerator("CmdOrCtrl+-").OnClick(func(ctx *application.Context) {
// Decrease zoom
})
viewMenu.Add("Reset Zoom").SetAccelerator("CmdOrCtrl+0").OnClick(func(ctx *application.Context) {
// Reset zoom
})
```
**Get the active window:**
```go
menuItem.OnClick(func(ctx *application.Context) {
window := application.ContextWindow(ctx)
// Use window
})
```
## Platform-Specific Considerations
### macOS
**Menu bar behaviour:**
- Appears at **top of screen** (global)
- Persists when all windows closed
- First menu is **always the application menu**
- Use `menu.AddRole(application.AppMenu)` for standard items
**Standard locations:**
- **About**: Application menu
- **Preferences**: Application menu (⌘,)
- **Quit**: Application menu (⌘Q)
- **Help**: Help menu
**Example:**
```go
if runtime.GOOS == "darwin" {
menu.AddRole(application.AppMenu) // Adds About, Preferences, Quit
// Don't add Quit to File menu on macOS
// Don't add About to Help menu on macOS
}
```
### Windows
**Menu bar behaviour:**
- Appears in **window title bar**
- Each window has its own menu
- No application menu
**Standard locations:**
- **Exit**: File menu (Alt+F4)
- **Settings**: Tools or Edit menu
- **About**: Help menu
**Example:**
```go
if runtime.GOOS == "windows" {
fileMenu := menu.AddRole(application.FileMenu)
// Exit is added automatically
helpMenu := menu.AddRole(application.HelpMenu)
// About is added automatically
}
```
### Linux
**Menu bar behaviour:**
- Usually per-window (like Windows)
- Some DEs support global menus (Unity, GNOME with extension)
- Appearance varies by desktop environment
**Best practice:** Follow Windows conventions, test on target DEs.
## Complete Example
Here's a production-ready menu structure:
```go
package main
import (
"runtime"
"github.com/wailsapp/wails/v3/pkg/application"
)
func main() {
app := application.New(application.Options{
Name: "My Application",
})
// Create and set menu
createMenu(app)
// Create main window
app.NewWebviewWindow()
app.Run()
}
func createMenu(app *application.Application) {
menu := app.NewMenu()
// Platform-specific application menu (macOS only)
if runtime.GOOS == "darwin" {
menu.AddRole(application.AppMenu)
}
// File menu
fileMenu := menu.AddRole(application.FileMenu)
fileMenu.Add("Import...").SetAccelerator("CmdOrCtrl+I").OnClick(handleImport)
fileMenu.Add("Export...").SetAccelerator("CmdOrCtrl+E").OnClick(handleExport)
// Edit menu
menu.AddRole(application.EditMenu)
// View menu
viewMenu := menu.AddSubmenu("View")
viewMenu.Add("Toggle Fullscreen").SetAccelerator("F11").OnClick(toggleFullscreen)
viewMenu.AddSeparator()
viewMenu.AddCheckbox("Show Sidebar", true).OnClick(toggleSidebar)
viewMenu.AddCheckbox("Show Toolbar", true).OnClick(toggleToolbar)
// Tools menu
toolsMenu := menu.AddSubmenu("Tools")
// Settings location varies by platform
if runtime.GOOS == "darwin" {
// On macOS, Preferences is in Application menu (added by AppMenu role)
} else {
toolsMenu.Add("Settings").SetAccelerator("CmdOrCtrl+,").OnClick(showSettings)
}
toolsMenu.AddSeparator()
toolsMenu.AddCheckbox("Dark Mode", false).OnClick(toggleDarkMode)
// Window menu
menu.AddRole(application.WindowMenu)
// Help menu
helpMenu := menu.AddRole(application.HelpMenu)
helpMenu.Add("Documentation").OnClick(openDocumentation)
// About location varies by platform
if runtime.GOOS == "darwin" {
// On macOS, About is in Application menu (added by AppMenu role)
} else {
helpMenu.AddSeparator()
helpMenu.Add("About").OnClick(showAbout)
}
// Set the menu
app.SetMenu(menu)
}
func handleImport(ctx *application.Context) {
// Implementation
}
func handleExport(ctx *application.Context) {
// Implementation
}
func toggleFullscreen(ctx *application.Context) {
window := application.ContextWindow(ctx)
window.SetFullscreen(!window.IsFullscreen())
}
func toggleSidebar(ctx *application.Context) {
// Implementation
}
func toggleToolbar(ctx *application.Context) {
// Implementation
}
func showSettings(ctx *application.Context) {
// Implementation
}
func toggleDarkMode(ctx *application.Context) {
isDark := ctx.ClickedMenuItem().Checked()
// Apply theme
}
func openDocumentation(ctx *application.Context) {
// Open browser
}
func showAbout(ctx *application.Context) {
// Show about dialog
}
```
## Best Practices
### ✅ Do
- **Use menu roles** for standard menus (File, Edit, etc.)
- **Follow platform conventions** for menu structure
- **Add keyboard shortcuts** to common actions
- **Call menu.Update()** after changing menu state
- **Test on all platforms** - behaviour varies
- **Keep menus shallow** - 2-3 levels maximum
- **Use clear labels** - "Save Project" not "Save"
### ❌ Don't
- **Don't hardcode platform shortcuts** - Use `CmdOrCtrl`
- **Don't put Quit in File menu on macOS** - It's in Application menu
- **Don't put About in Help menu on macOS** - It's in Application menu
- **Don't forget menu.Update()** - Menus won't work properly
- **Don't nest too deeply** - Users get lost
- **Don't use jargon** - Keep labels user-friendly
## Next Steps
<CardGrid>
<Card title="Menu Reference" icon="document">
Complete reference for menu item types and properties.
[Learn More →](/features/menus/reference)
</Card>
<Card title="Context Menus" icon="puzzle">
Create right-click context menus.
[Learn More →](/features/menus/context)
</Card>
<Card title="System Tray Menus" icon="star">
Add system tray/menu bar integration.
[Learn More →](/features/menus/systray)
</Card>
<Card title="Menu Patterns" icon="open-book">
Common menu patterns and best practices.
[Learn More →](/guides/patterns/menus)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [menu example](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/menu).

View file

@ -0,0 +1,728 @@
---
title: Context Menus
description: Create right-click context menus for your application
sidebar:
order: 2
---
import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components";
## The Problem
Users expect right-click menus with context-specific actions. Different elements need different menus:
- **Text**: Cut, Copy, Paste
- **Images**: Save, Copy, Open
- **Custom elements**: Application-specific actions
Building context menus manually means handling mouse events, positioning, and platform differences.
## The Wails Solution
Wails provides **declarative context menus** using CSS properties. Associate menus with HTML elements, pass data, and handle clicks—all with native platform behaviour.
## Quick Start
**Go code:**
```go
// Create context menu
contextMenu := app.NewContextMenu()
contextMenu.Add("Cut").OnClick(handleCut)
contextMenu.Add("Copy").OnClick(handleCopy)
contextMenu.Add("Paste").OnClick(handlePaste)
// Register with ID
app.RegisterContextMenu("editor-menu", contextMenu)
```
**HTML:**
```html
<textarea style="--custom-contextmenu: editor-menu">
Right-click me!
</textarea>
```
**That's it!** Right-clicking the textarea shows your custom menu.
## Creating Context Menus
### Basic Context Menu
```go
// Create menu
contextMenu := app.NewContextMenu()
// Add items
contextMenu.Add("Cut").SetAccelerator("CmdOrCtrl+X").OnClick(func(ctx *application.Context) {
// Handle cut
})
contextMenu.Add("Copy").SetAccelerator("CmdOrCtrl+C").OnClick(func(ctx *application.Context) {
// Handle copy
})
contextMenu.Add("Paste").SetAccelerator("CmdOrCtrl+V").OnClick(func(ctx *application.Context) {
// Handle paste
})
// Register with unique ID
app.RegisterContextMenu("text-menu", contextMenu)
```
**Menu ID:** Must be unique. Used to associate menu with HTML elements.
### With Submenus
```go
contextMenu := app.NewContextMenu()
// Add regular items
contextMenu.Add("Open").OnClick(handleOpen)
contextMenu.Add("Delete").OnClick(handleDelete)
contextMenu.AddSeparator()
// Add submenu
exportMenu := contextMenu.AddSubmenu("Export As")
exportMenu.Add("PNG").OnClick(exportPNG)
exportMenu.Add("JPEG").OnClick(exportJPEG)
exportMenu.Add("SVG").OnClick(exportSVG)
app.RegisterContextMenu("image-menu", contextMenu)
```
### With Checkboxes and Radio Groups
```go
contextMenu := app.NewContextMenu()
// Checkbox
contextMenu.AddCheckbox("Show Grid", true).OnClick(func(ctx *application.Context) {
showGrid := ctx.ClickedMenuItem().Checked()
// Toggle grid
})
contextMenu.AddSeparator()
// Radio group
contextMenu.AddRadio("Small", false).OnClick(handleSize)
contextMenu.AddRadio("Medium", true).OnClick(handleSize)
contextMenu.AddRadio("Large", false).OnClick(handleSize)
app.RegisterContextMenu("view-menu", contextMenu)
```
**For all menu item types**, see [Menu Reference](/features/menus/reference).
## Associating with HTML Elements
Use CSS custom properties to attach context menus:
### Basic Association
```html
<div style="--custom-contextmenu: menu-id">
Right-click me!
</div>
```
**CSS property:** `--custom-contextmenu: <menu-id>`
### With Context Data
Pass data from HTML to Go:
```html
<div style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-123">
Right-click this file
</div>
```
**Go handler:**
```go
contextMenu := app.NewContextMenu()
contextMenu.Add("Open").OnClick(func(ctx *application.Context) {
fileID := ctx.ContextMenuData() // "file-123"
openFile(fileID)
})
app.RegisterContextMenu("file-menu", contextMenu)
```
**CSS properties:**
- `--custom-contextmenu: <menu-id>` - Which menu to show
- `--custom-contextmenu-data: <data>` - Data to pass to handlers
### Dynamic Data
Generate data dynamically in JavaScript:
```html
<div id="file-item" style="--custom-contextmenu: file-menu">
File.txt
</div>
<script>
// Set data dynamically
const fileItem = document.getElementById('file-item')
fileItem.style.setProperty('--custom-contextmenu-data', 'file-' + fileId)
</script>
```
### Multiple Elements, Same Menu
```html
<div class="file-item" style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-1">
Document.pdf
</div>
<div class="file-item" style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-2">
Image.png
</div>
<div class="file-item" style="--custom-contextmenu: file-menu; --custom-contextmenu-data: file-3">
Video.mp4
</div>
```
**One menu, different data for each element.**
## Context Data
### Accessing Context Data
```go
contextMenu.Add("Process").OnClick(func(ctx *application.Context) {
data := ctx.ContextMenuData() // Get data from HTML
// Use the data
processItem(data)
})
```
**Data type:** Always `string`. Parse as needed.
### Passing Complex Data
Use JSON for complex data:
```html
<div style="--custom-contextmenu: item-menu; --custom-contextmenu-data: {&quot;id&quot;:123,&quot;type&quot;:&quot;image&quot;}">
Image.png
</div>
```
**Go handler:**
```go
import "encoding/json"
type ItemData struct {
ID int `json:"id"`
Type string `json:"type"`
}
contextMenu.Add("Process").OnClick(func(ctx *application.Context) {
dataStr := ctx.ContextMenuData()
var data ItemData
if err := json.Unmarshal([]byte(dataStr), &data); err != nil {
log.Printf("Invalid data: %v", err)
return
}
processItem(data.ID, data.Type)
})
```
:::caution[Security]
**Always validate context data** from the frontend. Users can manipulate CSS properties, so treat data as untrusted input.
:::
### Validation Example
```go
contextMenu.Add("Delete").OnClick(func(ctx *application.Context) {
fileID := ctx.ContextMenuData()
// Validate
if !isValidFileID(fileID) {
log.Printf("Invalid file ID: %s", fileID)
return
}
// Check permissions
if !canDeleteFile(fileID) {
showError("Permission denied")
return
}
// Safe to proceed
deleteFile(fileID)
})
```
## Default Context Menu
The WebView provides a built-in context menu for standard operations (copy, paste, inspect). Control it with `--default-contextmenu`:
### Hide Default Menu
```html
<div style="--default-contextmenu: hide">
No default menu here
</div>
```
**Use case:** Custom UI elements where default menu doesn't make sense.
### Show Default Menu
```html
<div style="--default-contextmenu: show">
Default menu always shown
</div>
```
**Use case:** Text areas, input fields, editable content.
### Auto (Smart) Mode
```html
<div style="--default-contextmenu: auto">
Smart context menu
</div>
```
**Default behaviour.** Shows default menu when:
- Text is selected
- In text input fields
- In editable content (`contenteditable`)
Hides default menu otherwise.
### Combining Custom and Default
```html
<!-- Custom menu + default menu -->
<textarea style="--custom-contextmenu: editor-menu; --default-contextmenu: show">
Both menus available
</textarea>
```
**Behaviour:**
1. Custom menu shows first
2. If custom menu is empty or not found, default menu shows
3. Both can coexist (platform-dependent)
## Dynamic Context Menus
Update menus based on application state:
### Enable/Disable Items
```go
var cutMenuItem *application.MenuItem
var copyMenuItem *application.MenuItem
func createContextMenu() {
contextMenu := app.NewContextMenu()
cutMenuItem = contextMenu.Add("Cut")
cutMenuItem.SetEnabled(false) // Initially disabled
cutMenuItem.OnClick(handleCut)
copyMenuItem = contextMenu.Add("Copy")
copyMenuItem.SetEnabled(false)
copyMenuItem.OnClick(handleCopy)
app.RegisterContextMenu("editor-menu", contextMenu)
}
func onSelectionChanged(hasSelection bool) {
cutMenuItem.SetEnabled(hasSelection)
copyMenuItem.SetEnabled(hasSelection)
contextMenu.Update() // Important!
}
```
:::caution[Always Call Update()]
After changing menu state, **call `contextMenu.Update()`**. This is critical on Windows.
See [Menu Reference](/features/menus/reference#enabled-state) for details.
:::
### Change Labels
```go
playMenuItem := contextMenu.Add("Play")
playMenuItem.OnClick(func(ctx *application.Context) {
if isPlaying {
playMenuItem.SetLabel("Pause")
} else {
playMenuItem.SetLabel("Play")
}
contextMenu.Update()
})
```
### Rebuild Menus
For major changes, rebuild the entire menu:
```go
func rebuildContextMenu(fileType string) {
contextMenu := app.NewContextMenu()
// Common items
contextMenu.Add("Open").OnClick(handleOpen)
contextMenu.Add("Delete").OnClick(handleDelete)
contextMenu.AddSeparator()
// Type-specific items
switch fileType {
case "image":
contextMenu.Add("Edit Image").OnClick(editImage)
contextMenu.Add("Set as Wallpaper").OnClick(setWallpaper)
case "video":
contextMenu.Add("Play").OnClick(playVideo)
contextMenu.Add("Extract Audio").OnClick(extractAudio)
case "document":
contextMenu.Add("Print").OnClick(printDocument)
contextMenu.Add("Export PDF").OnClick(exportPDF)
}
app.RegisterContextMenu("file-menu", contextMenu)
}
```
## Platform Behaviour
Context menus are **platform-native**:
<Tabs syncKey="platform">
<TabItem label="macOS" icon="apple">
**Native macOS context menus:**
- System animations and transitions
- Right-click = Control+Click (automatic)
- Adapts to system appearance (light/dark)
- Standard text operations in default menu
- Native scrolling for long menus
**macOS conventions:**
- Use sentence case for menu items
- Use ellipsis (...) for items that open dialogs
- Common shortcuts: ⌘C (Copy), ⌘V (Paste)
</TabItem>
<TabItem label="Windows" icon="seti:windows">
**Native Windows context menus:**
- Windows native style
- Follows Windows theme
- Standard Windows operations in default menu
- Touch and pen input support
**Windows conventions:**
- Use title case for menu items
- Use ellipsis (...) for items that open dialogs
- Common shortcuts: Ctrl+C (Copy), Ctrl+V (Paste)
</TabItem>
<TabItem label="Linux" icon="linux">
**Desktop environment integration:**
- Adapts to desktop theme (GTK, Qt, etc.)
- Right-click behaviour follows system settings
- Default menu content varies by environment
- Positioning follows DE conventions
**Linux considerations:**
- Test on target desktop environments
- GTK and Qt have different behaviours
- Some DEs customise context menus
</TabItem>
</Tabs>
## Complete Example
**Go code:**
```go
package main
import (
"encoding/json"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
)
type FileData struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
}
func main() {
app := application.New(application.Options{
Name: "Context Menu Demo",
})
// Create file context menu
fileMenu := createFileMenu(app)
app.RegisterContextMenu("file-menu", fileMenu)
// Create image context menu
imageMenu := createImageMenu(app)
app.RegisterContextMenu("image-menu", imageMenu)
// Create text context menu
textMenu := createTextMenu(app)
app.RegisterContextMenu("text-menu", textMenu)
app.NewWebviewWindow()
app.Run()
}
func createFileMenu(app *application.Application) *application.ContextMenu {
menu := app.NewContextMenu()
menu.Add("Open").OnClick(func(ctx *application.Context) {
data := parseFileData(ctx.ContextMenuData())
openFile(data.ID)
})
menu.Add("Rename").OnClick(func(ctx *application.Context) {
data := parseFileData(ctx.ContextMenuData())
renameFile(data.ID)
})
menu.AddSeparator()
menu.Add("Delete").OnClick(func(ctx *application.Context) {
data := parseFileData(ctx.ContextMenuData())
deleteFile(data.ID)
})
return menu
}
func createImageMenu(app *application.Application) *application.ContextMenu {
menu := app.NewContextMenu()
menu.Add("View").OnClick(func(ctx *application.Context) {
data := parseFileData(ctx.ContextMenuData())
viewImage(data.ID)
})
menu.Add("Edit").OnClick(func(ctx *application.Context) {
data := parseFileData(ctx.ContextMenuData())
editImage(data.ID)
})
menu.AddSeparator()
exportMenu := menu.AddSubmenu("Export As")
exportMenu.Add("PNG").OnClick(exportPNG)
exportMenu.Add("JPEG").OnClick(exportJPEG)
exportMenu.Add("WebP").OnClick(exportWebP)
return menu
}
func createTextMenu(app *application.Application) *application.ContextMenu {
menu := app.NewContextMenu()
menu.Add("Cut").SetAccelerator("CmdOrCtrl+X").OnClick(handleCut)
menu.Add("Copy").SetAccelerator("CmdOrCtrl+C").OnClick(handleCopy)
menu.Add("Paste").SetAccelerator("CmdOrCtrl+V").OnClick(handlePaste)
return menu
}
func parseFileData(dataStr string) FileData {
var data FileData
if err := json.Unmarshal([]byte(dataStr), &data); err != nil {
log.Printf("Invalid file data: %v", err)
}
return data
}
// Handler implementations...
func openFile(id string) { /* ... */ }
func renameFile(id string) { /* ... */ }
func deleteFile(id string) { /* ... */ }
func viewImage(id string) { /* ... */ }
func editImage(id string) { /* ... */ }
func exportPNG(ctx *application.Context) { /* ... */ }
func exportJPEG(ctx *application.Context) { /* ... */ }
func exportWebP(ctx *application.Context) { /* ... */ }
func handleCut(ctx *application.Context) { /* ... */ }
func handleCopy(ctx *application.Context) { /* ... */ }
func handlePaste(ctx *application.Context) { /* ... */ }
```
**HTML:**
```html
<!DOCTYPE html>
<html>
<head>
<style>
.file-item {
padding: 10px;
margin: 5px;
border: 1px solid #ccc;
cursor: pointer;
}
.file-item:hover {
background: #f0f0f0;
}
textarea {
width: 100%;
height: 200px;
}
</style>
</head>
<body>
<h2>Files</h2>
<!-- Regular file -->
<div class="file-item"
style="--custom-contextmenu: file-menu;
--custom-contextmenu-data: {&quot;id&quot;:&quot;file-1&quot;,&quot;type&quot;:&quot;document&quot;,&quot;name&quot;:&quot;Report.pdf&quot;}">
📄 Report.pdf
</div>
<!-- Image file -->
<div class="file-item"
style="--custom-contextmenu: image-menu;
--custom-contextmenu-data: {&quot;id&quot;:&quot;file-2&quot;,&quot;type&quot;:&quot;image&quot;,&quot;name&quot;:&quot;Photo.jpg&quot;}">
🖼️ Photo.jpg
</div>
<h2>Text Editor</h2>
<!-- Text area with custom menu + default menu -->
<textarea
style="--custom-contextmenu: text-menu; --default-contextmenu: show"
placeholder="Type here, then right-click...">
</textarea>
<h2>No Context Menu</h2>
<!-- Disable default menu -->
<div style="--default-contextmenu: hide; padding: 20px; border: 1px solid #ccc;">
Right-click here - no menu appears
</div>
</body>
</html>
```
## Best Practices
### ✅ Do
- **Keep menus focused** - Only relevant actions for the element
- **Validate context data** - Treat as untrusted input
- **Use clear labels** - "Delete File" not "Delete"
- **Call menu.Update()** - After changing menu state
- **Test on all platforms** - Behaviour varies
- **Provide keyboard shortcuts** - For common actions
- **Group related items** - Use separators
### ❌ Don't
- **Don't trust context data** - Always validate
- **Don't make menus too long** - 7-10 items maximum
- **Don't forget menu.Update()** - Menus won't work properly
- **Don't nest too deeply** - 2 levels maximum
- **Don't use jargon** - Keep labels user-friendly
- **Don't block handlers** - Keep them fast
## Troubleshooting
### Context Menu Not Appearing
**Possible causes:**
1. Menu ID mismatch
2. CSS property typo
3. Runtime not initialised
**Solution:**
```go
// Check menu is registered
app.RegisterContextMenu("my-menu", contextMenu)
```
```html
<!-- Check ID matches -->
<div style="--custom-contextmenu: my-menu">
```
### Context Data Not Received
**Possible causes:**
1. CSS property not set
2. Data contains special characters
**Solution:**
```html
<!-- Escape quotes in JSON -->
<div style="--custom-contextmenu-data: {&quot;id&quot;:123}">
```
Or use JavaScript:
```javascript
element.style.setProperty('--custom-contextmenu-data', JSON.stringify(data))
```
### Menu Items Not Responding
**Cause:** Forgot to call `menu.Update()` after enabling
**Solution:**
```go
menuItem.SetEnabled(true)
contextMenu.Update() // Add this!
```
## Next Steps
<CardGrid>
<Card title="Menu Reference" icon="document">
Complete reference for menu item types and properties.
[Learn More →](/features/menus/reference)
</Card>
<Card title="Application Menus" icon="list-format">
Create application menu bars.
[Learn More →](/features/menus/application)
</Card>
<Card title="System Tray Menus" icon="star">
Add system tray/menu bar integration.
[Learn More →](/features/menus/systray)
</Card>
<Card title="Menu Patterns" icon="open-book">
Common menu patterns and best practices.
[Learn More →](/guides/patterns/menus)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [context menu example](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/contextmenus).

View file

@ -0,0 +1,562 @@
---
title: Menu Reference
description: Complete reference for menu item types, properties, and methods
sidebar:
order: 4
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
## Menu Reference
Complete reference for menu item types, properties, and dynamic behaviour. Build professional, responsive menus with checkboxes, radio groups, separators, and dynamic updates.
## Menu Item Types
### Regular Menu Items
The most common type—displays text and triggers an action:
```go
menuItem := menu.Add("Click Me")
menuItem.OnClick(func(ctx *application.Context) {
fmt.Println("Menu item clicked!")
})
```
**Use for:** Commands, actions, opening windows
### Checkboxes
Toggle-able menu items with checked/unchecked state:
```go
checkbox := menu.AddCheckbox("Enable Feature", true) // true = initially checked
checkbox.OnClick(func(ctx *application.Context) {
isChecked := ctx.ClickedMenuItem().Checked()
fmt.Printf("Feature is now: %v\n", isChecked)
})
```
**Use for:** Boolean settings, feature toggles, view options
**Important:** The checked state toggles automatically when clicked.
### Radio Groups
Mutually exclusive options—only one can be selected:
```go
menu.AddRadio("Small", true) // true = initially selected
menu.AddRadio("Medium", false)
menu.AddRadio("Large", false)
```
**Use for:** Mutually exclusive choices (size, theme, mode)
**How grouping works:**
- Adjacent radio items form a group automatically
- Selecting one deselects others in the group
- Separate groups with a separator or regular item
**Example with multiple groups:**
```go
// Group 1: Size
menu.AddRadio("Small", true)
menu.AddRadio("Medium", false)
menu.AddRadio("Large", false)
menu.AddSeparator()
// Group 2: Theme
menu.AddRadio("Light", true)
menu.AddRadio("Dark", false)
```
### Submenus
Nested menu structures for organisation:
```go
submenu := menu.AddSubmenu("More Options")
submenu.Add("Submenu Item 1").OnClick(func(ctx *application.Context) {
// Handle click
})
submenu.Add("Submenu Item 2")
```
**Use for:** Grouping related items, reducing clutter
**Nesting limit:** Most platforms support 2-3 levels. Avoid deeper nesting.
### Separators
Visual dividers between menu items:
```go
menu.Add("Item 1")
menu.AddSeparator()
menu.Add("Item 2")
```
**Use for:** Grouping related items visually
**Best practice:** Don't start or end menus with separators.
## Menu Item Properties
### Label
The text displayed for the menu item:
```go
menuItem := menu.Add("Initial Label")
menuItem.SetLabel("New Label")
// Get current label
label := menuItem.Label()
```
**Dynamic labels:**
```go
updateMenuItem := menu.Add("Check for Updates")
updateMenuItem.OnClick(func(ctx *application.Context) {
updateMenuItem.SetLabel("Checking...")
menu.Update() // Important on Windows!
// Perform update check
checkForUpdates()
updateMenuItem.SetLabel("Check for Updates")
menu.Update()
})
```
### Enabled State
Control whether the menu item can be interacted with:
```go
menuItem := menu.Add("Save")
menuItem.SetEnabled(false) // Greyed out, can't click
// Enable it later
menuItem.SetEnabled(true)
menu.Update() // Important: Call this after changing enabled state!
// Check current state
isEnabled := menuItem.Enabled()
```
:::caution[Windows Menu Behaviour]
On Windows, menus need to be reconstructed when their state changes. **Always call `menu.Update()` after enabling/disabling menu items**, especially if the item was created whilst disabled.
**Why:** Windows menus are rebuilt from scratch when updated. If you don't call `Update()`, click handlers won't fire properly.
:::
**Example: Dynamic enable/disable**
```go
var hasSelection bool
cutMenuItem := menu.Add("Cut")
cutMenuItem.SetEnabled(false) // Initially disabled
copyMenuItem := menu.Add("Copy")
copyMenuItem.SetEnabled(false)
// When selection changes
func onSelectionChanged(selected bool) {
hasSelection = selected
cutMenuItem.SetEnabled(hasSelection)
copyMenuItem.SetEnabled(hasSelection)
menu.Update() // Critical on Windows!
}
```
**Common pattern: Enable on condition**
```go
saveMenuItem := menu.Add("Save")
func updateSaveMenuItem() {
canSave := hasUnsavedChanges() && !isSaving()
saveMenuItem.SetEnabled(canSave)
menu.Update()
}
// Call whenever state changes
onDocumentChanged(func() {
updateSaveMenuItem()
})
```
### Checked State
For checkbox and radio items, control or query their checked state:
```go
checkbox := menu.AddCheckbox("Feature", false)
checkbox.SetChecked(true)
menu.Update()
// Query state
isChecked := checkbox.Checked()
```
**Auto-toggle:** Checkboxes toggle automatically when clicked. You don't need to call `SetChecked()` in the click handler.
**Manual control:**
```go
checkbox := menu.AddCheckbox("Auto-save", false)
// Sync with external state
func syncAutoSave(enabled bool) {
checkbox.SetChecked(enabled)
menu.Update()
}
```
### Accelerators (Keyboard Shortcuts)
Add keyboard shortcuts to menu items:
```go
saveMenuItem := menu.Add("Save")
saveMenuItem.SetAccelerator("CmdOrCtrl+S")
quitMenuItem := menu.Add("Quit")
quitMenuItem.SetAccelerator("CmdOrCtrl+Q")
```
**Accelerator format:**
- `CmdOrCtrl` - Cmd on macOS, Ctrl on Windows/Linux
- `Shift`, `Alt`, `Option` - Modifier keys
- `A-Z`, `0-9` - Letter/number keys
- `F1-F12` - Function keys
- `Enter`, `Space`, `Backspace`, etc. - Special keys
**Examples:**
```go
"CmdOrCtrl+S" // Save
"CmdOrCtrl+Shift+S" // Save As
"CmdOrCtrl+W" // Close Window
"CmdOrCtrl+Q" // Quit
"F5" // Refresh
"CmdOrCtrl+," // Preferences (macOS convention)
"Alt+F4" // Close (Windows convention)
```
**Platform-specific accelerators:**
```go
if runtime.GOOS == "darwin" {
prefsMenuItem.SetAccelerator("Cmd+,")
} else {
prefsMenuItem.SetAccelerator("Ctrl+P")
}
```
### Tooltip
Add hover text to menu items (platform support varies):
```go
menuItem := menu.Add("Advanced Options")
menuItem.SetTooltip("Configure advanced settings")
```
**Platform support:**
- **Windows:** ✅ Supported
- **macOS:** ❌ Not supported (tooltips not standard for menus)
- **Linux:** ⚠️ Varies by desktop environment
### Hidden State
Hide menu items without removing them:
```go
debugMenuItem := menu.Add("Debug Mode")
debugMenuItem.SetHidden(true) // Hidden
// Show in debug builds
if isDebugBuild {
debugMenuItem.SetHidden(false)
menu.Update()
}
```
**Use for:** Debug options, feature flags, conditional features
## Event Handling
### OnClick Handler
Execute code when menu item is clicked:
```go
menuItem := menu.Add("Click Me")
menuItem.OnClick(func(ctx *application.Context) {
// Handle click
fmt.Println("Clicked!")
})
```
**Context provides:**
- `ctx.ClickedMenuItem()` - The menu item that was clicked
- Window context (if from window menu)
- Application context
**Example: Access menu item in handler**
```go
checkbox := menu.AddCheckbox("Feature", false)
checkbox.OnClick(func(ctx *application.Context) {
item := ctx.ClickedMenuItem()
isChecked := item.Checked()
fmt.Printf("Feature is now: %v\n", isChecked)
})
```
### Multiple Handlers
You can set multiple handlers (last one wins):
```go
menuItem := menu.Add("Action")
menuItem.OnClick(func(ctx *application.Context) {
fmt.Println("First handler")
})
// This replaces the first handler
menuItem.OnClick(func(ctx *application.Context) {
fmt.Println("Second handler - this one runs")
})
```
**Best practice:** Set handler once, use conditional logic inside if needed.
## Dynamic Menus
### Updating Menu Items
**The golden rule:** Always call `menu.Update()` after changing menu state.
```go
// ✅ Correct
menuItem.SetEnabled(true)
menu.Update()
// ❌ Wrong (especially on Windows)
menuItem.SetEnabled(true)
// Forgot to call Update() - click handlers may not work!
```
**Why this matters:**
- **Windows:** Menus are reconstructed when updated
- **macOS/Linux:** Less critical but still recommended
- **Click handlers:** Won't fire properly without Update()
### Rebuilding Menus
For major changes, rebuild the entire menu:
```go
func rebuildFileMenu() {
menu := app.Menu.New()
menu.Add("New").OnClick(handleNew)
menu.Add("Open").OnClick(handleOpen)
if hasRecentFiles() {
recentMenu := menu.AddSubmenu("Open Recent")
for _, file := range getRecentFiles() {
recentMenu.Add(file).OnClick(func(ctx *application.Context) {
openFile(file)
})
}
}
menu.AddSeparator()
menu.Add("Quit").OnClick(handleQuit)
// Set the new menu
window.SetMenu(menu)
}
```
**When to rebuild:**
- Recent files list changes
- Plugin menus change
- Major state transitions
**When to update:**
- Enable/disable items
- Change labels
- Toggle checkboxes
### Context-Sensitive Menus
Adjust menus based on application state:
```go
func updateEditMenu() {
cutMenuItem.SetEnabled(hasSelection())
copyMenuItem.SetEnabled(hasSelection())
pasteMenuItem.SetEnabled(hasClipboardContent())
undoMenuItem.SetEnabled(canUndo())
redoMenuItem.SetEnabled(canRedo())
menu.Update()
}
// Call whenever state changes
onSelectionChanged(updateEditMenu)
onClipboardChanged(updateEditMenu)
onUndoStackChanged(updateEditMenu)
```
## Platform Differences
### Menu Bar Location
| Platform | Location | Notes |
|----------|----------|-------|
| **macOS** | Top of screen | Global menu bar |
| **Windows** | Top of window | Per-window menu |
| **Linux** | Top of window | Per-window (usually) |
### Standard Menus
**macOS:**
- Has "Application" menu (with app name)
- "Preferences" in Application menu
- "Quit" in Application menu
**Windows/Linux:**
- No Application menu
- "Preferences" in Edit or Tools menu
- "Exit" in File menu
**Example: Platform-appropriate structure**
```go
menu := app.Menu.New()
// macOS gets Application menu
if runtime.GOOS == "darwin" {
menu.AddRole(application.AppMenu)
}
// File menu
fileMenu := menu.AddSubmenu("File")
fileMenu.Add("New")
fileMenu.Add("Open")
// Preferences location varies
if runtime.GOOS == "darwin" {
// On macOS, preferences are in Application menu (added by AppMenu role)
} else {
// On Windows/Linux, add to Edit or Tools menu
editMenu := menu.AddSubmenu("Edit")
editMenu.Add("Preferences")
}
```
### Accelerator Conventions
**macOS:**
- `Cmd+` for most shortcuts
- `Cmd+,` for Preferences
- `Cmd+Q` for Quit
**Windows:**
- `Ctrl+` for most shortcuts
- `Ctrl+P` or `Ctrl+,` for Preferences
- `Alt+F4` for Exit (or `Ctrl+Q`)
**Linux:**
- Generally follows Windows conventions
- Desktop environment may override
## Best Practices
### ✅ Do
- **Call menu.Update()** after changing menu state (especially on Windows)
- **Use radio groups** for mutually exclusive options
- **Use checkboxes** for toggleable features
- **Add accelerators** to common actions
- **Group related items** with separators
- **Test on all platforms** - behaviour varies
### ❌ Don't
- **Don't forget menu.Update()** - Click handlers won't work properly
- **Don't nest too deeply** - 2-3 levels maximum
- **Don't start/end with separators** - Looks unprofessional
- **Don't use tooltips on macOS** - Not supported
- **Don't hardcode platform shortcuts** - Use `CmdOrCtrl`
## Troubleshooting
### Menu Items Not Responding
**Symptom:** Click handlers don't fire
**Cause:** Forgot to call `menu.Update()` after enabling item
**Solution:**
```go
menuItem.SetEnabled(true)
menu.Update() // Add this!
```
### Menu Items Greyed Out
**Symptom:** Can't click menu items
**Cause:** Items are disabled
**Solution:**
```go
menuItem.SetEnabled(true)
menu.Update()
```
### Accelerators Not Working
**Symptom:** Keyboard shortcuts don't trigger menu items
**Causes:**
1. Accelerator format incorrect
2. Conflict with system shortcuts
3. Window doesn't have focus
**Solution:**
```go
// Check format
menuItem.SetAccelerator("CmdOrCtrl+S") // ✅ Correct
menuItem.SetAccelerator("Ctrl+S") // ❌ Wrong (macOS uses Cmd)
// Avoid conflicts
// ❌ Cmd+H (Hide Window on macOS - system shortcut)
// ✅ Cmd+Shift+H (Custom shortcut)
```
## Next Steps
- [Application Menus](/features/menus/application) - Create application menu bars
- [Context Menus](/features/menus/context) - Right-click context menus
- [System Tray Menus](/features/menus/systray) - System tray/menu bar menus
- [Menu Patterns](/guides/patterns/menus) - Common menu patterns and best practices
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [menu examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/menu).

View file

@ -0,0 +1,713 @@
---
title: System Tray Menus
description: Add system tray (notification area) integration to your application
sidebar:
order: 3
---
import { Tabs, TabItem, Card, CardGrid } from "@astrojs/starlight/components";
## System Tray Menus
Wails provides **unified system tray APIs** that work across all platforms. Create tray icons with menus, attach windows, and handle clicks with native platform behaviour for background applications, services, and quick-access utilities.
{/* VISUAL PLACEHOLDER: System Tray Comparison
Description: Three screenshots showing the same Wails system tray icon on:
1. Windows - Notification area (bottom-right)
2. macOS - Menu bar (top-right) with label
3. Linux (GNOME) - Top bar
All showing the same icon and menu structure
Style: Clean screenshots with arrows pointing to tray icon, menu expanded
*/}
## Quick Start
```go
package main
import (
_ "embed"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed assets/icon.png
var icon []byte
func main() {
app := application.New(application.Options{
Name: "Tray App",
})
// Create system tray
systray := app.NewSystemTray()
systray.SetIcon(icon)
systray.SetLabel("My App")
// Add menu
menu := app.NewMenu()
menu.Add("Show").OnClick(func(ctx *application.Context) {
// Show main window
})
menu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
systray.SetMenu(menu)
// Create hidden window
window := app.NewWebviewWindow()
window.Hide()
app.Run()
}
```
**Result:** System tray icon with menu on all platforms.
## Creating a System Tray
### Basic System Tray
```go
// Create system tray
systray := app.NewSystemTray()
// Set icon
systray.SetIcon(iconBytes)
// Set label (macOS) / tooltip (Windows)
systray.SetLabel("My Application")
```
### With Icon
Icons should be embedded:
```go
import _ "embed"
//go:embed assets/icon.png
var icon []byte
//go:embed assets/icon-dark.png
var iconDark []byte
func main() {
app := application.New(application.Options{
Name: "My App",
})
systray := app.NewSystemTray()
systray.SetIcon(icon)
systray.SetDarkModeIcon(iconDark) // macOS dark mode
app.Run()
}
```
**Icon requirements:**
| Platform | Size | Format | Notes |
|----------|------|--------|-------|
| **Windows** | 16x16 or 32x32 | PNG, ICO | Notification area |
| **macOS** | 18x18 to 22x22 | PNG | Menu bar, template recommended |
| **Linux** | 22x22 to 48x48 | PNG, SVG | Varies by DE |
### Template Icons (macOS)
Template icons adapt to light/dark mode automatically:
```go
systray.SetTemplateIcon(iconBytes)
```
**Template icon guidelines:**
- Use black and clear (transparent) colours only
- Black becomes white in dark mode
- Name file with `Template` suffix: `iconTemplate.png`
- [Design guide](https://bjango.com/articles/designingmenubarextras/)
## Adding Menus
System tray menus work like application menus:
```go
menu := app.NewMenu()
// Add items
menu.Add("Open").OnClick(func(ctx *application.Context) {
showMainWindow()
})
menu.AddSeparator()
menu.AddCheckbox("Start at Login", false).OnClick(func(ctx *application.Context) {
enabled := ctx.ClickedMenuItem().Checked()
setStartAtLogin(enabled)
})
menu.AddSeparator()
menu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
// Set menu
systray.SetMenu(menu)
```
**For all menu item types**, see [Menu Reference](/features/menus/reference).
## Attaching Windows
Attach a window to the tray icon for automatic show/hide:
```go
// Create window
window := app.NewWebviewWindow()
// Attach to tray
systray.AttachWindow(window)
// Configure behaviour
systray.SetWindowOffset(10) // Pixels from tray icon
systray.SetWindowDebounce(200 * time.Millisecond) // Click debounce
```
**Behaviour:**
- Window starts hidden
- **Left-click tray icon** → Toggle window visibility
- **Right-click tray icon** → Show menu (if set)
- Window positioned near tray icon
**Example: Popup window**
```go
window := app.NewWebviewWindow(application.WebviewWindowOptions{
Title: "Quick Access",
Width: 300,
Height: 400,
Frameless: true, // No title bar
AlwaysOnTop: true, // Stay on top
})
systray.AttachWindow(window)
systray.SetWindowOffset(5)
```
## Click Handlers
Handle tray icon clicks:
```go
systray := app.NewSystemTray()
// Left click
systray.OnClick(func() {
fmt.Println("Tray icon clicked")
})
// Right click
systray.OnRightClick(func() {
fmt.Println("Tray icon right-clicked")
})
// Double click
systray.OnDoubleClick(func() {
fmt.Println("Tray icon double-clicked")
})
// Mouse enter/leave
systray.OnMouseEnter(func() {
fmt.Println("Mouse entered tray icon")
})
systray.OnMouseLeave(func() {
fmt.Println("Mouse left tray icon")
})
```
**Platform support:**
| Event | Windows | macOS | Linux |
|-------|---------|-------|-------|
| OnClick | ✅ | ✅ | ✅ |
| OnRightClick | ✅ | ✅ | ✅ |
| OnDoubleClick | ✅ | ✅ | ⚠️ Varies |
| OnMouseEnter | ✅ | ✅ | ⚠️ Varies |
| OnMouseLeave | ✅ | ✅ | ⚠️ Varies |
## Dynamic Updates
Update tray icon and menu dynamically:
### Change Icon
```go
var isActive bool
func updateTrayIcon() {
if isActive {
systray.SetIcon(activeIcon)
systray.SetLabel("Active")
} else {
systray.SetIcon(inactiveIcon)
systray.SetLabel("Inactive")
}
}
```
### Update Menu
```go
var isPaused bool
pauseMenuItem := menu.Add("Pause")
pauseMenuItem.OnClick(func(ctx *application.Context) {
isPaused = !isPaused
if isPaused {
pauseMenuItem.SetLabel("Resume")
} else {
pauseMenuItem.SetLabel("Pause")
}
menu.Update() // Important!
})
```
:::caution[Always Call Update()]
After changing menu state, **call `menu.Update()`**. See [Menu Reference](/features/menus/reference#enabled-state).
:::
### Rebuild Menu
For major changes, rebuild the entire menu:
```go
func rebuildTrayMenu(status string) {
menu := app.NewMenu()
// Status-specific items
switch status {
case "syncing":
menu.Add("Syncing...").SetEnabled(false)
menu.Add("Pause Sync").OnClick(pauseSync)
case "synced":
menu.Add("Up to date ✓").SetEnabled(false)
menu.Add("Sync Now").OnClick(startSync)
case "error":
menu.Add("Sync Error").SetEnabled(false)
menu.Add("Retry").OnClick(retrySync)
}
menu.AddSeparator()
menu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
systray.SetMenu(menu)
}
```
## Platform-Specific Features
<Tabs syncKey="platform">
<TabItem label="macOS" icon="apple">
**Menu bar integration:**
```go
// Set label (appears next to icon)
systray.SetLabel("My App")
// Use template icon (adapts to dark mode)
systray.SetTemplateIcon(iconBytes)
// Set icon position
systray.SetIconPosition(application.IconPositionRight)
```
**Icon positions:**
- `IconPositionLeft` - Icon left of label
- `IconPositionRight` - Icon right of label
- `IconPositionOnly` - Icon only, no label
- `IconPositionNone` - Label only, no icon
**Best practices:**
- Use template icons (black + transparent)
- Keep labels short (3-5 characters)
- 18x18 to 22x22 pixels for Retina displays
- Test in both light and dark modes
</TabItem>
<TabItem label="Windows" icon="seti:windows">
**Notification area integration:**
```go
// Set tooltip (appears on hover)
systray.SetTooltip("My Application")
// Or use SetLabel (same as tooltip on Windows)
systray.SetLabel("My Application")
// Show/Hide functionality (fully functional)
systray.Show() // Show tray icon
systray.Hide() // Hide tray icon
```
**Icon requirements:**
- 16x16 or 32x32 pixels
- PNG or ICO format
- Transparent background
**Tooltip limits:**
- Maximum 127 UTF-16 characters
- Longer tooltips will be truncated
- Keep concise for best experience
**Platform features:**
- Tray icon survives Windows Explorer restarts
- Show() and Hide() methods fully functional
- Proper lifecycle management
**Best practices:**
- Use 32x32 for high-DPI displays
- Keep tooltips under 127 characters
- Test on different Windows versions
- Consider notification area overflow
- Use Show/Hide for conditional tray visibility
</TabItem>
<TabItem label="Linux" icon="linux">
**System tray integration:**
Uses StatusNotifierItem specification (most modern DEs).
```go
systray.SetIcon(iconBytes)
systray.SetLabel("My App")
```
**Desktop environment support:**
- **GNOME**: Top bar (with extension)
- **KDE Plasma**: System tray
- **XFCE**: Notification area
- **Others**: Varies
**Best practices:**
- Use 22x22 or 24x24 pixels
- SVG icons scale better
- Test on target desktop environments
- Provide fallback for unsupported DEs
</TabItem>
</Tabs>
## Complete Example
Here's a production-ready system tray application:
```go
package main
import (
_ "embed"
"fmt"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed assets/icon.png
var icon []byte
//go:embed assets/icon-active.png
var iconActive []byte
type TrayApp struct {
app *application.Application
systray *application.SystemTray
window *application.WebviewWindow
menu *application.Menu
isActive bool
}
func main() {
app := application.New(application.Options{
Name: "Tray Application",
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
})
trayApp := &TrayApp{app: app}
trayApp.setup()
app.Run()
}
func (t *TrayApp) setup() {
// Create system tray
t.systray = t.app.NewSystemTray()
t.systray.SetIcon(icon)
t.systray.SetLabel("Inactive")
// Create menu
t.createMenu()
// Create window (hidden by default)
t.window = t.app.NewWebviewWindow(application.WebviewWindowOptions{
Title: "Tray Application",
Width: 400,
Height: 600,
Hidden: true,
})
// Attach window to tray
t.systray.AttachWindow(t.window)
t.systray.SetWindowOffset(10)
// Handle tray clicks
t.systray.OnRightClick(func() {
t.systray.OpenMenu()
})
// Start background task
go t.backgroundTask()
}
func (t *TrayApp) createMenu() {
t.menu = t.app.NewMenu()
// Status item (disabled)
statusItem := t.menu.Add("Status: Inactive")
statusItem.SetEnabled(false)
t.menu.AddSeparator()
// Toggle active
t.menu.Add("Start").OnClick(func(ctx *application.Context) {
t.toggleActive()
})
// Show window
t.menu.Add("Show Window").OnClick(func(ctx *application.Context) {
t.window.Show()
t.window.SetFocus()
})
t.menu.AddSeparator()
// Settings
t.menu.AddCheckbox("Start at Login", false).OnClick(func(ctx *application.Context) {
enabled := ctx.ClickedMenuItem().Checked()
t.setStartAtLogin(enabled)
})
t.menu.AddSeparator()
// Quit
t.menu.Add("Quit").OnClick(func(ctx *application.Context) {
t.app.Quit()
})
t.systray.SetMenu(t.menu)
}
func (t *TrayApp) toggleActive() {
t.isActive = !t.isActive
t.updateTray()
}
func (t *TrayApp) updateTray() {
if t.isActive {
t.systray.SetIcon(iconActive)
t.systray.SetLabel("Active")
} else {
t.systray.SetIcon(icon)
t.systray.SetLabel("Inactive")
}
// Rebuild menu with new status
t.createMenu()
}
func (t *TrayApp) backgroundTask() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
if t.isActive {
fmt.Println("Background task running...")
// Do work
}
}
}
func (t *TrayApp) setStartAtLogin(enabled bool) {
// Implementation varies by platform
fmt.Printf("Start at login: %v\n", enabled)
}
```
## Visibility Control
Show/hide the tray icon dynamically:
```go
// Hide tray icon
systray.Hide()
// Show tray icon
systray.Show()
// Check visibility
if systray.IsVisible() {
fmt.Println("Tray icon is visible")
}
```
**Platform Support:**
| Platform | Hide() | Show() | Notes |
|----------|--------|--------|-------|
| **Windows** | ✅ | ✅ | Fully functional - icon appears/disappears from notification area |
| **macOS** | ✅ | ✅ | Menu bar item shows/hides |
| **Linux** | ✅ | ✅ | Varies by desktop environment |
**Use cases:**
- Temporarily hide tray icon based on user preference
- Headless mode with tray icon appearing only when needed
- Toggle visibility based on application state
**Example - Conditional Tray Visibility:**
```go
func (t *TrayApp) setTrayVisibility(visible bool) {
if visible {
t.systray.Show()
} else {
t.systray.Hide()
}
}
// Show tray only when updates are available
func (t *TrayApp) checkForUpdates() {
if hasUpdates {
t.systray.Show()
t.systray.SetLabel("Update Available")
} else {
t.systray.Hide()
}
}
```
## Cleanup
Destroy the tray icon when done:
```go
// In OnShutdown
app := application.New(application.Options{
OnShutdown: func() {
if systray != nil {
systray.Destroy()
}
},
})
```
**Important:** Always destroy system tray on shutdown to release resources.
## Best Practices
### ✅ Do
- **Use template icons on macOS** - Adapts to dark mode
- **Keep labels short** - 3-5 characters maximum
- **Provide tooltips on Windows** - Helps users identify your app
- **Test on all platforms** - Behaviour varies
- **Handle clicks appropriately** - Left-click for main action, right-click for menu
- **Update icon for status** - Visual feedback is important
- **Destroy on shutdown** - Release resources
### ❌ Don't
- **Don't use large icons** - Follow platform guidelines
- **Don't use long labels** - Gets truncated
- **Don't forget dark mode** - Test on macOS dark mode
- **Don't block click handlers** - Keep them fast
- **Don't forget menu.Update()** - After changing menu state
- **Don't assume tray support** - Some Linux DEs don't support it
## Troubleshooting
### Tray Icon Not Appearing
**Possible causes:**
1. Icon format not supported
2. Icon size too large/small
3. System tray not supported (Linux)
**Solution:**
```go
// Check if system tray is supported
if !application.SystemTraySupported() {
fmt.Println("System tray not supported")
// Fallback to window-only mode
}
```
### Icon Looks Wrong on macOS
**Cause:** Not using template icon
**Solution:**
```go
// Use template icon
systray.SetTemplateIcon(iconBytes)
// Or design icon as template (black + transparent)
```
### Menu Not Updating
**Cause:** Forgot to call `menu.Update()`
**Solution:**
```go
menuItem.SetLabel("New Label")
menu.Update() // Add this!
```
## Next Steps
<CardGrid>
<Card title="Menu Reference" icon="document">
Complete reference for menu item types and properties.
[Learn More →](/features/menus/reference)
</Card>
<Card title="Application Menus" icon="list-format">
Create application menu bars.
[Learn More →](/features/menus/application)
</Card>
<Card title="Context Menus" icon="puzzle">
Create right-click context menus.
[Learn More →](/features/menus/context)
</Card>
<Card title="System Tray Tutorial" icon="open-book">
Build a complete system tray application.
[Learn More →](/tutorials/system-tray)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [system tray examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/systray-basic).

View file

@ -1,5 +1,8 @@
---
title: Notifications
description: Display native system notifications with action buttons and text input
sidebar:
order: 1
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
@ -16,7 +19,7 @@ First, initialize the notifications service:
```go
import "github.com/wailsapp/wails/v3/pkg/application"
import "github.com/wailsapp/wails/v3/services/notifications"
import "github.com/wailsapp/wails/v3/pkg/services/notifications"
// Create a new notification service
notifier := notifications.New()

View file

@ -0,0 +1,235 @@
---
title: Dock & Taskbar
description: Manage dock icon visibility and display badges on macOS and Windows
sidebar:
order: 1
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
## Introduction
Wails provides a cross-platform Dock service for desktop applications. This service allows you to:
- Hide and show the application icon in the macOS Dock
- Display badges on your application tile or dock/taskbar icon (macOS and Windows)
## Basic Usage
### Creating the Service
First, initialize the dock service:
```go
import "github.com/wailsapp/wails/v3/pkg/application"
import "github.com/wailsapp/wails/v3/pkg/services/dock"
// Create a new Dock service
dockService := dock.New()
// Register the service with the application
app := application.New(application.Options{
Services: []application.Service{
application.NewService(dockService),
},
})
```
### Creating the Service with Custom Badge Options (Windows Only)
On Windows, you can customize the badge appearance with various options:
```go
import "github.com/wailsapp/wails/v3/pkg/application"
import "github.com/wailsapp/wails/v3/pkg/services/dock"
import "image/color"
// Create a dock service with custom badge options
options := dock.BadgeOptions{
TextColour: color.RGBA{255, 255, 255, 255}, // White text
BackgroundColour: color.RGBA{0, 0, 255, 255}, // Blue background
FontName: "consolab.ttf", // Bold Consolas font
FontSize: 20, // Font size for single character
SmallFontSize: 14, // Font size for multiple characters
}
dockService := dock.NewWithOptions(options)
// Register the service with the application
app := application.New(application.Options{
Services: []application.Service{
application.NewService(dockService),
},
})
```
## Dock Operations
### Hiding the dock app icon
Hide the app icon from the macOS Dock:
```go
// Hide the app icon
dockService.HideAppIcon()
```
### Showing the dock app icon
Show the app icon in the macOS Dock:
```go
// Show the app icon
dockService.ShowAppIcon()
```
## Badge Operations
### Setting a Badge
Set a badge on the application tile/dock icon:
```go
// Set a default badge
dockService.SetBadge("")
// Set a numeric badge
dockService.SetBadge("3")
// Set a text badge
dockService.SetBadge("New")
```
### Setting a Custom Badge (Windows Only)
Set a badge with one-off options applied:
```go
options := dock.BadgeOptions{
BackgroundColour: color.RGBA{0, 255, 255, 255},
FontName: "arialb.ttf", // System font
FontSize: 16,
SmallFontSize: 10,
TextColour: color.RGBA{0, 0, 0, 255},
}
// Set a default badge
dockService.SetCustomBadge("", options)
// Set a numeric badge
dockService.SetCustomBadge("3", options)
// Set a text badge
dockService.SetCustomBadge("New", options)
```
### Removing a Badge
Remove the badge from the application icon:
```go
dockService.RemoveBadge()
```
## Platform Considerations
<Tabs>
<TabItem label="macOS" icon="fa-brands:apple">
On macOS:
- The dock icon can be **hidden** and **shown**
- Badges are displayed directly on the dock icon
- Badge options are **not customizable** (any options passed to `NewWithOptions`/`SetCustomBadge` are ignored)
- The standard macOS dock badge styling is used and automatically adapts to appearance
- Label overflow is handled by the system
- Providing an empty label displays a default badge of "●"
</TabItem>
<TabItem label="Windows" icon="fa-brands:windows">
On Windows:
- Hiding/showing the taskbar icon is not currently supported by this service
- Badges are displayed as an overlay icon in the taskbar
- Badges support text values
- Badge appearance can be customized via `BadgeOptions`
- The application must have a window for badges to display
- A smaller font size is automatically used for multi-character labels
- Label overflow is not handled
- Customization options:
- **TextColour**: Text color (default: white)
- **BackgroundColour**: Badge background color (default: red)
- **FontName**: Font file name (default: "segoeuib.ttf")
- **FontSize**: Font size for single character (default: 18)
- **SmallFontSize**: Font size for multiple characters (default: 14)
</TabItem>
<TabItem label="Linux" icon="fa-brands:linux">
On Linux:
- Dock icon visibility and badge functionality are not available
</TabItem>
</Tabs>
## Best Practices
1. **When hiding the dock icon (macOS):**
- Ensure users can still access your app (e.g., via [system tray](https://v3alpha.wails.io/learn/systray/))
- Include a "Quit" option in your alternative UI
- The app won't appear in Command+Tab switcher
- Open windows remain visible and functional
- Closing all windows may not quit the app (macOS behavior varies)
- Users lose the standard way to quit via Dock right-click
2. **Use badges sparingly:**
- Too many badge updates can distract users
- Reserve badges for important notifications
3. **Keep badge text short:**
- Numeric badges are most effective
- On macOS, text badges should be brief
4. **For Windows badge customization:**
- Ensure high contrast between text and background colors
- Test with different text lengths as font size decreases with length
- Use common system fonts to ensure availability
## API Reference
### Service Management
| Method | Description |
|--------------------------------------------|-------------------------------------------------------|
| `New()` | Creates a new dock service |
| `NewWithOptions(options BadgeOptions)` | Creates a new dock service with custom badge options (Windows only; options are ignored on macOS and Linux) |
### Dock Operations
| Method | Description |
|--------------------------------|-------------------------------------------------------------|
| `HideAppIcon()` | Hides the app icon from the macOS Dock (macOS only) |
| `ShowAppIcon()` | Shows the app icon in the macOS Dock (macOS only) |
### Badge Operations
| Method | Description |
|---------------------------------------------------|------------------------------------------------------------|
| `SetBadge(label string) error` | Sets a badge with the specified label |
| `SetCustomBadge(label string, options BadgeOptions) error` | Sets a badge with the specified label and custom styling options (Windows only) |
| `RemoveBadge() error` | Removes the badge from the application icon |
### Structs and Types
```go
// Options for customizing badge appearance (Windows only)
type BadgeOptions struct {
TextColour color.RGBA // Color of the badge text
BackgroundColour color.RGBA // Color of the badge background
FontName string // Font file name (e.g., "segoeuib.ttf")
FontSize int // Font size for single character
SmallFontSize int // Font size for multiple characters
}
```

View file

@ -0,0 +1,466 @@
---
title: Screen Information
description: Get information about displays and monitors
sidebar:
order: 1
---
import { Card, CardGrid } from "@astrojs/starlight/components";
## Screen Information
Wails provides a **unified screen API** that works across all platforms. Get screen information, detect multiple monitors, query screen properties (size, position, DPI), identify the primary display, and handle DPI scaling with consistent code.
## Quick Start
```go
// Get all screens
screens := app.Screens.GetAll()
for _, screen := range screens {
fmt.Printf("Screen: %s (%dx%d)\n",
screen.Name, screen.Width, screen.Height)
}
// Get primary screen
primary := app.Screens.GetPrimary()
fmt.Printf("Primary: %s\n", primary.Name)
```
**That's it!** Cross-platform screen information.
## Getting Screen Information
### All Screens
```go
screens := app.Screens.GetAll()
for _, screen := range screens {
fmt.Printf("ID: %s\n", screen.ID)
fmt.Printf("Name: %s\n", screen.Name)
fmt.Printf("Size: %dx%d\n", screen.Width, screen.Height)
fmt.Printf("Position: %d,%d\n", screen.X, screen.Y)
fmt.Printf("Scale: %.2f\n", screen.ScaleFactor)
fmt.Printf("Primary: %v\n", screen.IsPrimary)
fmt.Println("---")
}
```
### Primary Screen
```go
primary := app.Screens.GetPrimary()
fmt.Printf("Primary screen: %s\n", primary.Name)
fmt.Printf("Resolution: %dx%d\n", primary.Width, primary.Height)
fmt.Printf("Scale factor: %.2f\n", primary.ScaleFactor)
```
### Current Screen
Get the screen containing a window:
```go
screen := app.Screens.GetCurrent(window)
fmt.Printf("Window is on: %s\n", screen.Name)
```
### Screen by ID
```go
screen := app.Screens.GetByID("screen-id")
if screen != nil {
fmt.Printf("Found screen: %s\n", screen.Name)
}
```
## Screen Properties
### Screen Structure
```go
type Screen struct {
ID string // Unique identifier
Name string // Display name
X int // X position
Y int // Y position
Width int // Width in pixels
Height int // Height in pixels
ScaleFactor float32 // DPI scale (1.0, 1.5, 2.0, etc.)
IsPrimary bool // Is this the primary screen?
}
```
### Physical vs Logical Pixels
```go
screen := app.Screens.GetPrimary()
// Logical pixels (what you use)
logicalWidth := screen.Width
logicalHeight := screen.Height
// Physical pixels (actual display)
physicalWidth := int(float32(screen.Width) * screen.ScaleFactor)
physicalHeight := int(float32(screen.Height) * screen.ScaleFactor)
fmt.Printf("Logical: %dx%d\n", logicalWidth, logicalHeight)
fmt.Printf("Physical: %dx%d\n", physicalWidth, physicalHeight)
fmt.Printf("Scale: %.2f\n", screen.ScaleFactor)
```
**Common scale factors:**
- `1.0` - Standard DPI (96 DPI)
- `1.25` - 125% scaling (120 DPI)
- `1.5` - 150% scaling (144 DPI)
- `2.0` - 200% scaling (192 DPI) - Retina
- `3.0` - 300% scaling (288 DPI) - 4K/5K
## Window Positioning
### Centre on Screen
```go
func centreOnScreen(window *application.WebviewWindow, screen *Screen) {
windowWidth, windowHeight := window.Size()
x := screen.X + (screen.Width-windowWidth)/2
y := screen.Y + (screen.Height-windowHeight)/2
window.SetPosition(x, y)
}
```
### Position on Specific Screen
```go
func moveToScreen(window *application.WebviewWindow, screenIndex int) {
screens := app.Screens.GetAll()
if screenIndex < 0 || screenIndex >= len(screens) {
return
}
screen := screens[screenIndex]
// Centre on target screen
centreOnScreen(window, screen)
}
```
### Position Relative to Screen
```go
// Top-left corner
func positionTopLeft(window *application.WebviewWindow, screen *Screen) {
window.SetPosition(screen.X+10, screen.Y+10)
}
// Top-right corner
func positionTopRight(window *application.WebviewWindow, screen *Screen) {
windowWidth, _ := window.Size()
window.SetPosition(screen.X+screen.Width-windowWidth-10, screen.Y+10)
}
// Bottom-right corner
func positionBottomRight(window *application.WebviewWindow, screen *Screen) {
windowWidth, windowHeight := window.Size()
window.SetPosition(
screen.X+screen.Width-windowWidth-10,
screen.Y+screen.Height-windowHeight-10,
)
}
```
## Multi-Monitor Support
### Detect Multiple Monitors
```go
func hasMultipleMonitors() bool {
return len(app.Screens.GetAll()) > 1
}
func getMonitorCount() int {
return len(app.Screens.GetAll())
}
```
### List All Monitors
```go
func listMonitors() {
screens := app.Screens.GetAll()
fmt.Printf("Found %d monitor(s):\n", len(screens))
for i, screen := range screens {
primary := ""
if screen.IsPrimary {
primary = " (Primary)"
}
fmt.Printf("%d. %s%s\n", i+1, screen.Name, primary)
fmt.Printf(" Resolution: %dx%d\n", screen.Width, screen.Height)
fmt.Printf(" Position: %d,%d\n", screen.X, screen.Y)
fmt.Printf(" Scale: %.2fx\n", screen.ScaleFactor)
}
}
```
### Choose Monitor
```go
func chooseMonitor() (*Screen, error) {
screens := app.Screens.GetAll()
if len(screens) == 1 {
return screens[0], nil
}
// Show dialog to choose
var options []string
for i, screen := range screens {
primary := ""
if screen.IsPrimary {
primary = " (Primary)"
}
options = append(options,
fmt.Sprintf("%d. %s%s - %dx%d",
i+1, screen.Name, primary, screen.Width, screen.Height))
}
// Use dialog to select
// (Implementation depends on your dialog system)
return screens[0], nil
}
```
## Complete Examples
### Multi-Monitor Window Manager
```go
type MultiMonitorManager struct {
app *application.Application
windows map[int]*application.WebviewWindow
}
func NewMultiMonitorManager(app *application.Application) *MultiMonitorManager {
return &MultiMonitorManager{
app: app,
windows: make(map[int]*application.WebviewWindow),
}
}
func (m *MultiMonitorManager) CreateWindowOnScreen(screenIndex int) error {
screens := m.app.Screens.GetAll()
if screenIndex < 0 || screenIndex >= len(screens) {
return errors.New("invalid screen index")
}
screen := screens[screenIndex]
// Create window
window := m.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: fmt.Sprintf("Window on %s", screen.Name),
Width: 800,
Height: 600,
})
// Centre on screen
x := screen.X + (screen.Width-800)/2
y := screen.Y + (screen.Height-600)/2
window.SetPosition(x, y)
window.Show()
m.windows[screenIndex] = window
return nil
}
func (m *MultiMonitorManager) CreateWindowOnEachScreen() {
screens := m.app.Screens.GetAll()
for i := range screens {
m.CreateWindowOnScreen(i)
}
}
```
### Screen Change Detection
```go
type ScreenMonitor struct {
app *application.Application
lastScreens []*Screen
changeHandler func([]*Screen)
}
func NewScreenMonitor(app *application.Application) *ScreenMonitor {
return &ScreenMonitor{
app: app,
lastScreens: app.Screens.GetAll(),
}
}
func (sm *ScreenMonitor) OnScreenChange(handler func([]*Screen)) {
sm.changeHandler = handler
}
func (sm *ScreenMonitor) Start() {
ticker := time.NewTicker(2 * time.Second)
go func() {
for range ticker.C {
sm.checkScreens()
}
}()
}
func (sm *ScreenMonitor) checkScreens() {
current := sm.app.Screens.GetAll()
if len(current) != len(sm.lastScreens) {
sm.lastScreens = current
if sm.changeHandler != nil {
sm.changeHandler(current)
}
}
}
```
### DPI-Aware Window Sizing
```go
func createDPIAwareWindow(screen *Screen) *application.WebviewWindow {
// Base size at 1.0 scale
baseWidth := 800
baseHeight := 600
// Adjust for DPI
width := int(float32(baseWidth) * screen.ScaleFactor)
height := int(float32(baseHeight) * screen.ScaleFactor)
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "DPI-Aware Window",
Width: width,
Height: height,
})
// Centre on screen
x := screen.X + (screen.Width-width)/2
y := screen.Y + (screen.Height-height)/2
window.SetPosition(x, y)
return window
}
```
### Screen Layout Visualiser
```go
func visualiseScreenLayout() string {
screens := app.Screens.GetAll()
var layout strings.Builder
layout.WriteString("Screen Layout:\n\n")
for i, screen := range screens {
primary := ""
if screen.IsPrimary {
primary = " [PRIMARY]"
}
layout.WriteString(fmt.Sprintf("Screen %d: %s%s\n", i+1, screen.Name, primary))
layout.WriteString(fmt.Sprintf(" Position: (%d, %d)\n", screen.X, screen.Y))
layout.WriteString(fmt.Sprintf(" Size: %dx%d\n", screen.Width, screen.Height))
layout.WriteString(fmt.Sprintf(" Scale: %.2fx\n", screen.ScaleFactor))
layout.WriteString(fmt.Sprintf(" Physical: %dx%d\n",
int(float32(screen.Width)*screen.ScaleFactor),
int(float32(screen.Height)*screen.ScaleFactor)))
layout.WriteString("\n")
}
return layout.String()
}
```
## Best Practices
### ✅ Do
- **Check screen count** - Handle single and multiple monitors
- **Use logical pixels** - Wails handles DPI automatically
- **Centre windows** - Better UX than fixed positions
- **Validate positions** - Ensure windows are visible
- **Handle screen changes** - Monitors can be added/removed
- **Test on different DPI** - 100%, 125%, 150%, 200%
### ❌ Don't
- **Don't hardcode positions** - Use screen dimensions
- **Don't assume primary screen** - User might have multiple
- **Don't ignore scale factor** - Important for DPI awareness
- **Don't position off-screen** - Validate coordinates
- **Don't forget screen changes** - Laptops dock/undock
- **Don't use physical pixels** - Use logical pixels
## Platform Differences
### macOS
- Retina displays (2x scale factor)
- Multiple displays common
- Coordinate system: (0,0) at bottom-left
- Spaces (virtual desktops) affect positioning
### Windows
- Various DPI scaling (100%, 125%, 150%, 200%)
- Multiple displays common
- Coordinate system: (0,0) at top-left
- Per-monitor DPI awareness
### Linux
- Varies by desktop environment
- X11 vs Wayland differences
- DPI scaling support varies
- Multiple displays supported
## Next Steps
<CardGrid>
<Card title="Windows" icon="laptop">
Learn about window management.
[Learn More →](/features/windows/basics)
</Card>
<Card title="Window Options" icon="seti:config">
Configure window appearance.
[Learn More →](/features/windows/options)
</Card>
<Card title="Multiple Windows" icon="puzzle">
Multi-window patterns.
[Learn More →](/features/windows/multiple)
</Card>
<Card title="Bindings" icon="rocket">
Call Go functions from JavaScript.
[Learn More →](/features/bindings/methods)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [screen examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).

View file

@ -0,0 +1,607 @@
---
title: Window Basics
description: Creating and managing application windows in Wails
sidebar:
order: 1
---
import { Tabs, TabItem, Card, CardGrid } from "@astrojs/starlight/components";
## Window Management
Wails provides a **unified window management API** that works across all platforms. Create windows, control their behaviour, and manage multiple windows with full control over creation, appearance, behaviour, and lifecycle.
## Quick Start
```go
package main
import "github.com/wailsapp/wails/v3/pkg/application"
func main() {
app := application.New(application.Options{
Name: "My App",
})
// Create a window
window := app.NewWebviewWindow()
// Configure it
window.SetTitle("Hello Wails")
window.SetSize(800, 600)
window.Center()
// Show it
window.Show()
app.Run()
}
```
**That's it!** You have a cross-platform window.
## Creating Windows
### Basic Window
The simplest way to create a window:
```go
window := app.NewWebviewWindow()
```
**What you get:**
- Default size (800x600)
- Default title (application name)
- WebView ready for your frontend
- Platform-native appearance
### Window with Options
Create a window with custom configuration:
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "My Application",
Width: 1200,
Height: 800,
X: 100, // Position from left
Y: 100, // Position from top
AlwaysOnTop: false,
Frameless: false,
Hidden: false,
MinWidth: 400,
MinHeight: 300,
MaxWidth: 1920,
MaxHeight: 1080,
})
```
**Common options:**
| Option | Type | Description |
|--------|------|-------------|
| `Title` | `string` | Window title |
| `Width` | `int` | Window width in pixels |
| `Height` | `int` | Window height in pixels |
| `X` | `int` | X position (from left) |
| `Y` | `int` | Y position (from top) |
| `AlwaysOnTop` | `bool` | Keep window above others |
| `Frameless` | `bool` | Remove title bar and borders |
| `Hidden` | `bool` | Start hidden |
| `MinWidth` | `int` | Minimum width |
| `MinHeight` | `int` | Minimum height |
| `MaxWidth` | `int` | Maximum width |
| `MaxHeight` | `int` | Maximum height |
**See [Window Options](/features/windows/options) for complete list.**
### Named Windows
Give windows names for easy retrieval:
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "main-window",
Title: "Main Application",
})
// Later, find it by name
mainWindow := app.GetWindowByName("main-window")
if mainWindow != nil {
mainWindow.Show()
}
```
**Use cases:**
- Multiple windows (main, settings, about)
- Finding windows from different parts of your code
- Window communication
## Controlling Windows
### Show and Hide
```go
// Show window
window.Show()
// Hide window
window.Hide()
// Check if visible
if window.IsVisible() {
fmt.Println("Window is visible")
}
```
**Use cases:**
- Splash screens (show, then hide)
- Settings windows (hide when not needed)
- Popup windows (show on demand)
### Position and Size
```go
// Set size
window.SetSize(1024, 768)
// Set position
window.SetPosition(100, 100)
// Centre on screen
window.Center()
// Get current size
width, height := window.Size()
// Get current position
x, y := window.Position()
```
**Coordinate system:**
- (0, 0) is top-left of primary screen
- Positive X goes right
- Positive Y goes down
### Window State
```go
// Minimise
window.Minimise()
// Maximise
window.Maximise()
// Fullscreen
window.Fullscreen()
// Restore to normal
window.Restore()
// Check state
if window.IsMinimised() {
fmt.Println("Window is minimised")
}
if window.IsMaximised() {
fmt.Println("Window is maximised")
}
if window.IsFullscreen() {
fmt.Println("Window is fullscreen")
}
```
**State transitions:**
```
Normal ←→ Minimised
Normal ←→ Maximised
Normal ←→ Fullscreen
```
### Title and Appearance
```go
// Set title
window.SetTitle("My Application - Document.txt")
// Set background colour
window.SetBackgroundColour(0, 0, 0, 255) // RGBA
// Set always on top
window.SetAlwaysOnTop(true)
// Set resizable
window.SetResizable(false)
```
### Closing Windows
```go
// Close window
window.Close()
// Destroy window (force close)
window.Destroy()
```
**Difference:**
- `Close()` - Triggers close event, can be cancelled
- `Destroy()` - Immediate destruction, cannot be cancelled
## Finding Windows
### By Name
```go
window := app.GetWindowByName("settings")
if window != nil {
window.Show()
}
```
### By ID
Every window has a unique ID:
```go
id := window.ID()
fmt.Printf("Window ID: %d\n", id)
// Find by ID
foundWindow := app.GetWindowByID(id)
```
### Current Window
Get the currently focused window:
```go
current := app.CurrentWindow()
if current != nil {
current.SetTitle("Active Window")
}
```
### All Windows
Get all windows:
```go
windows := app.GetAllWindows()
fmt.Printf("Total windows: %d\n", len(windows))
for _, w := range windows {
fmt.Printf("Window: %s (ID: %d)\n", w.Name(), w.ID())
}
```
## Window Lifecycle
### Creation
```go
app.OnWindowCreation(func(window *application.WebviewWindow) {
fmt.Printf("Window created: %s\n", window.Name())
// Configure new windows
window.SetMinSize(400, 300)
})
```
### Closing
```go
window.OnClose(func() bool {
// Return false to cancel close
// Return true to allow close
if hasUnsavedChanges() {
result := showConfirmdialog("Unsaved changes. Close anyway?")
return result == "yes"
}
return true
})
```
**Important:** `OnClose` only works for user-initiated closes (clicking X button). It doesn't prevent `window.Destroy()`.
### Destruction
```go
window.OnDestroy(func() {
fmt.Println("Window destroyed")
// Cleanup resources
})
```
## Multiple Windows
### Creating Multiple Windows
```go
// Main window
mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Main Application",
Width: 1200,
Height: 800,
})
// Settings window
settingsWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
Height: 400,
Hidden: true, // Start hidden
})
// Show settings when needed
settingsWindow.Show()
```
### Window Communication
Windows can communicate via events:
```go
// In main window
app.EmitEvent("data-updated", map[string]interface{}{
"value": 42,
})
// In settings window
app.OnEvent("data-updated", func(event *application.WailsEvent) {
data := event.Data.(map[string]interface{})
value := data["value"].(int)
fmt.Printf("Received: %d\n", value)
})
```
**See [Events](/features/events/system) for more.**
### Parent-Child Windows
```go
// Create child window
childWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Child Window",
Parent: mainWindow, // Set parent
})
```
**Behaviour:**
- Child closes when parent closes
- Child stays above parent (on some platforms)
- Child minimises with parent (on some platforms)
**Platform support:**
- **macOS:** Full support
- **Windows:** Partial support
- **Linux:** Varies by desktop environment
## Platform-Specific Features
<Tabs syncKey="platform">
<TabItem label="Windows" icon="seti:windows">
**Windows-specific features:**
```go
// Flash taskbar button
window.Flash(true) // Start flashing
window.Flash(false) // Stop flashing
// Trigger Windows 11 Snap Assist (Win+Z)
window.SnapAssist()
// Set window icon
window.SetIcon(iconBytes)
```
**Snap Assist:**
Shows Windows 11 snap layout options for the window.
**Taskbar flashing:**
Useful for notifications when window is minimised.
</TabItem>
<TabItem label="macOS" icon="apple">
**macOS-specific features:**
```go
// Transparent title bar
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Mac: application.MacOptions{
TitleBarAppearsTransparent: true,
Backdrop: application.MacBackdropTranslucent,
},
})
```
**Backdrop types:**
- `MacBackdropNormal` - Standard window
- `MacBackdropTranslucent` - Translucent background
- `MacBackdropTransparent` - Fully transparent
**Native fullscreen:**
macOS fullscreen creates a new Space (virtual desktop).
</TabItem>
<TabItem label="Linux" icon="linux">
**Linux-specific features:**
```go
// Set window icon
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Linux: application.LinuxOptions{
Icon: iconBytes,
},
})
```
**Desktop environment notes:**
- GNOME: Full support
- KDE Plasma: Full support
- XFCE: Partial support
- Others: Varies
**Window managers:**
- Tiling WMs may ignore size/position
- Some WMs don't support always-on-top
</TabItem>
</Tabs>
## Common Patterns
### Splash Screen
```go
// Create splash screen
splash := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Loading...",
Width: 400,
Height: 300,
Frameless: true,
AlwaysOnTop: true,
})
// Show splash
splash.Show()
// Initialise application
time.Sleep(2 * time.Second)
// Hide splash, show main window
splash.Close()
mainWindow.Show()
```
### Settings Window
```go
var settingsWindow *application.WebviewWindow
func showSettings() {
if settingsWindow == nil {
settingsWindow = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
Height: 400,
})
}
settingsWindow.Show()
settingsWindow.SetFocus()
}
```
### Confirm Before Close
```go
window.OnClose(func() bool {
if hasUnsavedChanges() {
// Show dialog
result := showConfirmdialog("Unsaved changes. Close anyway?")
return result == "yes"
}
return true
})
```
## Best Practices
### ✅ Do
- **Name important windows** - Easier to find later
- **Set minimum size** - Prevent unusable layouts
- **Centre windows** - Better UX than random position
- **Handle close events** - Prevent data loss
- **Test on all platforms** - Behaviour varies
- **Use appropriate sizes** - Consider different screen sizes
### ❌ Don't
- **Don't create too many windows** - Confusing for users
- **Don't forget to close windows** - Memory leaks
- **Don't hardcode positions** - Different screen sizes
- **Don't ignore platform differences** - Test thoroughly
- **Don't block the UI thread** - Use goroutines for long operations
## Troubleshooting
### Window Not Showing
**Possible causes:**
1. Window created as hidden
2. Window off-screen
3. Window behind other windows
**Solution:**
```go
window.Show()
window.Center()
window.SetFocus()
```
### Window Wrong Size
**Cause:** DPI scaling on Windows/Linux
**Solution:**
```go
// Wails handles DPI automatically
// Just use logical pixels
window.SetSize(800, 600)
```
### Window Closes Immediately
**Cause:** Application exits when last window closes
**Solution:**
```go
app := application.New(application.Options{
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
})
```
## Next Steps
<CardGrid>
<Card title="Window Options" icon="seti:config">
Complete reference for all window options.
[Learn More →](/features/windows/options)
</Card>
<Card title="Multiple Windows" icon="laptop">
Patterns for multi-window applications.
[Learn More →](/features/windows/multiple)
</Card>
<Card title="Frameless Windows" icon="star">
Create custom window chrome.
[Learn More →](/features/windows/frameless)
</Card>
<Card title="Window Events" icon="rocket">
Handle window lifecycle events.
[Learn More →](/features/windows/events)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [window examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).

View file

@ -0,0 +1,693 @@
---
title: Window Events
description: Handle window lifecycle and state change events
sidebar:
order: 5
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
## Window Events
Wails provides **comprehensive event hooks** for window lifecycle and state changes: creation, focus/blur, resize/move, minimise/maximise, and close events. Register callbacks, handle events, and coordinate between windows with simple, consistent APIs.
## Lifecycle Events
### OnCreate
Called when a window is created:
```go
app.OnWindowCreation(func(window *application.WebviewWindow) {
fmt.Printf("Window created: %s (ID: %d)\n", window.Name(), window.ID())
// Configure all new windows
window.SetMinSize(400, 300)
// Register window-specific handlers
window.OnClose(func() bool {
return confirmClose()
})
})
```
**Use cases:**
- Configure all windows consistently
- Register event handlers
- Track window creation
- Initialise window-specific resources
### OnClose
Called when user attempts to close window:
```go
window.OnClose(func() bool {
// Return false to cancel close
// Return true to allow close
if hasUnsavedChanges() {
result := showConfirmdialog("Unsaved changes. Close anyway?")
return result == "yes"
}
return true
})
```
**Important:**
- Only triggered by user actions (clicking X button)
- NOT triggered by `window.Destroy()`
- Can cancel the close by returning `false`
**Use cases:**
- Confirm before closing
- Save state
- Prevent accidental closure
- Cleanup before close
**Example with dialog:**
```go
window.OnClose(func() bool {
if !hasUnsavedChanges() {
return true
}
// Show confirmation dialog
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Confirm Close",
Width: 400,
Height: 150,
Parent: window,
AlwaysOnTop: true,
})
// Wait for user response
result := waitFordialogResult(dialog)
return result == "yes"
})
```
### OnDestroy
Called when window is destroyed:
```go
window.OnDestroy(func() {
fmt.Printf("Window destroyed: %s\n", window.Name())
// Cleanup resources
closeDatabase()
// Remove from tracking
removeWindowFromRegistry(window.ID())
// Update application state
updateWindowCount()
})
```
**Important:**
- Always called when window is destroyed
- Cannot be cancelled
- Last chance to cleanup
**Use cases:**
- Release resources
- Close connections
- Update application state
- Remove from tracking
**Example with resource cleanup:**
```go
type ManagedWindow struct {
window *application.WebviewWindow
db *sql.DB
listeners []func()
}
func (mw *ManagedWindow) Setup() {
mw.window.OnDestroy(func() {
// Close database
if mw.db != nil {
mw.db.Close()
}
// Remove event listeners
for _, listener := range mw.listeners {
listener()
}
// Clear references
mw.db = nil
mw.listeners = nil
})
}
```
## Focus Events
### OnFocus
Called when window gains focus:
```go
window.OnFocus(func() {
fmt.Println("Window gained focus")
// Update UI
updateTitleBar(true)
// Refresh data
refreshContent()
// Notify other windows
app.EmitEvent("window-focused", window.ID())
})
```
**Use cases:**
- Update UI appearance
- Refresh data
- Resume operations
- Coordinate with other windows
### OnBlur
Called when window loses focus:
```go
window.OnBlur(func() {
fmt.Println("Window lost focus")
// Update UI
updateTitleBar(false)
// Pause operations
pauseAnimations()
// Save state
saveCurrentState()
})
```
**Use cases:**
- Update UI appearance
- Pause operations
- Save state
- Reduce resource usage
**Example: Focus-aware UI:**
```go
type FocusAwareWindow struct {
window *application.WebviewWindow
focused bool
}
func (fw *FocusAwareWindow) Setup() {
fw.window.OnFocus(func() {
fw.focused = true
fw.updateAppearance()
})
fw.window.OnBlur(func() {
fw.focused = false
fw.updateAppearance()
})
}
func (fw *FocusAwareWindow) updateAppearance() {
if fw.focused {
fw.window.EmitEvent("update-theme", "active")
} else {
fw.window.EmitEvent("update-theme", "inactive")
}
}
```
## State Change Events
### OnMinimise / OnUnMinimise
Called when window is minimised or restored:
```go
window.OnMinimise(func() {
fmt.Println("Window minimised")
// Pause expensive operations
pauseRendering()
// Save state
saveWindowState()
})
window.OnUnMinimise(func() {
fmt.Println("Window restored from minimised")
// Resume operations
resumeRendering()
// Refresh data
refreshContent()
})
```
**Use cases:**
- Pause/resume operations
- Save/restore state
- Reduce resource usage
- Update UI
### OnMaximise / OnUnMaximise
Called when window is maximised or restored:
```go
window.OnMaximise(func() {
fmt.Println("Window maximised")
// Adjust layout
window.EmitEvent("layout-mode", "maximised")
// Update button icon
updateMaximiseButton("restore")
})
window.OnUnMaximise(func() {
fmt.Println("Window restored from maximised")
// Adjust layout
window.EmitEvent("layout-mode", "normal")
// Update button icon
updateMaximiseButton("maximise")
})
```
**Use cases:**
- Adjust layout
- Update UI
- Save window state
- Coordinate with other windows
### OnFullscreen / OnUnFullscreen
Called when window enters or exits fullscreen:
```go
window.OnFullscreen(func() {
fmt.Println("Window entered fullscreen")
// Hide UI chrome
window.EmitEvent("chrome-visibility", false)
// Adjust layout
window.EmitEvent("layout-mode", "fullscreen")
})
window.OnUnFullscreen(func() {
fmt.Println("Window exited fullscreen")
// Show UI chrome
window.EmitEvent("chrome-visibility", true)
// Restore layout
window.EmitEvent("layout-mode", "normal")
})
```
**Use cases:**
- Show/hide UI elements
- Adjust layout
- Update controls
- Save preferences
## Position and Size Events
### OnMove
Called when window is moved:
```go
window.OnMove(func(x, y int) {
fmt.Printf("Window moved to: %d, %d\n", x, y)
// Save position
saveWindowPosition(x, y)
// Update related windows
updateRelatedWindowPositions(x, y)
})
```
**Use cases:**
- Save window position
- Update related windows
- Snap to edges
- Multi-monitor handling
### OnResize
Called when window is resized:
```go
window.OnResize(func(width, height int) {
fmt.Printf("Window resized to: %dx%d\n", width, height)
// Save size
saveWindowSize(width, height)
// Adjust layout
window.EmitEvent("window-size", map[string]int{
"width": width,
"height": height,
})
})
```
**Use cases:**
- Save window size
- Adjust layout
- Update UI
- Responsive design
**Example: Responsive layout:**
```go
window.OnResize(func(width, height int) {
var layout string
if width < 600 {
layout = "compact"
} else if width < 1200 {
layout = "normal"
} else {
layout = "wide"
}
window.EmitEvent("layout-changed", layout)
})
```
## Complete Example
Here's a production-ready window with full event handling:
```go
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/wailsapp/wails/v3/pkg/application"
)
type WindowState struct {
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
Maximised bool `json:"maximised"`
Fullscreen bool `json:"fullscreen"`
}
type ManagedWindow struct {
app *application.Application
window *application.WebviewWindow
state WindowState
dirty bool
}
func main() {
app := application.New(application.Options{
Name: "Event Demo",
})
mw := &ManagedWindow{app: app}
mw.CreateWindow()
mw.LoadState()
mw.SetupEventHandlers()
app.Run()
}
func (mw *ManagedWindow) CreateWindow() {
mw.window = mw.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Event Demo",
Width: 800,
Height: 600,
})
}
func (mw *ManagedWindow) SetupEventHandlers() {
// Focus events
mw.window.OnFocus(func() {
fmt.Println("Window focused")
mw.window.EmitEvent("focus-state", true)
})
mw.window.OnBlur(func() {
fmt.Println("Window blurred")
mw.window.EmitEvent("focus-state", false)
})
// State change events
mw.window.OnMinimise(func() {
fmt.Println("Window minimised")
mw.SaveState()
})
mw.window.OnUnMinimise(func() {
fmt.Println("Window restored")
})
mw.window.OnMaximise(func() {
fmt.Println("Window maximised")
mw.state.Maximised = true
mw.dirty = true
})
mw.window.OnUnMaximise(func() {
fmt.Println("Window restored from maximised")
mw.state.Maximised = false
mw.dirty = true
})
mw.window.OnFullscreen(func() {
fmt.Println("Window fullscreen")
mw.state.Fullscreen = true
mw.dirty = true
})
mw.window.OnUnFullscreen(func() {
fmt.Println("Window exited fullscreen")
mw.state.Fullscreen = false
mw.dirty = true
})
// Position and size events
mw.window.OnMove(func(x, y int) {
mw.state.X = x
mw.state.Y = y
mw.dirty = true
})
mw.window.OnResize(func(width, height int) {
mw.state.Width = width
mw.state.Height = height
mw.dirty = true
})
// Lifecycle events
mw.window.OnClose(func() bool {
if mw.dirty {
mw.SaveState()
}
return true
})
mw.window.OnDestroy(func() {
fmt.Println("Window destroyed")
if mw.dirty {
mw.SaveState()
}
})
}
func (mw *ManagedWindow) LoadState() {
data, err := os.ReadFile("window-state.json")
if err != nil {
return
}
if err := json.Unmarshal(data, &mw.state); err != nil {
return
}
// Restore window state
mw.window.SetPosition(mw.state.X, mw.state.Y)
mw.window.SetSize(mw.state.Width, mw.state.Height)
if mw.state.Maximised {
mw.window.Maximise()
}
if mw.state.Fullscreen {
mw.window.Fullscreen()
}
}
func (mw *ManagedWindow) SaveState() {
data, err := json.Marshal(mw.state)
if err != nil {
return
}
os.WriteFile("window-state.json", data, 0644)
mw.dirty = false
fmt.Println("Window state saved")
}
```
## Event Coordination
### Cross-Window Events
Coordinate between multiple windows:
```go
// In main window
mainWindow.OnFocus(func() {
// Notify all windows
app.EmitEvent("main-window-focused", nil)
})
// In other windows
app.OnEvent("main-window-focused", func(event *application.WailsEvent) {
// Update UI
updateRelativeToMain()
})
```
### Event Chains
Chain events together:
```go
window.OnMaximise(func() {
// Save state
saveWindowState()
// Update layout
window.EmitEvent("layout-changed", "maximised")
// Notify other windows
app.EmitEvent("window-maximised", window.ID())
})
```
### Debounced Events
Debounce frequent events:
```go
var resizeTimer *time.Timer
window.OnResize(func(width, height int) {
if resizeTimer != nil {
resizeTimer.Stop()
}
resizeTimer = time.AfterFunc(500*time.Millisecond, func() {
// Save after resize stops
saveWindowSize(width, height)
})
})
```
## Best Practices
### ✅ Do
- **Save state on close** - Restore window position/size
- **Cleanup on destroy** - Release resources
- **Debounce frequent events** - Resize, move
- **Handle focus changes** - Update UI appropriately
- **Coordinate windows** - Use events for communication
- **Test all events** - Ensure handlers work correctly
### ❌ Don't
- **Don't block event handlers** - Keep them fast
- **Don't forget cleanup** - Memory leaks
- **Don't ignore errors** - Log or handle them
- **Don't save on every event** - Debounce first
- **Don't create circular events** - Infinite loops
- **Don't forget platform differences** - Test thoroughly
## Troubleshooting
### OnClose Not Firing
**Cause:** Using `window.Destroy()` instead of `window.Close()`
**Solution:**
```go
// ✅ Triggers OnClose
window.Close()
// ❌ Doesn't trigger OnClose
window.Destroy()
```
### Events Not Firing
**Cause:** Handler registered after event occurred
**Solution:**
```go
// Register handlers immediately after creation
window := app.NewWebviewWindow()
window.OnClose(func() bool { return true })
```
### Memory Leaks
**Cause:** Not cleaning up in OnDestroy
**Solution:**
```go
window.OnDestroy(func() {
// Always cleanup
closeResources()
removeReferences()
})
```
## Next Steps
**Window Basics** - Learn the fundamentals of window management
[Learn More →](/features/windows/basics)
**Multiple Windows** - Patterns for multi-window applications
[Learn More →](/features/windows/multiple)
**Events System** - Deep dive into the event system
[Learn More →](/features/events/system)
**Application Lifecycle** - Understand the application lifecycle
[Learn More →](/concepts/lifecycle)
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).

View file

@ -0,0 +1,870 @@
---
title: Frameless Windows
description: Create custom window chrome with frameless windows
sidebar:
order: 4
---
import { Tabs, TabItem, Card, CardGrid } from "@astrojs/starlight/components";
## Frameless Windows
Wails provides **frameless window support** with CSS-based drag regions and platform-native behaviour. Remove the platform-native title bar for complete control over window chrome, custom designs, and unique user experiences whilst maintaining essential functionality like dragging, resizing, and system controls.
## Quick Start
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Frameless App",
Width: 800,
Height: 600,
Frameless: true,
})
```
**CSS for draggable title bar:**
```css
.titlebar {
--wails-draggable: drag;
height: 40px;
background: #333;
}
.titlebar button {
--wails-draggable: no-drag;
}
```
**HTML:**
```html
<div class="titlebar">
<span>My Application</span>
<button onclick="window.close()">×</button>
</div>
```
**That's it!** You have a custom title bar.
## Creating Frameless Windows
### Basic Frameless Window
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Frameless: true,
Width: 800,
Height: 600,
})
```
**What you get:**
- No title bar
- No window borders
- No system buttons
- Transparent background (optional)
**What you need to implement:**
- Draggable area
- Close/minimise/maximise buttons
- Resize handles (if resizable)
### With Transparent Background
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Frameless: true,
BackgroundType: application.BackgroundTypeTransparent,
})
```
**Use cases:**
- Rounded corners
- Custom shapes
- Overlay windows
- Splash screens
## Drag Regions
### CSS-Based Dragging
Use the `--wails-draggable` CSS property:
```css
/* Draggable area */
.titlebar {
--wails-draggable: drag;
}
/* Non-draggable elements within draggable area */
.titlebar button {
--wails-draggable: no-drag;
}
```
**Values:**
- `drag` - Area is draggable
- `no-drag` - Area is not draggable (even if parent is)
### Complete Title Bar Example
```html
<div class="titlebar">
<div class="title">My Application</div>
<div class="controls">
<button class="minimize"></button>
<button class="maximize">□</button>
<button class="close">×</button>
</div>
</div>
```
```css
.titlebar {
--wails-draggable: drag;
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
background: #2c2c2c;
color: white;
padding: 0 16px;
}
.title {
font-size: 14px;
user-select: none;
}
.controls {
display: flex;
gap: 8px;
}
.controls button {
--wails-draggable: no-drag;
width: 32px;
height: 32px;
border: none;
background: transparent;
color: white;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
}
.controls button:hover {
background: rgba(255, 255, 255, 0.1);
}
.controls .close:hover {
background: #e81123;
}
```
**JavaScript for buttons:**
```javascript
import { WindowMinimise, WindowMaximise, WindowClose } from '@wailsio/runtime'
document.querySelector('.minimize').addEventListener('click', WindowMinimise)
document.querySelector('.maximize').addEventListener('click', WindowMaximise)
document.querySelector('.close').addEventListener('click', WindowClose)
```
## System Buttons
### Implementing Close/Minimise/Maximise
**Go side:**
```go
type WindowControls struct {
window *application.WebviewWindow
}
func (wc *WindowControls) Minimise() {
wc.window.Minimise()
}
func (wc *WindowControls) Maximise() {
if wc.window.IsMaximised() {
wc.window.UnMaximise()
} else {
wc.window.Maximise()
}
}
func (wc *WindowControls) Close() {
wc.window.Close()
}
```
**JavaScript side:**
```javascript
import { Minimise, Maximise, Close } from './bindings/WindowControls'
document.querySelector('.minimize').addEventListener('click', Minimise)
document.querySelector('.maximize').addEventListener('click', Maximise)
document.querySelector('.close').addEventListener('click', Close)
```
**Or use runtime methods:**
```javascript
import {
WindowMinimise,
WindowMaximise,
WindowClose
} from '@wailsio/runtime'
document.querySelector('.minimize').addEventListener('click', WindowMinimise)
document.querySelector('.maximize').addEventListener('click', WindowMaximise)
document.querySelector('.close').addEventListener('click', WindowClose)
```
### Toggle Maximise State
Track maximise state for button icon:
```javascript
import { WindowIsMaximised, WindowMaximise, WindowUnMaximise } from '@wailsio/runtime'
async function toggleMaximise() {
const isMaximised = await WindowIsMaximised()
if (isMaximised) {
await WindowUnMaximise()
} else {
await WindowMaximise()
}
updateMaximiseButton()
}
async function updateMaximiseButton() {
const isMaximised = await WindowIsMaximised()
const button = document.querySelector('.maximize')
button.textContent = isMaximised ? '❐' : '□'
}
```
## Resize Handles
### CSS-Based Resize
Wails provides automatic resize handles for frameless windows:
```css
/* Enable resize on all edges */
body {
--wails-resize: all;
}
/* Or specific edges */
.resize-top {
--wails-resize: top;
}
.resize-bottom {
--wails-resize: bottom;
}
.resize-left {
--wails-resize: left;
}
.resize-right {
--wails-resize: right;
}
/* Corners */
.resize-top-left {
--wails-resize: top-left;
}
.resize-top-right {
--wails-resize: top-right;
}
.resize-bottom-left {
--wails-resize: bottom-left;
}
.resize-bottom-right {
--wails-resize: bottom-right;
}
```
**Values:**
- `all` - Resize from all edges
- `top`, `bottom`, `left`, `right` - Specific edges
- `top-left`, `top-right`, `bottom-left`, `bottom-right` - Corners
- `none` - No resize
### Resize Handle Example
```html
<div class="window">
<div class="titlebar">...</div>
<div class="content">...</div>
<div class="resize-handle resize-bottom-right"></div>
</div>
```
```css
.resize-handle {
position: absolute;
width: 16px;
height: 16px;
}
.resize-bottom-right {
--wails-resize: bottom-right;
bottom: 0;
right: 0;
cursor: nwse-resize;
}
```
## Platform-Specific Behaviour
<Tabs syncKey="platform">
<TabItem label="Windows" icon="seti:windows">
**Windows frameless windows:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Frameless: true,
Windows: application.WindowsOptions{
DisableFramelessWindowDecorations: false,
},
})
```
**Features:**
- Automatic drop shadow
- Snap layouts support (Windows 11)
- Aero Snap support
- DPI scaling
**Disable decorations:**
```go
Windows: application.WindowsOptions{
DisableFramelessWindowDecorations: true,
},
```
**Snap Assist:**
```go
// Trigger Windows 11 Snap Assist
window.SnapAssist()
```
**Custom title bar height:**
Windows automatically detects drag regions from CSS.
</TabItem>
<TabItem label="macOS" icon="apple">
**macOS frameless windows:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Frameless: true,
Mac: application.MacOptions{
TitleBarAppearsTransparent: true,
InvisibleTitleBarHeight: 40,
},
})
```
**Features:**
- Native fullscreen support
- Traffic light buttons (optional)
- Vibrancy effects
- Transparent title bar
**Hide traffic lights:**
```go
Mac: application.MacOptions{
TitleBarStyle: application.MacTitleBarStyleHidden,
},
```
**Invisible title bar:**
Allows dragging whilst hiding the title bar:
```go
Mac: application.MacOptions{
InvisibleTitleBarHeight: 40,
},
```
</TabItem>
<TabItem label="Linux" icon="linux">
**Linux frameless windows:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Frameless: true,
})
```
**Features:**
- Basic frameless support
- CSS drag regions
- Varies by desktop environment
**Desktop environment notes:**
- **GNOME:** Good support
- **KDE Plasma:** Good support
- **XFCE:** Basic support
- **Tiling WMs:** Limited support
**Compositor required:**
Transparency requires a compositor (most modern DEs have one).
</TabItem>
</Tabs>
## Common Patterns
### Pattern 1: Modern Title Bar
```html
<div class="modern-titlebar">
<div class="app-icon">
<img src="/icon.png" alt="App Icon">
</div>
<div class="title">My Application</div>
<div class="controls">
<button class="minimize"></button>
<button class="maximize">□</button>
<button class="close">×</button>
</div>
</div>
```
```css
.modern-titlebar {
--wails-draggable: drag;
display: flex;
align-items: center;
height: 40px;
background: linear-gradient(to bottom, #3a3a3a, #2c2c2c);
border-bottom: 1px solid #1a1a1a;
padding: 0 16px;
}
.app-icon {
--wails-draggable: no-drag;
width: 24px;
height: 24px;
margin-right: 12px;
}
.title {
flex: 1;
font-size: 13px;
color: #e0e0e0;
user-select: none;
}
.controls {
display: flex;
gap: 1px;
}
.controls button {
--wails-draggable: no-drag;
width: 46px;
height: 32px;
border: none;
background: transparent;
color: #e0e0e0;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.controls button:hover {
background: rgba(255, 255, 255, 0.1);
}
.controls .close:hover {
background: #e81123;
color: white;
}
```
### Pattern 2: Splash Screen
```go
splash := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Loading...",
Width: 400,
Height: 300,
Frameless: true,
AlwaysOnTop: true,
BackgroundType: application.BackgroundTypeTransparent,
})
```
```css
body {
background: transparent;
display: flex;
justify-content: center;
align-items: center;
}
.splash {
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
padding: 40px;
text-align: center;
}
```
### Pattern 3: Rounded Window
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Frameless: true,
BackgroundType: application.BackgroundTypeTransparent,
})
```
```css
body {
background: transparent;
margin: 8px;
}
.window {
background: white;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
overflow: hidden;
height: calc(100vh - 16px);
}
.titlebar {
--wails-draggable: drag;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
```
### Pattern 4: Overlay Window
```go
overlay := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Frameless: true,
AlwaysOnTop: true,
BackgroundType: application.BackgroundTypeTransparent,
})
```
```css
body {
background: transparent;
}
.overlay {
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
border-radius: 8px;
padding: 20px;
}
```
## Complete Example
Here's a production-ready frameless window:
**Go:**
```go
package main
import (
_ "embed"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed frontend/dist
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "Frameless App",
})
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Frameless Application",
Width: 1000,
Height: 700,
MinWidth: 800,
MinHeight: 600,
Frameless: true,
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
TitleBarAppearsTransparent: true,
InvisibleTitleBarHeight: 40,
},
Windows: application.WindowsOptions{
DisableFramelessWindowDecorations: false,
},
})
window.Center()
window.Show()
app.Run()
}
```
**HTML:**
```html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="window">
<div class="titlebar">
<div class="title">Frameless Application</div>
<div class="controls">
<button class="minimize" title="Minimise"></button>
<button class="maximize" title="Maximise">□</button>
<button class="close" title="Close">×</button>
</div>
</div>
<div class="content">
<h1>Hello from Frameless Window!</h1>
</div>
</div>
<script src="/main.js" type="module"></script>
</body>
</html>
```
**CSS:**
```css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
}
.window {
height: 100vh;
display: flex;
flex-direction: column;
}
.titlebar {
--wails-draggable: drag;
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
background: #ffffff;
border-bottom: 1px solid #e0e0e0;
padding: 0 16px;
}
.title {
font-size: 13px;
font-weight: 500;
color: #333;
user-select: none;
}
.controls {
display: flex;
gap: 8px;
}
.controls button {
--wails-draggable: no-drag;
width: 32px;
height: 32px;
border: none;
background: transparent;
color: #666;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.controls button:hover {
background: #f0f0f0;
color: #333;
}
.controls .close:hover {
background: #e81123;
color: white;
}
.content {
flex: 1;
padding: 40px;
overflow: auto;
}
```
**JavaScript:**
```javascript
import {
WindowMinimise,
WindowMaximise,
WindowUnMaximise,
WindowIsMaximised,
WindowClose
} from '@wailsio/runtime'
// Minimise button
document.querySelector('.minimize').addEventListener('click', () => {
WindowMinimise()
})
// Maximise/restore button
const maximiseBtn = document.querySelector('.maximize')
maximiseBtn.addEventListener('click', async () => {
const isMaximised = await WindowIsMaximised()
if (isMaximised) {
await WindowUnMaximise()
} else {
await WindowMaximise()
}
updateMaximiseButton()
})
// Close button
document.querySelector('.close').addEventListener('click', () => {
WindowClose()
})
// Update maximise button icon
async function updateMaximiseButton() {
const isMaximised = await WindowIsMaximised()
maximiseBtn.textContent = isMaximised ? '❐' : '□'
maximiseBtn.title = isMaximised ? 'Restore' : 'Maximise'
}
// Initial state
updateMaximiseButton()
```
## Best Practices
### ✅ Do
- **Provide draggable area** - Users need to move the window
- **Implement system buttons** - Close, minimise, maximise
- **Set minimum size** - Prevent unusable layouts
- **Test on all platforms** - Behaviour varies
- **Use CSS for drag regions** - Flexible and maintainable
- **Provide visual feedback** - Hover states on buttons
### ❌ Don't
- **Don't forget resize handles** - If window is resizable
- **Don't make entire window draggable** - Prevents interaction
- **Don't forget no-drag on buttons** - They won't work
- **Don't use tiny drag areas** - Hard to grab
- **Don't forget platform differences** - Test thoroughly
## Troubleshooting
### Window Won't Drag
**Cause:** Missing `--wails-draggable: drag`
**Solution:**
```css
.titlebar {
--wails-draggable: drag;
}
```
### Buttons Don't Work
**Cause:** Buttons are in draggable area
**Solution:**
```css
.titlebar button {
--wails-draggable: no-drag;
}
```
### Can't Resize Window
**Cause:** Missing resize handles
**Solution:**
```css
body {
--wails-resize: all;
}
```
## Next Steps
<CardGrid>
<Card title="Window Basics" icon="laptop">
Learn the fundamentals of window management.
[Learn More →](/features/windows/basics)
</Card>
<Card title="Window Options" icon="seti:config">
Complete reference for window options.
[Learn More →](/features/windows/options)
</Card>
<Card title="Window Events" icon="rocket">
Handle window lifecycle events.
[Learn More →](/features/windows/events)
</Card>
<Card title="Multiple Windows" icon="puzzle">
Patterns for multi-window applications.
[Learn More →](/features/windows/multiple)
</Card>
</CardGrid>
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [frameless example](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/frameless).

View file

@ -0,0 +1,814 @@
---
title: Multiple Windows
description: Patterns and best practices for multi-window applications
sidebar:
order: 3
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
## Multi-Window Applications
Wails v3 provides **native multi-window support** for creating settings windows, document windows, tool palettes, and inspector windows. Track windows, enable communication between them, and manage their lifecycle with simple, consistent APIs.
### Main + Settings Window
```go
package main
import "github.com/wailsapp/wails/v3/pkg/application"
type App struct {
app *application.Application
mainWindow *application.WebviewWindow
settingsWindow *application.WebviewWindow
}
func main() {
app := &App{}
app.app = application.New(application.Options{
Name: "Multi-Window App",
})
// Create main window
app.mainWindow = app.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Main Application",
Width: 1200,
Height: 800,
})
// Create settings window (hidden initially)
app.settingsWindow = app.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
Height: 400,
Hidden: true,
})
app.app.Run()
}
// Show settings from main window
func (a *App) ShowSettings() {
if a.settingsWindow != nil {
a.settingsWindow.Show()
a.settingsWindow.SetFocus()
}
}
```
**Key points:**
- Main window always visible
- Settings window created but hidden
- Show settings on demand
- Reuse same window (don't create multiple)
## Window Tracking
### Get All Windows
```go
windows := app.GetAllWindows()
fmt.Printf("Total windows: %d\n", len(windows))
for _, window := range windows {
fmt.Printf("- %s (ID: %d)\n", window.Name(), window.ID())
}
```
### Find Specific Window
```go
// By name
settings := app.GetWindowByName("settings")
if settings != nil {
settings.Show()
}
// By ID
window := app.GetWindowByID(123)
// Current (focused) window
current := app.CurrentWindow()
```
### Window Registry Pattern
Track windows in your application:
```go
type WindowManager struct {
windows map[string]*application.WebviewWindow
mu sync.RWMutex
}
func (wm *WindowManager) Register(name string, window *application.WebviewWindow) {
wm.mu.Lock()
defer wm.mu.Unlock()
wm.windows[name] = window
}
func (wm *WindowManager) Get(name string) *application.WebviewWindow {
wm.mu.RLock()
defer wm.mu.RUnlock()
return wm.windows[name]
}
func (wm *WindowManager) Remove(name string) {
wm.mu.Lock()
defer wm.mu.Unlock()
delete(wm.windows, name)
}
```
## Window Communication
### Using Events
Windows communicate via the event system:
```go
// In main window - emit event
app.EmitEvent("settings-changed", map[string]interface{}{
"theme": "dark",
"fontSize": 14,
})
// In settings window - listen for event
app.OnEvent("settings-changed", func(event *application.WailsEvent) {
data := event.Data.(map[string]interface{})
theme := data["theme"].(string)
fontSize := data["fontSize"].(int)
// Update UI
updateSettings(theme, fontSize)
})
```
### Shared State Pattern
Use a shared state manager:
```go
type AppState struct {
theme string
fontSize int
mu sync.RWMutex
}
var state = &AppState{
theme: "light",
fontSize: 12,
}
func (s *AppState) SetTheme(theme string) {
s.mu.Lock()
s.theme = theme
s.mu.Unlock()
// Notify all windows
app.EmitEvent("theme-changed", theme)
}
func (s *AppState) GetTheme() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.theme
}
```
### Window-to-Window Messages
Send messages between specific windows:
```go
// Get target window
targetWindow := app.GetWindowByName("preview")
// Emit event to specific window
targetWindow.EmitEvent("update-preview", previewData)
```
## Common Patterns
### Pattern 1: Singleton Windows
Ensure only one instance of a window exists:
```go
var settingsWindow *application.WebviewWindow
func ShowSettings(app *application.Application) {
// Create if doesn't exist
if settingsWindow == nil {
settingsWindow = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
Height: 400,
})
// Cleanup on close
settingsWindow.OnDestroy(func() {
settingsWindow = nil
})
}
// Show and focus
settingsWindow.Show()
settingsWindow.SetFocus()
}
```
### Pattern 2: Document Windows
Multiple instances of the same window type:
```go
type DocumentWindow struct {
window *application.WebviewWindow
filePath string
modified bool
}
var documents = make(map[string]*DocumentWindow)
func OpenDocument(app *application.Application, filePath string) {
// Check if already open
if doc, exists := documents[filePath]; exists {
doc.window.Show()
doc.window.SetFocus()
return
}
// Create new document window
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: filepath.Base(filePath),
Width: 800,
Height: 600,
})
doc := &DocumentWindow{
window: window,
filePath: filePath,
modified: false,
}
documents[filePath] = doc
// Cleanup on close
window.OnDestroy(func() {
delete(documents, filePath)
})
// Load document
loadDocument(window, filePath)
}
```
### Pattern 3: Tool Palettes
Floating windows that stay on top:
```go
func CreateToolPalette(app *application.Application) *application.WebviewWindow {
palette := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "tools",
Title: "Tools",
Width: 200,
Height: 400,
AlwaysOnTop: true,
Resizable: false,
})
return palette
}
```
### Pattern 4: Modal dialogs
Child windows that block parent:
```go
func ShowModaldialog(parent *application.WebviewWindow, title string) {
dialog := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: title,
Width: 400,
Height: 200,
Parent: parent,
AlwaysOnTop: true,
Resizable: false,
})
// Disable parent (platform-specific)
parent.SetEnabled(false)
// Re-enable parent on close
dialog.OnDestroy(func() {
parent.SetEnabled(true)
parent.SetFocus()
})
}
```
### Pattern 5: Inspector/Preview Windows
Linked windows that update together:
```go
type EditorApp struct {
editor *application.WebviewWindow
preview *application.WebviewWindow
}
func (e *EditorApp) UpdatePreview(content string) {
if e.preview != nil && e.preview.IsVisible() {
e.preview.EmitEvent("content-changed", content)
}
}
func (e *EditorApp) TogglePreview() {
if e.preview == nil {
e.preview = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "preview",
Title: "Preview",
Width: 600,
Height: 800,
})
e.preview.OnDestroy(func() {
e.preview = nil
})
}
if e.preview.IsVisible() {
e.preview.Hide()
} else {
e.preview.Show()
}
}
```
## Parent-Child Relationships
### Creating Child Windows
```go
childWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Child Window",
Parent: parentWindow,
})
```
**Behaviour:**
- Child closes when parent closes
- Child stays above parent (on some platforms)
- Child minimises with parent (on some platforms)
**Platform support:**
| Feature | macOS | Windows | Linux |
|---------|-------|---------|-------|
| Auto-close | ✅ | ✅ | ⚠️ Varies |
| Stay above | ✅ | ⚠️ Partial | ⚠️ Varies |
| Minimise together | ✅ | ❌ | ⚠️ Varies |
### Modal Behaviour
Create modal-like behaviour:
```go
func ShowModal(parent *application.WebviewWindow) {
modal := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Modal dialog",
Width: 400,
Height: 200,
Parent: parent,
AlwaysOnTop: true,
})
// Disable parent interaction
parent.SetEnabled(false)
// Re-enable on close
modal.OnClose(func() bool {
parent.SetEnabled(true)
parent.SetFocus()
return true
})
}
```
**Note:** True modal behaviour (blocking) varies by platform.
## Window Lifecycle Management
### Creation Callbacks
Be notified when windows are created:
```go
app.OnWindowCreation(func(window *application.WebviewWindow) {
fmt.Printf("Window created: %s\n", window.Name())
// Configure all new windows
window.SetMinSize(400, 300)
})
```
### Destruction Callbacks
Cleanup when windows are destroyed:
```go
window.OnDestroy(func() {
fmt.Printf("Window %s destroyed\n", window.Name())
// Cleanup resources
cleanup(window.ID())
// Remove from tracking
removeFromRegistry(window.Name())
})
```
### Application Quit Behaviour
Control when application quits:
```go
app := application.New(application.Options{
Mac: application.MacOptions{
// Don't quit when last window closes
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
})
```
**Use cases:**
- System tray applications
- Background services
- Menu bar applications (macOS)
## Memory Management
### Preventing Leaks
Always clean up window references:
```go
var windows = make(map[string]*application.WebviewWindow)
func CreateWindow(name string) {
window := app.NewWebviewWindow()
windows[name] = window
// IMPORTANT: Clean up on destroy
window.OnDestroy(func() {
delete(windows, name)
})
}
```
### Closing vs Destroying
```go
// Close - triggers OnClose, can be cancelled
window.Close()
// Destroy - immediate, cannot be cancelled
window.Destroy()
```
**Best practice:** Use `Close()` for user-initiated closes, `Destroy()` for cleanup.
### Resource Cleanup
```go
type ManagedWindow struct {
window *application.WebviewWindow
resources []io.Closer
}
func (mw *ManagedWindow) Destroy() {
// Close all resources
for _, resource := range mw.resources {
resource.Close()
}
// Destroy window
mw.window.Destroy()
}
```
## Advanced Patterns
### Window Pool
Reuse windows instead of creating new ones:
```go
type WindowPool struct {
available []*application.WebviewWindow
inUse map[uint]*application.WebviewWindow
mu sync.Mutex
}
func (wp *WindowPool) Acquire() *application.WebviewWindow {
wp.mu.Lock()
defer wp.mu.Unlock()
// Reuse available window
if len(wp.available) > 0 {
window := wp.available[0]
wp.available = wp.available[1:]
wp.inUse[window.ID()] = window
return window
}
// Create new window
window := app.NewWebviewWindow()
wp.inUse[window.ID()] = window
return window
}
func (wp *WindowPool) Release(window *application.WebviewWindow) {
wp.mu.Lock()
defer wp.mu.Unlock()
delete(wp.inUse, window.ID())
window.Hide()
wp.available = append(wp.available, window)
}
```
### Window Groups
Manage related windows together:
```go
type WindowGroup struct {
name string
windows []*application.WebviewWindow
}
func (wg *WindowGroup) Add(window *application.WebviewWindow) {
wg.windows = append(wg.windows, window)
}
func (wg *WindowGroup) ShowAll() {
for _, window := range wg.windows {
window.Show()
}
}
func (wg *WindowGroup) HideAll() {
for _, window := range wg.windows {
window.Hide()
}
}
func (wg *WindowGroup) CloseAll() {
for _, window := range wg.windows {
window.Close()
}
}
```
### Workspace Management
Save and restore window layouts:
```go
type WindowLayout struct {
Windows []WindowState `json:"windows"`
}
type WindowState struct {
Name string `json:"name"`
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
}
func SaveLayout() *WindowLayout {
layout := &WindowLayout{}
for _, window := range app.GetAllWindows() {
x, y := window.Position()
width, height := window.Size()
layout.Windows = append(layout.Windows, WindowState{
Name: window.Name(),
X: x,
Y: y,
Width: width,
Height: height,
})
}
return layout
}
func RestoreLayout(layout *WindowLayout) {
for _, state := range layout.Windows {
window := app.GetWindowByName(state.Name)
if window != nil {
window.SetPosition(state.X, state.Y)
window.SetSize(state.Width, state.Height)
}
}
}
```
## Complete Example
Here's a production-ready multi-window application:
```go
package main
import (
"encoding/json"
"os"
"sync"
"github.com/wailsapp/wails/v3/pkg/application"
)
type MultiWindowApp struct {
app *application.Application
windows map[string]*application.WebviewWindow
mu sync.RWMutex
}
func main() {
mwa := &MultiWindowApp{
windows: make(map[string]*application.WebviewWindow),
}
mwa.app = application.New(application.Options{
Name: "Multi-Window Application",
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
})
// Create main window
mwa.CreateMainWindow()
// Load saved layout
mwa.LoadLayout()
mwa.app.Run()
}
func (mwa *MultiWindowApp) CreateMainWindow() {
window := mwa.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Main Application",
Width: 1200,
Height: 800,
})
mwa.RegisterWindow("main", window)
}
func (mwa *MultiWindowApp) ShowSettings() {
if window := mwa.GetWindow("settings"); window != nil {
window.Show()
window.SetFocus()
return
}
window := mwa.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
Height: 400,
})
mwa.RegisterWindow("settings", window)
}
func (mwa *MultiWindowApp) OpenDocument(path string) {
name := "doc-" + path
if window := mwa.GetWindow(name); window != nil {
window.Show()
window.SetFocus()
return
}
window := mwa.app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: name,
Title: path,
Width: 800,
Height: 600,
})
mwa.RegisterWindow(name, window)
}
func (mwa *MultiWindowApp) RegisterWindow(name string, window *application.WebviewWindow) {
mwa.mu.Lock()
mwa.windows[name] = window
mwa.mu.Unlock()
window.OnDestroy(func() {
mwa.UnregisterWindow(name)
})
}
func (mwa *MultiWindowApp) UnregisterWindow(name string) {
mwa.mu.Lock()
delete(mwa.windows, name)
mwa.mu.Unlock()
}
func (mwa *MultiWindowApp) GetWindow(name string) *application.WebviewWindow {
mwa.mu.RLock()
defer mwa.mu.RUnlock()
return mwa.windows[name]
}
func (mwa *MultiWindowApp) SaveLayout() {
layout := make(map[string]WindowState)
mwa.mu.RLock()
for name, window := range mwa.windows {
x, y := window.Position()
width, height := window.Size()
layout[name] = WindowState{
X: x,
Y: y,
Width: width,
Height: height,
}
}
mwa.mu.RUnlock()
data, _ := json.Marshal(layout)
os.WriteFile("layout.json", data, 0644)
}
func (mwa *MultiWindowApp) LoadLayout() {
data, err := os.ReadFile("layout.json")
if err != nil {
return
}
var layout map[string]WindowState
if err := json.Unmarshal(data, &layout); err != nil {
return
}
for name, state := range layout {
if window := mwa.GetWindow(name); window != nil {
window.SetPosition(state.X, state.Y)
window.SetSize(state.Width, state.Height)
}
}
}
type WindowState struct {
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
}
```
## Best Practices
### ✅ Do
- **Track windows** - Keep references for easy access
- **Clean up on destroy** - Prevent memory leaks
- **Use events for communication** - Decoupled architecture
- **Reuse windows** - Don't create duplicates
- **Save/restore layouts** - Better UX
- **Handle window close** - Confirm before closing with unsaved data
### ❌ Don't
- **Don't create unlimited windows** - Memory and performance issues
- **Don't forget to clean up** - Memory leaks
- **Don't use global variables carelessly** - Thread-safety issues
- **Don't block window creation** - Create asynchronously if needed
- **Don't ignore platform differences** - Test on all platforms
## Next Steps
- [Window Basics](/features/windows/basics) - Learn the fundamentals of window management
- [Window Events](/features/windows/events) - Handle window lifecycle events
- [Events System](/features/events/system) - Deep dive into the event system
- [Frameless Windows](/features/windows/frameless) - Create custom window chrome
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [multi-window example](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/multi-window).

View file

@ -0,0 +1,852 @@
---
title: Window Options
description: Complete reference for WebviewWindowOptions
sidebar:
order: 2
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
## Window Configuration Options
Wails provides comprehensive window configuration with dozens of options for size, position, appearance, and behaviour. This reference covers all available options across Windows, macOS, and Linux as the **complete reference** for `WebviewWindowOptions`. Every option, every platform, with examples and constraints.
## WebviewWindowOptions Structure
```go
type WebviewWindowOptions struct {
// Identity
Name string
Title string
// Size and Position
Width int
Height int
X int
Y int
MinWidth int
MinHeight int
MaxWidth int
MaxHeight int
// Initial State
Hidden bool
Frameless bool
Resizable bool
AlwaysOnTop bool
Fullscreen bool
Minimised bool
Maximised bool
WindowState WindowState
// Appearance
BackgroundColour RGBA
BackgroundType BackgroundType
// Content
URL string
HTML string
// Assets
Assets application.AssetOptions
// Security
ContentProtectionEnabled bool
// Lifecycle
OnClose func() bool
OnDestroy func()
// Platform-Specific
Mac MacOptions
Windows WindowsOptions
Linux LinuxOptions
}
```
## Core Options
### Name
**Type:** `string`
**Default:** Auto-generated UUID
**Platform:** All
```go
Name: "main-window"
```
**Purpose:** Unique identifier for finding windows later.
**Best practices:**
- Use descriptive names: `"main"`, `"settings"`, `"about"`
- Use kebab-case: `"file-browser"`, `"color-picker"`
- Keep it short and memorable
**Example:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "settings-window",
})
// Later...
settings := app.GetWindowByName("settings-window")
```
### Title
**Type:** `string`
**Default:** Application name
**Platform:** All
```go
Title: "My Application"
```
**Purpose:** Text shown in title bar and taskbar.
**Dynamic updates:**
```go
window.SetTitle("My Application - Document.txt")
```
### Width / Height
**Type:** `int` (pixels)
**Default:** 800 x 600
**Platform:** All
**Constraints:** Must be positive
```go
Width: 1200,
Height: 800,
```
**Purpose:** Initial window size in logical pixels.
**Notes:**
- Wails handles DPI scaling automatically
- Use logical pixels, not physical pixels
- Consider minimum screen resolution (1024x768)
**Example sizes:**
| Use Case | Width | Height |
|----------|-------|--------|
| Small utility | 400 | 300 |
| Standard app | 1024 | 768 |
| Large app | 1440 | 900 |
| Full HD | 1920 | 1080 |
### X / Y
**Type:** `int` (pixels)
**Default:** Centred on screen
**Platform:** All
```go
X: 100, // 100px from left edge
Y: 100, // 100px from top edge
```
**Purpose:** Initial window position.
**Coordinate system:**
- (0, 0) is top-left of primary screen
- Positive X goes right
- Positive Y goes down
**Best practice:** Use `Center()` instead:
```go
window := app.NewWebviewWindow()
window.Center()
```
### MinWidth / MinHeight
**Type:** `int` (pixels)
**Default:** 0 (no minimum)
**Platform:** All
```go
MinWidth: 400,
MinHeight: 300,
```
**Purpose:** Prevent window from being too small.
**Use cases:**
- Prevent broken layouts
- Ensure usability
- Maintain aspect ratio
**Example:**
```go
// Prevent window smaller than 400x300
MinWidth: 400,
MinHeight: 300,
```
### MaxWidth / MaxHeight
**Type:** `int` (pixels)
**Default:** 0 (no maximum)
**Platform:** All
```go
MaxWidth: 1920,
MaxHeight: 1080,
```
**Purpose:** Prevent window from being too large.
**Use cases:**
- Fixed-size applications
- Prevent excessive resource usage
- Maintain design constraints
## State Options
### Hidden
**Type:** `bool`
**Default:** `false`
**Platform:** All
```go
Hidden: true,
```
**Purpose:** Create window without showing it.
**Use cases:**
- Background windows
- Windows shown on demand
- Splash screens (create, load, then show)
- Prevent white flash while loading content
**Platform improvements:**
- **Windows:** Fixed white window flash - window stays invisible until `Show()` is called
- **macOS:** Full support
- **Linux:** Full support
**Recommended pattern for smooth loading:**
```go
// Create hidden window
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "main-window",
Hidden: true,
BackgroundColour: application.RGBA{R: 30, G: 30, B: 30, A: 255}, // Match your theme
})
// Load content while hidden
// ... content loads ...
// Show when ready (no flash!)
window.Show()
```
**Example:**
```go
settings := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "settings",
Hidden: true,
})
// Show when needed
settings.Show()
```
### Frameless
**Type:** `bool`
**Default:** `false`
**Platform:** All
```go
Frameless: true,
```
**Purpose:** Remove title bar and window borders.
**Use cases:**
- Custom window chrome
- Splash screens
- Kiosk applications
- Custom-designed windows
**Important:** You'll need to implement:
- Window dragging
- Close/minimise/maximise buttons
- Resize handles (if resizable)
**See [Frameless Windows](/features/windows/frameless) for details.**
### Resizable
**Type:** `bool`
**Default:** `true`
**Platform:** All
```go
Resizable: false,
```
**Purpose:** Allow/prevent window resizing.
**Use cases:**
- Fixed-size applications
- Splash screens
- dialogs
**Note:** Users can still maximise/fullscreen unless you prevent those too.
### AlwaysOnTop
**Type:** `bool`
**Default:** `false`
**Platform:** All
```go
AlwaysOnTop: true,
```
**Purpose:** Keep window above all other windows.
**Use cases:**
- Floating toolbars
- Notifications
- Picture-in-picture
- Timers
**Platform notes:**
- **macOS:** Full support
- **Windows:** Full support
- **Linux:** Depends on window manager
### Fullscreen
**Type:** `bool`
**Default:** `false`
**Platform:** All
```go
Fullscreen: true,
```
**Purpose:** Start in fullscreen mode.
**Platform behaviour:**
- **macOS:** Creates new Space (virtual desktop)
- **Windows:** Covers taskbar
- **Linux:** Varies by desktop environment
**Toggle at runtime:**
```go
window.Fullscreen()
window.UnFullscreen()
```
### Minimised / Maximised
**Type:** `bool`
**Default:** `false`
**Platform:** All
```go
Minimised: true, // Start minimised
Maximised: true, // Start maximised
```
**Purpose:** Set initial window state.
**Note:** Don't set both to `true` - behaviour is undefined.
### WindowState
**Type:** `WindowState` enum
**Default:** `WindowStateNormal`
**Platform:** All
```go
WindowState: application.WindowStateMaximised,
```
**Values:**
- `WindowStateNormal` - Normal window
- `WindowStateMinimised` - Minimised
- `WindowStateMaximised` - Maximised
- `WindowStateFullscreen` - Fullscreen
- `WindowStateHidden` - Hidden
**Preferred over individual flags:**
```go
// ✅ Preferred
WindowState: application.WindowStateMaximised,
// ❌ Avoid
Maximised: true,
```
## Appearance Options
### BackgroundColour
**Type:** `RGBA` struct
**Default:** White (255, 255, 255, 255)
**Platform:** All
```go
BackgroundColour: application.RGBA{R: 0, G: 0, H: 0, A: 255},
```
**Purpose:** Window background colour before content loads.
**Use cases:**
- Match your app's theme
- Prevent white flash on dark themes
- Smooth loading experience
**Example:**
```go
// Dark theme
BackgroundColour: application.RGBA{R: 30, G: 30, B: 30, A: 255},
// Light theme
BackgroundColour: application.RGBA{R: 255, G: 255, B: 255, A: 255},
```
**Helper method:**
```go
window.SetBackgroundColour(30, 30, 30, 255)
```
### BackgroundType
**Type:** `BackgroundType` enum
**Default:** `BackgroundTypeSolid`
**Platform:** macOS, Windows (partial)
```go
BackgroundType: application.BackgroundTypeTranslucent,
```
**Values:**
- `BackgroundTypeSolid` - Solid colour
- `BackgroundTypeTransparent` - Fully transparent
- `BackgroundTypeTranslucent` - Semi-transparent blur
**Platform support:**
- **macOS:** All types supported
- **Windows:** Transparent and Translucent (Windows 11+)
- **Linux:** Solid only
**Example (macOS):**
```go
BackgroundType: application.BackgroundTypeTranslucent,
Mac: application.MacOptions{
Backdrop: application.MacBackdropTranslucent,
},
```
## Content Options
### URL
**Type:** `string`
**Default:** Empty (loads from Assets)
**Platform:** All
```go
URL: "https://example.com",
```
**Purpose:** Load external URL instead of embedded assets.
**Use cases:**
- Development (load from dev server)
- Web-based applications
- Hybrid applications
**Example:**
```go
// Development
URL: "http://localhost:5173",
// Production
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
```
### HTML
**Type:** `string`
**Default:** Empty
**Platform:** All
```go
HTML: "<h1>Hello World</h1>",
```
**Purpose:** Load HTML string directly.
**Use cases:**
- Simple windows
- Generated content
- Testing
**Example:**
```go
HTML: `
<!DOCTYPE html>
<html>
<head><title>Simple Window</title></head>
<body><h1>Hello from Wails!</h1></body>
</html>
`,
```
### Assets
**Type:** `AssetOptions`
**Default:** Inherited from application
**Platform:** All
```go
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
```
**Purpose:** Serve embedded frontend assets.
**See [Build System](/concepts/build-system) for details.**
## Security Options
### ContentProtectionEnabled
**Type:** `bool`
**Default:** `false`
**Platform:** Windows (10+), macOS
```go
ContentProtectionEnabled: true,
```
**Purpose:** Prevent screen capture of window contents.
**Platform support:**
- **Windows:** Windows 10 build 19041+ (full), older versions (partial)
- **macOS:** Full support
- **Linux:** Not supported
**Use cases:**
- Banking applications
- Password managers
- Medical records
- Confidential documents
**Important notes:**
1. Doesn't prevent physical photography
2. Some tools may bypass protection
3. Part of comprehensive security, not sole protection
4. DevTools windows not protected automatically
**Example:**
```go
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Secure Window",
ContentProtectionEnabled: true,
})
// Toggle at runtime
window.SetContentProtection(true)
```
## Lifecycle Options
### OnClose
**Type:** `func() bool`
**Default:** `nil` (always allow close)
**Platform:** All
```go
OnClose: func() bool {
// Return false to cancel close
// Return true to allow close
return true
},
```
**Purpose:** Handle window close events, optionally prevent closing.
**Use cases:**
- Confirm before closing with unsaved changes
- Save state before closing
- Prevent accidental closure
**Example:**
```go
OnClose: func() bool {
if hasUnsavedChanges() {
result := showConfirmdialog("Unsaved changes. Close anyway?")
return result == "yes"
}
return true
},
```
**Important:** Only triggered by user actions (clicking X), not `window.Destroy()`.
### OnDestroy
**Type:** `func()`
**Default:** `nil`
**Platform:** All
```go
OnDestroy: func() {
// Cleanup code
fmt.Println("Window destroyed")
},
```
**Purpose:** Cleanup when window is destroyed.
**Use cases:**
- Release resources
- Close connections
- Update application state
**Example:**
```go
OnDestroy: func() {
// Close database connection
if db != nil {
db.Close()
}
// Remove from window list
removeWindow(window.ID())
},
```
## Platform-Specific Options
### Mac Options
```go
Mac: application.MacOptions{
TitleBarAppearsTransparent: true,
Backdrop: application.MacBackdropTranslucent,
InvisibleTitleBarHeight: 50,
TitleBarStyle: application.MacTitleBarStyleHidden,
},
```
**TitleBarAppearsTransparent** (`bool`)
- Makes title bar transparent
- Content extends into title bar area
**Backdrop** (`MacBackdrop`)
- `MacBackdropNormal` - Standard
- `MacBackdropTranslucent` - Blurred translucent
- `MacBackdropTransparent` - Fully transparent
**InvisibleTitleBarHeight** (`int`)
- Height of invisible title bar (for dragging)
- Only when `TitleBarStyle` is `MacTitleBarStyleHidden`
**TitleBarStyle** (`MacTitleBarStyle`)
- `MacTitleBarStyleDefault` - Standard title bar
- `MacTitleBarStyleHidden` - Hidden title bar
- `MacTitleBarStyleHiddenInset` - Hidden with inset
**Example:**
```go
Mac: application.MacOptions{
TitleBarAppearsTransparent: true,
Backdrop: application.MacBackdropTranslucent,
InvisibleTitleBarHeight: 50,
},
```
### Windows Options
```go
Windows: application.WindowsOptions{
DisableWindowIcon: false,
WindowBackdropType: application.WindowsBackdropTypeAuto,
CustomTheme: nil,
DisableFramelessWindowDecorations: false,
},
```
**DisableWindowIcon** (`bool`)
- Remove icon from title bar
- Cleaner appearance
**WindowBackdropType** (`WindowsBackdropType`)
- `WindowsBackdropTypeAuto` - System default
- `WindowsBackdropTypeNone` - No backdrop
- `WindowsBackdropTypeMica` - Mica material (Windows 11)
- `WindowsBackdropTypeAcrylic` - Acrylic material (Windows 11)
- `WindowsBackdropTypeTabbed` - Tabbed material (Windows 11)
**CustomTheme** (`*WindowsTheme`)
- Custom colour theme
- Override system colours
**DisableFramelessWindowDecorations** (`bool`)
- Disable default frameless decorations
- For custom window chrome
**Example:**
```go
Windows: application.WindowsOptions{
WindowBackdropType: application.WindowsBackdropTypeMica,
DisableWindowIcon: true,
},
```
### Linux Options
```go
Linux: application.LinuxOptions{
Icon: []byte{/* PNG data */},
WindowIsTranslucent: false,
},
```
**Icon** (`[]byte`)
- Window icon (PNG format)
- Shown in title bar and taskbar
**WindowIsTranslucent** (`bool`)
- Enable window translucency
- Requires compositor support
**Example:**
```go
//go:embed icon.png
var icon []byte
Linux: application.LinuxOptions{
Icon: icon,
},
```
## Complete Example
Here's a production-ready window configuration:
```go
package main
import (
_ "embed"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed frontend/dist
var assets embed.FS
//go:embed icon.png
var icon []byte
func main() {
app := application.New(application.Options{
Name: "My Application",
})
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
// Identity
Name: "main-window",
Title: "My Application",
// Size and Position
Width: 1200,
Height: 800,
MinWidth: 800,
MinHeight: 600,
// Initial State
WindowState: application.WindowStateNormal,
Resizable: true,
// Appearance
BackgroundColour: application.RGBA{R: 255, G: 255, B: 255, A: 255},
// Content
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
// Lifecycle
OnClose: func() bool {
if hasUnsavedChanges() {
result := showConfirmdialog("Unsaved changes. Close anyway?")
return result == "yes"
}
return true
},
OnDestroy: func() {
cleanup()
},
// Platform-Specific
Mac: application.MacOptions{
TitleBarAppearsTransparent: true,
Backdrop: application.MacBackdropTranslucent,
},
Windows: application.WindowsOptions{
WindowBackdropType: application.WindowsBackdropTypeMica,
DisableWindowIcon: false,
},
Linux: application.LinuxOptions{
Icon: icon,
},
})
window.Center()
window.Show()
app.Run()
}
```
## Next Steps
- [Window Basics](/features/windows/basics) - Creating and controlling windows
- [Multiple Windows](/features/windows/multiple) - Multi-window patterns
- [Frameless Windows](/features/windows/frameless) - Custom window chrome
- [Window Events](/features/windows/events) - Lifecycle events
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).

View file

@ -14,10 +14,7 @@ import wails_build from '../../../assets/wails_build.mp4';
import wails_dev from '../../../assets/wails_dev.mp4';
Creating your first application with Wails v3 is an exciting journey into
the world of modern desktop app development. This guide will walk you through
the process of creating a basic application, showcasing the power and simplicity
of Wails.
This guide shows you how to create your first Wails v3 application, covering project setup, building, and development workflow.
<br/>
<br/>

View file

@ -0,0 +1,199 @@
---
title: Architecture Patterns
description: Design patterns for Wails applications
sidebar:
order: 7
---
## Overview
Proven patterns for organising your Wails application.
## Service Layer Pattern
### Structure
```
app/
├── main.go
├── services/
│ ├── user_service.go
│ ├── data_service.go
│ └── file_service.go
└── models/
└── user.go
```
### Implementation
```go
// Service interface
type UserService interface {
Create(email, password string) (*User, error)
GetByID(id int) (*User, error)
Update(user *User) error
Delete(id int) error
}
// Implementation
type userService struct {
app *application.Application
db *sql.DB
}
func NewUserService(app *application.Application, db *sql.DB) UserService {
return &userService{app: app, db: db}
}
```
## Repository Pattern
### Structure
```go
// Repository interface
type UserRepository interface {
Create(user *User) error
FindByID(id int) (*User, error)
Update(user *User) error
Delete(id int) error
}
// Service uses repository
type UserService struct {
repo UserRepository
}
func (s *UserService) Create(email, password string) (*User, error) {
user := &User{Email: email}
return user, s.repo.Create(user)
}
```
## Event-Driven Architecture
### Event Bus
```go
type EventBus struct {
app *application.Application
listeners map[string][]func(interface{})
mu sync.RWMutex
}
func (eb *EventBus) Subscribe(event string, handler func(interface{})) {
eb.mu.Lock()
defer eb.mu.Unlock()
eb.listeners[event] = append(eb.listeners[event], handler)
}
func (eb *EventBus) Publish(event string, data interface{}) {
eb.mu.RLock()
handlers := eb.listeners[event]
eb.mu.RUnlock()
for _, handler := range handlers {
go handler(data)
}
}
```
### Usage
```go
// Subscribe
eventBus.Subscribe("user.created", func(data interface{}) {
user := data.(*User)
sendWelcomeEmail(user)
})
// Publish
eventBus.Publish("user.created", user)
```
## Dependency Injection
### Manual DI
```go
type App struct {
userService *UserService
fileService *FileService
db *sql.DB
}
func NewApp() *App {
db := openDatabase()
return &App{
db: db,
userService: NewUserService(db),
fileService: NewFileService(db),
}
}
```
### Using Wire
```go
// wire.go
//go:build wireinject
func InitializeApp() (*App, error) {
wire.Build(
openDatabase,
NewUserService,
NewFileService,
NewApp,
)
return nil, nil
}
```
## State Management
### Centralised State
```go
type AppState struct {
currentUser *User
settings *Settings
mu sync.RWMutex
}
func (s *AppState) SetUser(user *User) {
s.mu.Lock()
defer s.mu.Unlock()
s.currentUser = user
}
func (s *AppState) GetUser() *User {
s.mu.RLock()
defer s.mu.RUnlock()
return s.currentUser
}
```
## Best Practices
### ✅ Do
- Separate concerns
- Use interfaces
- Inject dependencies
- Handle errors properly
- Keep services focused
- Document architecture
### ❌ Don't
- Don't create god objects
- Don't tightly couple components
- Don't skip error handling
- Don't ignore concurrency
- Don't over-engineer
## Next Steps
- [Security](/guides/security) - Security best practices
- [Best Practices](/features/bindings/best-practices) - Bindings best practices

View file

@ -0,0 +1,171 @@
---
title: Auto-Updates
description: Implement automatic application updates
sidebar:
order: 4
---
## Overview
Keep your application up-to-date with automatic updates.
## Update Strategies
### 1. Check on Startup
```go
func (a *App) checkForUpdates() {
latest, err := getLatestVersion()
if err != nil {
return
}
if isNewer(latest, currentVersion) {
a.promptUpdate(latest)
}
}
```
### 2. Periodic Checks
```go
func (a *App) startUpdateChecker() {
ticker := time.NewTicker(24 * time.Hour)
go func() {
for range ticker.C {
a.checkForUpdates()
}
}()
}
```
### 3. Manual Check
```go
func (a *App) CheckForUpdates() {
result, _ := a.app.QuestionDialog().
SetMessage("Check for updates?").
SetButtons("Check", "Cancel").
Show()
if result == "Check" {
a.checkForUpdates()
}
}
```
## Implementation
### Version Checking
```go
type UpdateInfo struct {
Version string `json:"version"`
DownloadURL string `json:"download_url"`
ReleaseNotes string `json:"release_notes"`
}
func getLatestVersion() (*UpdateInfo, error) {
resp, err := http.Get("https://api.example.com/latest")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var info UpdateInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return nil, err
}
return &info, nil
}
```
### Download and Install
```go
func (a *App) downloadUpdate(url string) error {
// Download update
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Save to temp file
tmpFile, err := os.CreateTemp("", "update-*.exe")
if err != nil {
return err
}
defer tmpFile.Close()
// Copy data
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
return err
}
// Launch installer and quit
return a.installUpdate(tmpFile.Name())
}
```
## Third-Party Solutions
### Squirrel
```go
import "github.com/Squirrel/go-squirrel"
func setupUpdater() {
updater := squirrel.NewUpdater(squirrel.Options{
URL: "https://updates.example.com",
})
updater.CheckForUpdates()
}
```
### Self-Hosted
Host update manifests on your own server:
```json
{
"version": "1.0.1",
"platforms": {
"windows": {
"url": "https://example.com/myapp-1.0.1-windows.exe",
"sha256": "..."
},
"darwin": {
"url": "https://example.com/myapp-1.0.1-macos.dmg",
"sha256": "..."
}
},
"release_notes": "Bug fixes and improvements"
}
```
## Best Practices
### ✅ Do
- Verify update signatures
- Show release notes
- Allow users to skip versions
- Test update process thoroughly
- Provide rollback mechanism
- Use HTTPS for downloads
### ❌ Don't
- Don't force immediate updates
- Don't skip signature verification
- Don't interrupt user work
- Don't forget error handling
- Don't lose user data
## Next Steps
- [Creating Installers](/guides/installers) - Package your application
- [Testing](/guides/testing) - Test your application

View file

@ -1,7 +1,8 @@
---
title: Build System
title: Build Customization
description: Customize your build process using Task and Taskfile.yml
sidebar:
order: 40
order: 1
---
import { FileTree } from "@astrojs/starlight/components";

View file

@ -0,0 +1,415 @@
---
title: Windows Packaging
description: Package your Wails application for Windows distribution
sidebar:
order: 4
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
Learn how to package your Wails application for Windows distribution using various installer formats.
## Overview
Wails provides several packaging options for Windows:
- **NSIS** - Nullsoft Scriptable Install System (recommended)
- **MSI** - Windows Installer Package
- **Portable** - Standalone executable (no installation)
- **MSIX** - Modern Windows app package
## NSIS Installer
NSIS creates professional installers with customization options and automatic system integration.
### Quick Start
Build an NSIS installer:
```bash
wails3 build
wails3 task nsis
```
This generates `build/bin/<AppName>-<version>-<arch>-installer.exe`.
### Features
**Automatic Integration:**
- ✅ Start Menu shortcuts
- ✅ Desktop shortcuts (optional)
- ✅ File associations
- ✅ **Custom URL protocol registration** (NEW)
- ✅ Uninstaller creation
- ✅ Registry entries
**Custom Protocols:**
Wails automatically registers custom URL protocols defined in your application:
```go
app := application.New(application.Options{
Name: "My Application",
Protocols: []application.Protocol{
{
Scheme: "myapp",
Description: "My Application Protocol",
},
},
})
```
The NSIS installer:
1. Registers all protocols during installation
2. Associates them with your application executable
3. Removes them during uninstallation
**No additional configuration needed!**
### Customization
Customize the installer via `wails.json`:
```json
{
"name": "MyApp",
"outputfilename": "myapp.exe",
"nsis": {
"companyName": "My Company",
"productName": "My Application",
"productDescription": "An amazing desktop application",
"productVersion": "1.0.0",
"installerIcon": "build/appicon.ico",
"license": "LICENSE.txt",
"allowInstallDirCustomization": true,
"installDirectory": "$PROGRAMFILES64\\MyApp",
"createDesktopShortcut": true,
"runAfterInstall": true,
"adminPrivileges": false
}
}
```
### NSIS Options
| Option | Type | Description | Default |
|--------|------|-------------|---------|
| `companyName` | string | Company name shown in installer | Project name |
| `productName` | string | Product name | App name |
| `productDescription` | string | Description shown in installer | App description |
| `productVersion` | string | Version number (e.g., "1.0.0") | "1.0.0" |
| `installerIcon` | string | Path to .ico file for installer | App icon |
| `license` | string | Path to license file (txt) | None |
| `allowInstallDirCustomization` | boolean | Let users choose install location | true |
| `installDirectory` | string | Default installation directory | `$PROGRAMFILES64\<ProductName>` |
| `createDesktopShortcut` | boolean | Create desktop shortcut | true |
| `runAfterInstall` | boolean | Run app after installation | false |
| `adminPrivileges` | boolean | Require admin rights to install | false |
### Advanced NSIS
#### Custom NSIS Script
For advanced customization, provide your own NSIS script:
```bash
# Create custom template
cp build/nsis/installer.nsi build/nsis/installer.custom.nsi
# Edit build/nsis/installer.custom.nsi
# Add custom sections, pages, or logic
# Build with custom script
wails3 task nsis --script build/nsis/installer.custom.nsi
```
#### Available Macros
Wails provides NSIS macros for common tasks:
**wails.associateCustomProtocols**
- Registers all custom URL protocols defined in `application.Options.Protocols`
- Called automatically during installation
- Creates registry entries under `HKEY_CURRENT_USER\SOFTWARE\Classes\`
**wails.unassociateCustomProtocols**
- Removes custom URL protocol registrations
- Called automatically during uninstallation
- Cleans up all protocol-related registry entries
**Example usage in custom NSIS script:**
```nsis
Section "Install"
# ... your installation code ...
# Register custom protocols
!insertmacro wails.associateCustomProtocols
# ... more installation code ...
SectionEnd
Section "Uninstall"
# Remove custom protocols
!insertmacro wails.unassociateCustomProtocols
# ... your uninstallation code ...
SectionEnd
```
## MSI Installer
Windows Installer Package format with Windows logo certification support.
### Build MSI
```bash
wails3 build
wails3 task msi
```
Generates `build/bin/<AppName>-<version>-<arch>.msi`.
### Customization
Configure via `wails.json`:
```json
{
"msi": {
"productCode": "{GUID}",
"upgradeCode": "{GUID}",
"manufacturer": "My Company",
"installScope": "perMachine",
"shortcuts": {
"desktop": true,
"startMenu": true
}
}
}
```
### MSI vs NSIS
| Feature | NSIS | MSI |
|---------|------|-----|
| Customization | ✅ High | ⚠️ Limited |
| File Size | ✅ Smaller | ⚠️ Larger |
| Corporate Deployment | ⚠️ Less common | ✅ Preferred |
| Custom UI | ✅ Full control | ⚠️ Restricted |
| Windows Logo | ❌ No | ✅ Yes |
| Protocol Registration | ✅ Automatic | ⚠️ Manual |
**Use NSIS when:**
- You want maximum customization
- You need custom branding and UI
- You want automatic protocol registration
- File size matters
**Use MSI when:**
- You need Windows logo certification
- You're deploying in enterprise environments
- You need Group Policy support
- You want Windows Update integration
## Portable Executable
Single executable with no installation required.
### Build Portable
```bash
wails3 build
```
Output: `build/bin/<appname>.exe`
### Characteristics
- No installation required
- No registry changes
- No administrator privileges needed
- Can run from USB drives
- No automatic updates
- No Start Menu integration
### Use Cases
- Trial versions
- USB stick applications
- Corporate environments with restricted installations
- Quick testing and demos
## MSIX Packages
Modern Windows app package format for Microsoft Store and sideloading.
See [MSIX Packaging Guide](/guides/build/msix) for detailed information.
## Code Signing
Sign your executables and installers to:
- Avoid Windows SmartScreen warnings
- Establish publisher identity
- Enable automatic updates
- Meet corporate security requirements
See [Code Signing Guide](/guides/build/signing) for details.
## Icon Requirements
### Application Icon
**Format:** `.ico` file with multiple resolutions
**Recommended sizes:**
- 16x16, 32x32, 48x48, 64x64, 128x128, 256x256
**Create from PNG:**
```bash
# Using ImageMagick
magick convert icon.png -define icon:auto-resize=256,128,64,48,32,16 appicon.ico
# Using online tools
# https://icoconvert.com/
```
### Installer Icon
Same `.ico` file can be used for both application and installer.
Configure in `wails.json`:
```json
{
"nsis": {
"installerIcon": "build/appicon.ico"
},
"windows": {
"applicationIcon": "build/appicon.ico"
}
}
```
## Building for Different Architectures
### AMD64 (64-bit)
```bash
wails3 build -platform windows/amd64
```
### ARM64
```bash
wails3 build -platform windows/arm64
```
### Universal Build
Build for all architectures:
```bash
wails3 build -platform windows/amd64,windows/arm64
```
## Distribution Checklist
Before distributing your Windows application:
- [ ] Test installer on clean Windows installation
- [ ] Verify application icon displays correctly
- [ ] Test uninstaller completely removes application
- [ ] Verify Start Menu shortcuts work
- [ ] Test custom protocol handlers (if used)
- [ ] Check SmartScreen behavior (sign your app if possible)
- [ ] Test on Windows 10 and Windows 11
- [ ] Verify file associations work (if used)
- [ ] Test with antivirus software
- [ ] Include license and documentation
## Troubleshooting
### NSIS Build Fails
**Error:** `makensis: command not found`
**Solution:**
```bash
# Install NSIS
winget install NSIS.NSIS
# Or download from https://nsis.sourceforge.io/
```
### Custom Protocols Not Working
**Check registration:**
```powershell
# Check registry
Get-ItemProperty -Path "HKCU:\SOFTWARE\Classes\myapp"
# Test protocol
Start-Process "myapp://test"
```
**Fix:**
1. Reinstall with NSIS installer
2. Run installer as administrator if needed
3. Verify `Protocols` configuration in Go code
### SmartScreen Warning
**Cause:** Unsigned executable
**Solutions:**
1. **Code sign your application** (recommended)
2. Build reputation (downloads over time)
3. Submit to Microsoft for analysis
4. Use Extended Validation (EV) certificate for immediate trust
### Installer Won't Run
**Possible causes:**
- Antivirus blocking
- Missing dependencies
- Corrupted download
- User permissions
**Solutions:**
1. Temporarily disable antivirus for testing
2. Run as administrator
3. Re-download installer
4. Check Windows Event Viewer for errors
## Best Practices
### ✅ Do
- **Code sign your releases** - Avoids SmartScreen warnings
- **Test on clean Windows installations** - Don't rely on dev environment
- **Provide both installer and portable versions** - Give users choice
- **Include comprehensive uninstaller** - Remove all traces
- **Use semantic versioning** - Clear version numbering
- **Test protocol handlers thoroughly** - Validate all URL inputs
- **Provide clear installation instructions** - Help users succeed
### ❌ Don't
- **Don't skip code signing** - Users will see scary warnings
- **Don't require admin for normal apps** - Only if truly necessary
- **Don't install to non-standard locations** - Use `$PROGRAMFILES64`
- **Don't leave orphaned registry entries** - Clean up properly
- **Don't forget to test uninstaller** - Broken uninstallers frustrate users
- **Don't hardcode paths** - Use Windows environment variables
## Next Steps
- [Code Signing](/guides/build/signing) - Sign your executables and installers
- [MSIX Packaging](/guides/build/msix) - Modern Windows app packages
- [Custom Protocols](/guides/distribution/custom-protocols) - Deep linking and URL schemes
- [Auto-Updates](/guides/distribution/auto-updates) - Keep your app current
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [Windows packaging examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).

View file

@ -0,0 +1,176 @@
---
title: Building Applications
description: Build and package your Wails application
sidebar:
order: 1
---
import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components";
## Overview
Wails provides simple commands to build your application for development and production.
## Development Build
### Quick Start
```bash
wails3 dev
```
**Features:**
- Hot reload for frontend changes
- Automatic Go rebuild on changes
- Debug mode enabled
- Fast iteration
### Dev Options
```bash
# Specify frontend dev server
wails3 dev -devserver http://localhost:5173
# Skip frontend dev server
wails3 dev -nofrontend
# Custom build flags
wails3 dev -tags dev
```
## Production Build
### Basic Build
```bash
wails3 build
```
**Output:** Optimized binary in `build/bin/`
### Build Options
```bash
# Build for specific platform
wails3 build -platform windows/amd64
# Custom output directory
wails3 build -o ./dist/myapp
# Skip frontend build
wails3 build -nofrontend
# Production optimizations
wails3 build -ldflags "-s -w"
```
## Build Configuration
### wails.json
```json
{
"name": "myapp",
"frontend": {
"dir": "./frontend",
"install": "npm install",
"build": "npm run build",
"dev": "npm run dev",
"devServerUrl": "http://localhost:5173"
},
"build": {
"output": "myapp",
"ldflags": "-s -w"
}
}
```
## Platform-Specific Builds
<Tabs syncKey="platform">
<TabItem label="Windows" icon="seti:windows">
```bash
# Windows executable
wails3 build -platform windows/amd64
# With icon
wails3 build -platform windows/amd64 -icon icon.ico
# Console app (shows terminal)
wails3 build -platform windows/amd64 -windowsconsole
```
</TabItem>
<TabItem label="macOS" icon="apple">
```bash
# macOS app bundle
wails3 build -platform darwin/amd64
# Universal binary (Intel + Apple Silicon)
wails3 build -platform darwin/universal
# With icon
wails3 build -platform darwin/amd64 -icon icon.icns
```
</TabItem>
<TabItem label="Linux" icon="linux">
```bash
# Linux executable
wails3 build -platform linux/amd64
# With icon
wails3 build -platform linux/amd64 -icon icon.png
```
</TabItem>
</Tabs>
## Optimization
### Binary Size
```bash
# Strip debug symbols
wails3 build -ldflags "-s -w"
# UPX compression (external tool)
upx --best --lzma build/bin/myapp
```
### Performance
```bash
# Enable optimizations
wails3 build -tags production
# Disable debug features
wails3 build -ldflags "-X main.debug=false"
```
## Troubleshooting
### Build Fails
**Problem:** Build errors
**Solutions:**
- Check `go.mod` is up to date
- Run `go mod tidy`
- Verify frontend builds: `cd frontend && npm run build`
- Check wails.json configuration
### Large Binary Size
**Problem:** Binary is too large
**Solutions:**
- Use `-ldflags "-s -w"` to strip symbols
- Remove unused dependencies
- Use UPX compression
- Check embedded assets size
## Next Steps
- [Cross-Platform Building](/guides/cross-platform) - Build for multiple platforms
- [Distribution](/guides/installers) - Create installers and packages
- [Build System](/concepts/build-system) - Understand the build system

View file

@ -1,7 +1,6 @@
---
title: CLI Reference
description: Complete reference for the Wails CLI commands
sidebar:
sidebar:
order: 1
---

View file

@ -0,0 +1,202 @@
---
title: Cross-Platform Building
description: Build for multiple platforms from a single machine
sidebar:
order: 2
---
## Overview
Wails supports cross-platform compilation, allowing you to build for Windows, macOS, and Linux from any platform.
## Supported Platforms
```bash
# List available platforms
wails3 build -platform list
# Common platforms:
# - windows/amd64
# - darwin/amd64
# - darwin/arm64
# - darwin/universal
# - linux/amd64
# - linux/arm64
```
## Building for Windows
### From macOS/Linux
```bash
# Install dependencies (one-time)
# macOS: brew install mingw-w64
# Linux: apt-get install mingw-w64
# Build for Windows
wails3 build -platform windows/amd64
```
### Windows-Specific Options
```bash
# With custom icon
wails3 build -platform windows/amd64 -icon app.ico
# Console application
wails3 build -platform windows/amd64 -windowsconsole
# With manifest
wails3 build -platform windows/amd64 -manifest app.manifest
```
## Building for macOS
### From Windows/Linux
```bash
# Note: macOS builds from other platforms have limitations
# Recommended: Use macOS for macOS builds
# Build for Intel Macs
wails3 build -platform darwin/amd64
# Build for Apple Silicon
wails3 build -platform darwin/arm64
# Universal binary (both architectures)
wails3 build -platform darwin/universal
```
### macOS-Specific Options
```bash
# With custom icon
wails3 build -platform darwin/universal -icon app.icns
# Code signing (macOS only)
wails3 build -platform darwin/universal -codesign "Developer ID"
```
## Building for Linux
### From Any Platform
```bash
# Build for Linux AMD64
wails3 build -platform linux/amd64
# Build for Linux ARM64
wails3 build -platform linux/arm64
```
### Linux-Specific Options
```bash
# With custom icon
wails3 build -platform linux/amd64 -icon app.png
```
## Build Matrix
### Build All Platforms
```bash
#!/bin/bash
# build-all.sh
platforms=("windows/amd64" "darwin/universal" "linux/amd64")
for platform in "${platforms[@]}"; do
echo "Building for $platform..."
wails3 build -platform "$platform" -o "dist/myapp-$platform"
done
```
### Using Taskfile
```yaml
# Taskfile.yml
version: '3'
tasks:
build:all:
desc: Build for all platforms
cmds:
- task: build:windows
- task: build:macos
- task: build:linux
build:windows:
desc: Build for Windows
cmds:
- wails3 build -platform windows/amd64 -o dist/myapp-windows.exe
build:macos:
desc: Build for macOS
cmds:
- wails3 build -platform darwin/universal -o dist/myapp-macos
build:linux:
desc: Build for Linux
cmds:
- wails3 build -platform linux/amd64 -o dist/myapp-linux
```
## CI/CD Integration
### GitHub Actions
```yaml
name: Build
on: [push]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install Wails
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
- name: Build
run: wails3 build
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: app-${{ matrix.os }}
path: build/bin/
```
## Best Practices
### ✅ Do
- Test on target platforms
- Use CI/CD for builds
- Version your builds
- Include platform in filename
- Document build requirements
### ❌ Don't
- Don't skip testing on target platform
- Don't forget platform-specific icons
- Don't hardcode paths
- Don't ignore build warnings
## Next Steps
- [Creating Installers](/guides/installers) - Package your application
- [Building](/guides/building) - Basic building guide

View file

@ -127,7 +127,7 @@ While Wails aims for a unified event, the underlying mechanism for custom protoc
- **How it Works:** The installer registers your application executable to be called with the URL as a command-line argument (`%1`). For example, `your_app.exe "myapp://some/data"`.
- The Wails runtime for Windows (`v3/pkg/application/application_windows.go`) has been updated to check `os.Args` upon startup. If it detects an argument that looks like a URL (e.g., `os.Args[1]` contains `"://"`), it now emits the `events.Common.ApplicationLaunchedWithUrl` event with this URL.
<Aside type="important">
<Aside type="caution">
For Windows, custom protocol schemes are typically only registered when your application is installed via an installer (like the one generated by Wails with NSIS). Running the bare executable might not have the schemes registered system-wide.
</Aside>

View file

@ -0,0 +1,593 @@
---
title: Custom URL Protocols
description: Register custom URL schemes to launch your application from links
sidebar:
order: 3
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
Custom URL protocols (also called URL schemes) allow your application to be launched when users click links with your custom protocol, such as `myapp://action` or `myapp://open/document`.
## Overview
Custom protocols enable:
- **Deep linking**: Launch your app with specific data
- **Browser integration**: Handle links from web pages
- **Email links**: Open your app from email clients
- **Inter-app communication**: Launch from other applications
**Example**: `myapp://open/document?id=123` launches your app and opens document 123.
## Configuration
Define custom protocols in your application options:
```go
package main
import "github.com/wailsapp/wails/v3/pkg/application"
func main() {
app := application.New(application.Options{
Name: "My Application",
Description: "My awesome application",
Protocols: []application.Protocol{
{
Scheme: "myapp",
Description: "My Application Protocol",
Role: "Editor", // macOS only
},
},
})
// Register handler for protocol events
app.OnEvent(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
url := event.Context().ClickedURL()
handleCustomURL(url)
})
app.Run()
}
func handleCustomURL(url string) {
// Parse and handle the custom URL
// Example: myapp://open/document?id=123
println("Received URL:", url)
}
```
## Protocol Handler
Listen for protocol events to handle incoming URLs:
```go
app.OnEvent(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
url := event.Context().ClickedURL()
// Parse the URL
parsedURL, err := parseCustomURL(url)
if err != nil {
app.Logger.Error("Failed to parse URL:", err)
return
}
// Handle different actions
switch parsedURL.Action {
case "open":
openDocument(parsedURL.DocumentID)
case "settings":
showSettings()
case "user":
showUser Profile(parsedURL.UserID)
default:
app.Logger.Warn("Unknown action:", parsedURL.Action)
}
})
```
## URL Structure
Design clear, hierarchical URL structures:
```
myapp://action/resource?param=value
Examples:
myapp://open/document?id=123
myapp://settings/theme?mode=dark
myapp://user/profile?username=john
```
**Best practices:**
- Use lowercase scheme names
- Keep schemes short and memorable
- Use hierarchical paths for resources
- Include query parameters for optional data
- URL-encode special characters
## Platform Registration
Custom protocols are registered differently on each platform.
<Tabs syncKey="platform">
<TabItem label="Windows" icon="seti:windows">
### Windows NSIS Installer
**Wails v3 automatically registers custom protocols** when using NSIS installers.
#### Automatic Registration
When you build your application with `wails3 build`, the NSIS installer:
1. Automatically registers all protocols defined in `application.Options.Protocols`
2. Associates protocols with your application executable
3. Sets up proper registry entries
4. Removes protocol associations during uninstall
**No additional configuration required!**
#### How It Works
The NSIS template includes built-in macros:
- `wails.associateCustomProtocols` - Registers protocols during installation
- `wails.unassociateCustomProtocols` - Removes protocols during uninstall
These macros are automatically called based on your `Protocols` configuration.
#### Manual Registry (Advanced)
If you need manual registration (outside NSIS):
```batch
@echo off
REM Register custom protocol
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp" /ve /d "URL:My Application Protocol" /f
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp" /v "URL Protocol" /t REG_SZ /d "" /f
REG ADD "HKEY_CURRENT_USER\SOFTWARE\Classes\myapp\shell\open\command" /ve /d "\"%1\"" /f
```
#### Testing
Test your protocol registration:
```powershell
# Open protocol URL from PowerShell
Start-Process "myapp://test/action"
# Or from command prompt
start myapp://test/action
```
</TabItem>
<TabItem label="macOS" icon="apple">
### Info.plist Configuration
On macOS, protocols are registered via your `Info.plist` file.
#### Automatic Configuration
Wails automatically generates the `Info.plist` with your protocols when you build with `wails3 build`.
The protocols from `application.Options.Protocols` are added to:
```xml
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>My Application Protocol</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
```
#### Testing
```bash
# Open protocol URL from terminal
open "myapp://test/action"
# Check registered handlers
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump | grep myapp
```
</TabItem>
<TabItem label="Linux" icon="linux">
### Desktop Entry
On Linux, protocols are registered via `.desktop` files.
#### Automatic Configuration
Wails generates a desktop entry file with protocol handlers when you build with `wails3 build`.
**Fixed in v3**: Linux desktop template now properly includes protocol handling.
The generated desktop file includes:
```ini
[Desktop Entry]
Type=Application
Name=My Application
Exec=/usr/bin/myapp %u
MimeType=x-scheme-handler/myapp;
```
#### Manual Registration
If needed, manually install the desktop file:
```bash
# Copy desktop file
cp myapp.desktop ~/.local/share/applications/
# Update desktop database
update-desktop-database ~/.local/share/applications/
# Register protocol handler
xdg-mime default myapp.desktop x-scheme-handler/myapp
```
#### Testing
```bash
# Open protocol URL
xdg-open "myapp://test/action"
# Check registered handler
xdg-mime query default x-scheme-handler/myapp
```
</TabItem>
</Tabs>
## Complete Example
Here's a complete example handling multiple protocol actions:
```go
package main
import (
"fmt"
"net/url"
"strings"
"github.com/wailsapp/wails/v3/pkg/application"
)
type App struct {
app *application.Application
window *application.WebviewWindow
}
func main() {
app := application.New(application.Options{
Name: "DeepLink Demo",
Description: "Custom protocol demonstration",
Protocols: []application.Protocol{
{
Scheme: "deeplink",
Description: "DeepLink Demo Protocol",
Role: "Editor",
},
},
})
myApp := &App{app: app}
myApp.setup()
app.Run()
}
func (a *App) setup() {
// Create window
a.window = a.app.NewWebviewWindow(application.WebviewWindowOptions{
Title: "DeepLink Demo",
Width: 800,
Height: 600,
URL: "http://wails.localhost/",
})
// Handle custom protocol URLs
a.app.OnEvent(application.Events.ApplicationOpenedWithURL, func(event *application.ApplicationEvent) {
customURL := event.Context().ClickedURL()
a.handleDeepLink(customURL)
})
}
func (a *App) handleDeepLink(rawURL string) {
// Parse URL
parsedURL, err := url.Parse(rawURL)
if err != nil {
a.app.Logger.Error("Failed to parse URL:", err)
return
}
// Bring window to front
a.window.Show()
a.window.SetFocus()
// Extract path and query
path := strings.Trim(parsedURL.Path, "/")
query := parsedURL.Query()
// Handle different actions
parts := strings.Split(path, "/")
if len(parts) == 0 {
return
}
action := parts[0]
switch action {
case "open":
if len(parts) >= 2 {
resource := parts[1]
id := query.Get("id")
a.openResource(resource, id)
}
case "settings":
section := ""
if len(parts) >= 2 {
section = parts[1]
}
a.openSettings(section)
case "user":
if len(parts) >= 2 {
username := parts[1]
a.openUserProfile(username)
}
default:
a.app.Logger.Warn("Unknown action:", action)
}
}
func (a *App) openResource(resourceType, id string) {
fmt.Printf("Opening %s with ID: %s\n", resourceType, id)
// Emit event to frontend
a.app.Event.Emit("navigate", map[string]string{
"type": resourceType,
"id": id,
})
}
func (a *App) openSettings(section string) {
fmt.Printf("Opening settings section: %s\n", section)
a.app.Event.Emit("navigate", map[string]string{
"page": "settings",
"section": section,
})
}
func (a *App) openUserProfile(username string) {
fmt.Printf("Opening user profile: %s\n", username)
a.app.Event.Emit("navigate", map[string]string{
"page": "user",
"user": username,
})
}
```
## Frontend Integration
Handle navigation events in your frontend:
```javascript
import { Events } from '@wailsio/runtime'
// Listen for navigation events from protocol handler
Events.On('navigate', (event) => {
const { type, id, page, section, user } = event.data
if (type === 'document') {
// Open document with ID
router.push(`/document/${id}`)
} else if (page === 'settings') {
// Open settings
router.push(`/settings/${section}`)
} else if (page === 'user') {
// Open user profile
router.push(`/user/${user}`)
}
})
```
## Security Considerations
### Validate All Input
Always validate and sanitize URLs from external sources:
```go
func (a *App) handleDeepLink(rawURL string) {
// Parse URL
parsedURL, err := url.Parse(rawURL)
if err != nil {
a.app.Logger.Error("Invalid URL:", err)
return
}
// Validate scheme
if parsedURL.Scheme != "myapp" {
a.app.Logger.Warn("Invalid scheme:", parsedURL.Scheme)
return
}
// Validate path
path := strings.Trim(parsedURL.Path, "/")
if !isValidPath(path) {
a.app.Logger.Warn("Invalid path:", path)
return
}
// Sanitize parameters
params := sanitizeQueryParams(parsedURL.Query())
// Process validated URL
a.processDeepLink(path, params)
}
func isValidPath(path string) bool {
// Only allow alphanumeric and forward slashes
validPath := regexp.MustCompile(`^[a-zA-Z0-9/]+$`)
return validPath.MatchString(path)
}
func sanitizeQueryParams(query url.Values) map[string]string {
sanitized := make(map[string]string)
for key, values := range query {
if len(values) > 0 {
// Take first value and sanitize
sanitized[key] = sanitizeString(values[0])
}
}
return sanitized
}
```
### Prevent Injection Attacks
Never execute URLs directly as code or SQL:
```go
// ❌ DON'T: Execute URL content
func badHandler(url string) {
exec.Command("sh", "-c", url).Run() // DANGEROUS!
}
// ✅ DO: Parse and validate
func goodHandler(url string) {
parsed, _ := url.Parse(url)
action := parsed.Query().Get("action")
// Whitelist allowed actions
allowed := map[string]bool{
"open": true,
"settings": true,
"help": true,
}
if allowed[action] {
handleAction(action)
}
}
```
## Testing
### Manual Testing
Test protocol handlers during development:
**Windows:**
```powershell
Start-Process "myapp://test/action?id=123"
```
**macOS:**
```bash
open "myapp://test/action?id=123"
```
**Linux:**
```bash
xdg-open "myapp://test/action?id=123"
```
### HTML Testing
Create a test HTML page:
```html
<!DOCTYPE html>
<html>
<head>
<title>Protocol Test</title>
</head>
<body>
<h1>Custom Protocol Test Links</h1>
<ul>
<li><a href="myapp://open/document?id=123">Open Document 123</a></li>
<li><a href="myapp://settings/theme?mode=dark">Dark Mode Settings</a></li>
<li><a href="myapp://user/profile?username=john">User Profile</a></li>
</ul>
</body>
</html>
```
## Troubleshooting
### Protocol Not Registered
**Windows:**
- Check registry: `HKEY_CURRENT_USER\SOFTWARE\Classes\<scheme>`
- Reinstall with NSIS installer
- Verify installer ran with proper permissions
**macOS:**
- Rebuild application with `wails3 build`
- Check `Info.plist` in app bundle: `MyApp.app/Contents/Info.plist`
- Reset Launch Services: `/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill`
**Linux:**
- Check desktop file: `~/.local/share/applications/myapp.desktop`
- Update database: `update-desktop-database ~/.local/share/applications/`
- Verify handler: `xdg-mime query default x-scheme-handler/myapp`
### Application Not Launching
**Check logs:**
```go
app := application.New(application.Options{
Logger: application.NewLogger(application.LogLevelDebug),
// ...
})
```
**Common issues:**
- Application not installed in expected location
- Executable path in registration doesn't match actual location
- Permissions issues
## Best Practices
### ✅ Do
- **Use descriptive scheme names** - `mycompany-myapp` instead of `mca`
- **Validate all input** - Never trust URLs from external sources
- **Handle errors gracefully** - Log invalid URLs, don't crash
- **Provide user feedback** - Show what action was triggered
- **Test on all platforms** - Protocol handling varies
- **Document your URL structure** - Help users and integrators
### ❌ Don't
- **Don't use common scheme names** - Avoid `http`, `file`, `app`, etc.
- **Don't execute URLs as code** - Huge security risk
- **Don't expose sensitive operations** - Require confirmation for destructive actions
- **Don't assume protocols work everywhere** - Have fallback mechanisms
- **Don't forget URL encoding** - Handle special characters properly
## Next Steps
- [Windows Packaging](/guides/build/windows) - Learn about NSIS installer options
- [File Associations](/guides/distribution/file-associations) - Open files with your app
- [Single Instance](/guides/distribution/single-instance) - Prevent multiple app instances
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).

View file

@ -0,0 +1,173 @@
---
title: End-to-End Testing
description: Test complete user workflows
sidebar:
order: 6
---
## Overview
End-to-end testing validates complete user workflows in your application.
## Using Playwright
### Setup
```bash
# Install Playwright
npm install -D @playwright/test
# Initialize
npx playwright install
```
### Configuration
```javascript
// playwright.config.js
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
use: {
baseURL: 'http://localhost:34115', // Wails dev server
},
})
```
### Writing Tests
```javascript
// e2e/app.spec.js
import { test, expect } from '@playwright/test'
test('create note', async ({ page }) => {
await page.goto('/')
// Click new note button
await page.click('#new-note-btn')
// Fill in title
await page.fill('#note-title', 'Test Note')
// Fill in content
await page.fill('#note-content', 'Test content')
// Verify note appears in list
await expect(page.locator('.note-item')).toContainText('Test Note')
})
test('delete note', async ({ page }) => {
await page.goto('/')
// Create a note first
await page.click('#new-note-btn')
await page.fill('#note-title', 'To Delete')
// Delete it
await page.click('#delete-btn')
// Confirm dialog
page.on('dialog', dialog => dialog.accept())
// Verify it's gone
await expect(page.locator('.note-item')).not.toContainText('To Delete')
})
```
## Testing dialogs
```javascript
test('file save dialog', async ({ page }) => {
await page.goto('/')
// Intercept file dialog
page.on('filechooser', async (fileChooser) => {
await fileChooser.setFiles('/path/to/test/file.json')
})
// Trigger save
await page.click('#save-btn')
// Verify success message
await expect(page.locator('.success-message')).toBeVisible()
})
```
## Testing Window Behaviour
```javascript
test('window state', async ({ page }) => {
await page.goto('/')
// Test window title
await expect(page).toHaveTitle('My App')
// Test window size
const size = await page.viewportSize()
expect(size.width).toBe(800)
expect(size.height).toBe(600)
})
```
## Best Practices
### ✅ Do
- Test critical user flows
- Use data-testid attributes
- Clean up test data
- Run tests in CI/CD
- Test error scenarios
- Keep tests independent
### ❌ Don't
- Don't test implementation details
- Don't write brittle selectors
- Don't skip cleanup
- Don't ignore flaky tests
- Don't test everything
## Running E2E Tests
```bash
# Run all tests
npx playwright test
# Run in headed mode
npx playwright test --headed
# Run specific test
npx playwright test e2e/app.spec.js
# Debug mode
npx playwright test --debug
```
## CI/CD Integration
```yaml
# .github/workflows/e2e.yml
name: E2E Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run tests
run: npx playwright test
```
## Next Steps
- [Testing](/guides/testing) - Learn unit testing
- [Building](/guides/building) - Build your application

View file

@ -74,24 +74,41 @@ import (
func (s *Service) UpdateData() {
// Do some data processing...
// Notify the frontend
app := application.Get()
app.Event.EmitEvent(&application.CustomEvent{
Name: "my-app:data-updated",
Data: map[string]interface{}{
app.Event.Emit("my-app:data-updated",
map[string]interface{}{
"timestamp": time.Now(),
"count": 42,
},
})
)
}
```
### Emitting Events (Frontend)
While not as commonly used, you can also emit events from your frontend that your Go code can listen to:
```javascript
import { Events } from '@wailsio/runtime';
// Event without data
Events.Emit('myapp:close-window')
// Event with data
Events.Emit('myapp:disconnect-requested', 'id-123')
```
If you are using TypeScript in your frontend and [registering typed events](#typed-events-with-typecheck) in your Go code, you will get event name autocomplete/checking and data type checking.
### Removing Event Listeners
Always clean up your event listeners when they're no longer needed:
```javascript
import { Events } from '@wailsio/runtime';
// Store the handler reference
const focusHandler = () => {
console.log('Window focused');
@ -114,6 +131,8 @@ Events.Off('common:WindowFocus');
Many applications need to pause certain activities when the window loses focus:
```javascript
import { Events } from '@wailsio/runtime';
let animationRunning = true;
Events.On('common:WindowLostFocus', () => {
@ -132,9 +151,11 @@ Events.On('common:WindowFocus', () => {
Keep your app in sync with the system theme:
```javascript
import { Events } from '@wailsio/runtime';
Events.On('common:ThemeChanged', (event) => {
const isDarkMode = event.data.isDark;
if (isDarkMode) {
document.body.classList.add('dark-theme');
document.body.classList.remove('light-theme');
@ -150,9 +171,11 @@ Events.On('common:ThemeChanged', (event) => {
Make your app accept dragged files:
```javascript
import { Events } from '@wailsio/runtime';
Events.On('common:WindowFilesDropped', (event) => {
const files = event.data.files;
files.forEach(file => {
console.log('File dropped:', file);
// Process the dropped files
@ -166,10 +189,12 @@ Events.On('common:WindowFilesDropped', (event) => {
Respond to window state changes:
```javascript
import { Events } from '@wailsio/runtime';
Events.On('common:WindowClosing', () => {
// Save user data before closing
saveApplicationState();
// You could also prevent closing by returning false
// from a registered window close handler
});
@ -190,6 +215,8 @@ Events.On('common:WindowRestore', () => {
Handle platform-specific events when needed:
```javascript
import { Events } from '@wailsio/runtime';
// Windows-specific power management
Events.On('windows:APMSuspend', () => {
console.log('System is going to sleep');
@ -210,7 +237,7 @@ Events.On('mac:ApplicationWillTerminate', () => {
## Creating Custom Events
You can create your own events for application-specific needs:
You can create your own events for application-specific needs.
### Backend (Go)
@ -222,15 +249,13 @@ func (s *Service) ProcessUserData(userData UserData) error {
app := application.Get()
// Notify all listeners
app.Event.EmitEvent(&application.CustomEvent{
Name: "user:data-processed",
Data: map[string]interface{}{
app.Event.Emit("user:data-processed",
map[string]interface{}{
"userId": userData.ID,
"status": "completed",
"timestamp": time.Now(),
},
})
)
return nil
}
@ -250,10 +275,12 @@ func (s *Service) StartMonitoring() {
### Frontend (JavaScript)
```javascript
import { Events } from '@wailsio/runtime';
// Listen for your custom events
Events.On('user:data-processed', (event) => {
const { userId, status, timestamp } = event.data;
showNotification(`User ${userId} processing ${status}`);
updateUIWithNewData();
});
@ -263,6 +290,128 @@ Events.On('monitor:stats-updated', (event) => {
});
```
## Typed Events with Type Safety
Wails v3 supports typed events with full TypeScript type safety through event registration and automatic binding generation.
### Registering Custom Events
Call `application.RegisterEvent` at init time to register custom event names with their data types:
```go
package main
import "github.com/wailsapp/wails/v3/pkg/application"
type UserLoginData struct {
UserID string
Username string
LoginTime string
}
type MonitorStats struct {
CPUUsage float64
MemoryUsage float64
}
func init() {
// Register events with their data types
application.RegisterEvent[UserLoginData]("user:login")
application.RegisterEvent[MonitorStats]("monitor:stats")
// Register events without data (void events)
application.RegisterEvent[application.Void]("app:ready")
}
```
:::caution
`RegisterEvent` is meant to be called at init time and will panic if:
- Arguments are not valid
- The same event name is registered twice with different data types
:::
:::note
It is safe to register the same event multiple times as long as the data type is always the same. This can be useful to ensure an event is registered when any of multiple packages is loaded.
:::
### Benefits of Event Registration
Once registered, data arguments passed to `Event.Emit` are type-checked against the specified type. On mismatch:
- An error is emitted and logged (or passed to the registered error handler)
- The offending event will not be propagated
- This ensures the data field of registered events is always assignable to the declared type
### Strict Mode
Use the `strictevents` build tag to enable warnings for unregistered events in development:
```bash
go build -tags strictevents
```
With strict mode enabled, the runtime emits at most one warning per unregistered event name to avoid spamming logs.
### TypeScript Binding Generation
The binding generator outputs TypeScript definitions and glue code for transparent typed event support in the frontend.
#### 1. Set up the Vite Plugin
In your `vite.config.ts`:
```typescript
import { defineConfig } from 'vite'
import wails from '@wailsio/runtime/plugins/vite'
export default defineConfig({
plugins: [wails()],
})
```
#### 2. Generate Bindings
Run the binding generator:
```bash
wails3 generate bindings
```
This creates TypeScript files in your frontend directory with typed event creators and data interfaces.
#### 3. Use Typed Events in Frontend
```typescript
import { Events } from '@wailsio/runtime'
import { UserLogin, MonitorStats } from './bindings/events'
// Type-safe event emission with autocomplete
Events.Emit(UserLogin({
UserID: "123",
Username: "john_doe",
LoginTime: new Date().toISOString()
}))
// Type-safe event listening
Events.On(UserLogin, (event) => {
// event.data is typed as UserLoginData
console.log(`User ${event.data.Username} logged in`)
})
Events.On(MonitorStats, (event) => {
// event.data is typed as MonitorStats
updateDashboard({
cpu: event.data.CPUUsage,
memory: event.data.MemoryUsage
})
})
```
The typed events provide:
- **Autocomplete** for event names
- **Type checking** for event data
- **Compile-time errors** for mismatched data types
- **IntelliSense** documentation
## Event Reference
### Common Events (Cross-platform)
@ -326,6 +475,8 @@ Core Linux window events:
When creating custom events, use namespaces to avoid conflicts:
```javascript
import { Events } from '@wailsio/runtime';
// Good - namespaced events
Events.Emit('myapp:user:login');
Events.Emit('myapp:data:updated');
@ -341,14 +492,16 @@ Events.Emit('update');
Always remove event listeners when components unmount:
```javascript
import { Events } from '@wailsio/runtime';
// React example
useEffect(() => {
const handler = (event) => {
// Handle event
};
Events.On('common:WindowResize', handler);
// Cleanup
return () => {
Events.Off('common:WindowResize', handler);
@ -361,7 +514,7 @@ useEffect(() => {
Check platform availability when using platform-specific events:
```javascript
import { Platform } from '@wailsio/runtime';
import { Platform, Events } from '@wailsio/runtime';
if (Platform.isWindows) {
Events.On('windows:APMSuspend', handleSuspend);
@ -382,6 +535,8 @@ While events are powerful, don't use them for everything:
To debug event issues:
```javascript
import { Events } from '@wailsio/runtime';
// Log all events (development only)
if (isDevelopment) {
const originalOn = Events.On;

View file

@ -9,9 +9,9 @@ This guide demonstrates how to integrate the [Gin web framework](https://github.
Wails v3 provides a flexible asset system that allows you to use any HTTP handler, including popular web frameworks like Gin. This integration enables you to:
- Serve web content using Gin's powerful routing and middleware capabilities
- Create RESTful APIs that can be accessed from your Wails application
- Leverage Gin's extensive feature set whilst maintaining the benefits of Wails
- Serve web content using Gin's routing and middleware
- Create RESTful APIs accessible from your Wails application
- Use Gin's features while maintaining Wails desktop integration
## Setting Up Gin with Wails
@ -102,43 +102,20 @@ app.Window.NewWithOptions(application.WebviewWindowOptions{
## Serving Static Content
There are several ways to serve static content with Gin in a Wails application:
### Option 1: Using Go's embed Package
The recommended approach is to use Go's `embed` package to embed static files into your binary:
Use Go's `embed` package to embed static files into your binary:
```go
//go:embed static
var staticFiles embed.FS
// In your main function:
ginEngine.StaticFS("/static", http.FS(staticFiles))
// Serve index.html
ginEngine.GET("/", func(c *gin.Context) {
file, err := staticFiles.ReadFile("static/index.html")
if err != nil {
c.String(http.StatusInternalServerError, "Error reading index.html")
return
}
file, _ := staticFiles.ReadFile("static/index.html")
c.Data(http.StatusOK, "text/html; charset=utf-8", file)
})
```
### Option 2: Serving from Disk
For development purposes, you might prefer to serve files directly from disk:
```go
// Serve static files from the "static" directory
ginEngine.Static("/static", "./static")
// Serve index.html
ginEngine.GET("/", func(c *gin.Context) {
c.File("./static/index.html")
})
```
For development, serve files directly from disk using `ginEngine.Static("/static", "./static")`.
## Custom Middleware
@ -206,219 +183,42 @@ ginEngine.GET("/api/search", func(c *gin.Context) {
})
```
## Event Communication
## Using Wails Features
One of the powerful features of Wails is its event system, which allows for communication between the frontend and backend. When using Gin as a service, you can still leverage this event system by serving the Wails runtime.js file to your frontend.
Your Gin-served web content can interact with Wails features using the `@wailsio/runtime` package.
### Serving the Wails Runtime
### Event Handling
To enable event communication, you need to serve the Wails runtime.js file at the `/wails/runtime.js` path. Here's how to implement this in your Gin service:
Register event handlers in Go:
```go
import (
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/runtime"
)
// GinService implements a Wails service that uses Gin for HTTP handling
type GinService struct {
ginEngine *gin.Engine
app *application.App
// Other fields...
}
// ServeHTTP implements the http.Handler interface
func (s *GinService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Special handling for Wails runtime.js
if r.URL.Path == "/wails/runtime.js" {
s.serveWailsRuntime(w, r)
return
}
// All other requests go to the Gin router
s.ginEngine.ServeHTTP(w, r)
}
// serveWailsRuntime serves the Wails runtime.js file
func (s *GinService) serveWailsRuntime(w http.ResponseWriter, r *http.Request) {
// Open the runtime.js file from the public runtime package
runtimeFile, err := runtime.Assets.Open(runtime.RuntimeJSPath)
if err != nil {
http.Error(w, "Failed to access runtime assets", http.StatusInternalServerError)
return
}
defer runtimeFile.Close()
// Set the content type
w.Header().Set("Content-Type", "application/javascript")
// Copy the file to the response
_, err = io.Copy(w, runtimeFile)
if err != nil {
http.Error(w, "Failed to serve runtime.js", http.StatusInternalServerError)
}
}
```
### Handling Events
You'll also need to add an endpoint to handle events from the frontend. This endpoint will bridge the gap between the HTTP requests and the Wails event system:
```go
// In your setupRoutes method
func (s *GinService) setupRoutes() {
// Event handling endpoint
s.ginEngine.POST("/events/emit", func(c *gin.Context) {
var eventData struct {
Name string `json:"name" binding:"required"`
Data interface{} `json:"data"`
}
if err := c.ShouldBindJSON(&eventData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Process the event using the Wails event system
s.app.Event.Emit(eventData.Name, eventData.Data)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Event processed successfully",
})
})
// Other routes...
}
```
### Using Events in the Frontend
In your frontend HTML, include the Wails runtime.js script and use the event API:
```html
<!DOCTYPE html>
<html>
<head>
<script src="/wails/runtime.js"></script>
</head>
<body>
<button id="triggerEvent">Trigger Event</button>
<pre id="eventResponse"></pre>
<script>
// Emit an event to the backend
document.getElementById('triggerEvent').addEventListener('click', () => {
ce.Events.Emit("my-event", {
message: "Hello from the frontend!",
timestamp: new Date().toISOString()
});
});
// Listen for events from the backend
ce.Events.On("response-event", (data) => {
document.getElementById('eventResponse').textContent =
JSON.stringify(data, null, 2);
});
// For the runtime.js stub implementation, add this polyfill
if (window.ce && !window.ce._isNative) {
const originalFetch = window.fetch;
window.fetch = async function(url, options) {
if (typeof url === 'string' && url.includes('/wails/events/emit')) {
const req = new Request(url, options);
const data = await req.json();
// Forward the event to the backend through a regular API call
await fetch('/api/events/emit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
});
}
return originalFetch.apply(this, arguments);
};
}
</script>
</body>
</html>
```
This approach allows you to use the Wails event system seamlessly with your Gin service, providing a consistent experience across your application.
## Interacting with Wails
Your Gin-served web content can interact with Wails features like events and bindings. To enable this interaction, you
must use the JavaScript API package `@wailsio/runtime`.
### Handling Wails Events in Go
```go
// Register event handler
app.Event.On("my-event", func(event *application.CustomEvent) {
log.Printf("Received event from frontend: %v", event.Data)
// Process the event...
log.Printf("Received event: %v", event.Data)
})
```
### Emitting Events from JavaScript
Call from JavaScript using the runtime:
```html
<script>
document.getElementById('callApi').addEventListener('click', async () => {
try {
const response = await fetch('/api/hello');
const data = await response.json();
document.getElementById('apiResult').textContent = JSON.stringify(data, null, 2);
} catch (error) {
console.error('Error calling API:', error);
document.getElementById('apiResult').textContent = 'Error: ' + error.message;
}
});
</script>
<script type="module">
import { Events } from '@wailsio/runtime';
Events.Emit("my-event", { message: "Hello from frontend" });
Events.On("response-event", (data) => console.log(data));
</script>
```
## Advanced Configuration
### Customising Gin's Mode
Gin has three modes: debug, release, and test. For production applications, you should use release mode:
Set Gin to release mode for production:
```go
// Set Gin to release mode
gin.SetMode(gin.ReleaseMode)
// Create a new Gin router
gin.SetMode(gin.ReleaseMode) // Use gin.DebugMode for development
ginEngine := gin.New()
```
You can use Go's build tags to set the mode based on the build environment:
```go[main_prod.go]
// +build production
var ginMode = gin.ReleaseMode
```
```go[main_dev.go]
// +build !production
var ginMode = gin.DebugMode
```
```go [main.go]
// In your main function:
gin.SetMode(ginMode)
```
### Handling WebSockets
You can integrate WebSockets with Gin using libraries like Gorilla WebSocket:
@ -447,126 +247,6 @@ ginEngine.GET("/ws", func(c *gin.Context) {
})
```
## Complete Example
Here's a complete example of integrating Gin with Wails:
```go
package main
import (
"embed"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed static
var staticFiles embed.FS
// GinMiddleware creates a middleware that passes requests to Gin if they're not handled by Wails
func GinMiddleware(ginEngine *gin.Engine) application.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Let Wails handle the `/wails` route
if r.URL.Path == "/wails" {
next.ServeHTTP(w, r)
return
}
// Let Gin handle everything else
ginEngine.ServeHTTP(w, r)
})
}
}
// LoggingMiddleware is a Gin middleware that logs request details
func LoggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Start timer
startTime := time.Now()
// Process request
c.Next()
// Calculate latency
latency := time.Since(startTime)
// Log request details
log.Printf("[GIN] %s | %s | %s | %d | %s",
c.Request.Method,
c.Request.URL.Path,
c.ClientIP(),
c.Writer.Status(),
latency,
)
}
}
func main() {
// Create a new Gin router
ginEngine := gin.New() // Using New() instead of Default() to add our own middleware
// Add middlewares
ginEngine.Use(gin.Recovery())
ginEngine.Use(LoggingMiddleware())
// Serve embedded static files
ginEngine.StaticFS("/static", http.FS(staticFiles))
// Define routes
ginEngine.GET("/", func(c *gin.Context) {
file, err := staticFiles.ReadFile("static/index.html")
if err != nil {
c.String(http.StatusInternalServerError, "Error reading index.html")
return
}
c.Data(http.StatusOK, "text/html; charset=utf-8", file)
})
ginEngine.GET("/api/hello", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Hello from Gin API!",
"time": time.Now().Format(time.RFC3339),
})
})
// Create a new Wails application
app := application.New(application.Options{
Name: "Gin Example",
Description: "A demo of using Gin with Wails",
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
Assets: application.AssetOptions{
Handler: ginEngine,
Middleware: GinMiddleware(ginEngine),
},
})
// Register event handler
app.Event.On("gin-button-clicked", func(event *application.CustomEvent) {
log.Printf("Received event from frontend: %v", event.Data)
})
// Create window
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Wails + Gin Example",
Width: 900,
Height: 700,
URL: "/",
})
// Run the app
err := app.Run()
if err != nil {
log.Fatal(err)
}
}
```
## Best Practices
- **Use Go's embed Package:** Embed static files into your binary for better distribution.
@ -578,6 +258,6 @@ func main() {
## Conclusion
Integrating Gin with Wails provides a powerful combination for building desktop applications with web technologies. Gin's performance and feature set complement Wails' desktop integration capabilities, allowing you to create sophisticated applications that leverage the best of both worlds.
Integrating Gin with Wails provides a powerful combination for building desktop applications with web technologies. Gin's performance and feature set complement Wails' desktop integration capabilities, allowing you to create sophisticated applications that use the best of both worlds.
For more information, refer to the [Gin documentation](https://github.com/gin-gonic/gin) and the [Wails documentation](https://wails.io).

View file

@ -127,13 +127,12 @@ func (s *GinService) ServiceStartup(ctx context.Context, options application.Ser
s.app.Logger.Info("Received event from frontend", "data", event.Data)
// Emit an event back to the frontend
s.app.Event.EmitEvent(&application.CustomEvent{
Name: "gin-api-response",
Data: map[string]interface{}{
s.app.Event.Emit("gin-api-response",
map[string]interface{}{
"message": "Response from Gin API Service",
"time": time.Now().Format(time.RFC3339),
},
})
)
})
return nil
@ -316,13 +315,12 @@ s.app.Event.On("gin-api-event", func(event *application.CustomEvent) {
s.app.Logger.Info("Received event from frontend", "data", event.Data)
// Emit an event back to the frontend
s.app.Event.EmitEvent(&application.CustomEvent{
Name: "gin-api-response",
Data: map[string]interface{}{
s.app.Event.Emit("gin-api-response",
map[string]interface{}{
"message": "Response from Gin API Service",
"time": time.Now().Format(time.RFC3339),
},
})
)
})
```
@ -349,10 +347,7 @@ Then use it in your code:
import * as wails from '@wailsio/runtime';
// Event emission
wails.Events.Emit({
name: 'gin-api-event',
data: eventData,
});
wails.Events.Emit('gin-api-event', eventData);
```
Here's an example of how to set up frontend integration:
@ -528,7 +523,7 @@ Here's an example of how to set up frontend integration:
message: "Hello from the frontend!",
timestamp: new Date().toISOString()
};
wails.Events.Emit({name: 'gin-api-event', data: eventData});
wails.Events.Emit('gin-api-event', eventData);
});
// Set up event listener for responses from the backend

View file

@ -0,0 +1,160 @@
---
title: Creating Installers
description: Package your application for distribution
sidebar:
order: 3
---
import { Tabs, TabItem } from "@astrojs/starlight/components";
## Overview
Create professional installers for your Wails application on all platforms.
## Platform Installers
<Tabs syncKey="platform">
<TabItem label="Windows" icon="seti:windows">
### NSIS Installer
```bash
# Install NSIS
# Download from: https://nsis.sourceforge.io/
# Create installer script (installer.nsi)
makensis installer.nsi
```
**installer.nsi:**
```nsis
!define APPNAME "MyApp"
!define VERSION "1.0.0"
Name "${APPNAME}"
OutFile "MyApp-Setup.exe"
InstallDir "$PROGRAMFILES\${APPNAME}"
Section "Install"
SetOutPath "$INSTDIR"
File "build\bin\myapp.exe"
CreateShortcut "$DESKTOP\${APPNAME}.lnk" "$INSTDIR\myapp.exe"
SectionEnd
```
### WiX Toolset
Alternative for MSI installers.
</TabItem>
<TabItem label="macOS" icon="apple">
### DMG Creation
```bash
# Create DMG
hdiutil create -volname "MyApp" -srcfolder build/bin/MyApp.app -ov -format UDZO MyApp.dmg
```
### Code Signing
```bash
# Sign application
codesign --deep --force --verify --verbose --sign "Developer ID" MyApp.app
# Notarize
xcrun notarytool submit MyApp.dmg --apple-id "email" --password "app-password"
```
### App Store
Use Xcode for App Store distribution.
</TabItem>
<TabItem label="Linux" icon="linux">
### DEB Package
```bash
# Create package structure
mkdir -p myapp_1.0.0/DEBIAN
mkdir -p myapp_1.0.0/usr/bin
# Copy binary
cp build/bin/myapp myapp_1.0.0/usr/bin/
# Create control file
cat > myapp_1.0.0/DEBIAN/control << EOF
Package: myapp
Version: 1.0.0
Architecture: amd64
Maintainer: Your Name
Description: My Application
EOF
# Build package
dpkg-deb --build myapp_1.0.0
```
### RPM Package
Use `rpmbuild` for RPM-based distributions.
### AppImage
```bash
# Use appimagetool
appimagetool myapp.AppDir
```
</TabItem>
</Tabs>
## Automated Packaging
### Using GoReleaser
```yaml
# .goreleaser.yml
project_name: myapp
builds:
- binary: myapp
goos:
- windows
- darwin
- linux
goarch:
- amd64
- arm64
archives:
- format: zip
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
nfpms:
- formats:
- deb
- rpm
vendor: Your Company
homepage: https://example.com
description: My Application
```
## Best Practices
### ✅ Do
- Code sign on all platforms
- Include version information
- Create uninstallers
- Test installation process
- Provide clear documentation
### ❌ Don't
- Don't skip code signing
- Don't forget file associations
- Don't hardcode paths
- Don't skip testing
## Next Steps
- [Auto-Updates](/guides/auto-updates) - Implement automatic updates
- [Cross-Platform Building](/guides/cross-platform) - Build for multiple platforms

View file

@ -0,0 +1,341 @@
---
title: Performance Optimisation
description: Optimise your Wails application for maximum performance
sidebar:
order: 9
---
## Overview
Optimise your Wails application for speed, memory efficiency, and responsiveness.
## Frontend Optimisation
### Bundle Size
```javascript
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
},
},
},
}
```
### Code Splitting
```javascript
// Lazy load components
const Settings = lazy(() => import('./Settings'))
function App() {
return (
<Suspense fallback={<Loading />}>
<Settings />
</Suspense>
)
}
```
### Asset Optimisation
```javascript
// Optimise images
import { defineConfig } from 'vite'
import imagemin from 'vite-plugin-imagemin'
export default defineConfig({
plugins: [
imagemin({
gifsicle: { optimizationLevel: 3 },
optipng: { optimizationLevel: 7 },
svgo: { plugins: [{ removeViewBox: false }] },
}),
],
})
```
## Backend Optimisation
### Efficient Bindings
```go
// ❌ Bad: Return everything
func (s *Service) GetAllData() []Data {
return s.db.FindAll() // Could be huge
}
// ✅ Good: Paginate
func (s *Service) GetData(page, size int) (*PagedData, error) {
return s.db.FindPaged(page, size)
}
```
### Caching
```go
type CachedService struct {
cache *lru.Cache
ttl time.Duration
}
func (s *CachedService) GetData(key string) (interface{}, error) {
// Check cache
if val, ok := s.cache.Get(key); ok {
return val, nil
}
// Fetch and cache
data, err := s.fetchData(key)
if err != nil {
return nil, err
}
s.cache.Add(key, data)
return data, nil
}
```
### Goroutines for Long Operations
```go
func (s *Service) ProcessLargeFile(path string) error {
// Process in background
go func() {
result, err := s.process(path)
if err != nil {
s.app.EmitEvent("process-error", err.Error())
return
}
s.app.EmitEvent("process-complete", result)
}()
return nil
}
```
## Memory Optimisation
### Avoid Memory Leaks
```go
// ❌ Bad: Goroutine leak
func (s *Service) StartPolling() {
ticker := time.NewTicker(1 * time.Second)
go func() {
for range ticker.C {
s.poll()
}
}()
// ticker never stopped!
}
// ✅ Good: Proper cleanup
func (s *Service) StartPolling() {
ticker := time.NewTicker(1 * time.Second)
s.stopChan = make(chan bool)
go func() {
for {
select {
case <-ticker.C:
s.poll()
case <-s.stopChan:
ticker.Stop()
return
}
}
}()
}
func (s *Service) StopPolling() {
close(s.stopChan)
}
```
### Pool Resources
```go
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func processData(data []byte) []byte {
buf := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buf)
buf.Reset()
buf.Write(data)
// Process...
return buf.Bytes()
}
```
## Event Optimisation
### Debounce Events
```javascript
// Debounce frequent events
let debounceTimer
function handleInput(value) {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
UpdateData(value)
}, 300)
}
```
### Batch Updates
```go
type BatchProcessor struct {
items []Item
mu sync.Mutex
timer *time.Timer
}
func (b *BatchProcessor) Add(item Item) {
b.mu.Lock()
defer b.mu.Unlock()
b.items = append(b.items, item)
if b.timer == nil {
b.timer = time.AfterFunc(100*time.Millisecond, b.flush)
}
}
func (b *BatchProcessor) flush() {
b.mu.Lock()
items := b.items
b.items = nil
b.timer = nil
b.mu.Unlock()
// Process batch
processBatch(items)
}
```
## Build Optimisation
### Binary Size
```bash
# Strip debug symbols
wails3 build -ldflags "-s -w"
# Reduce binary size further
go build -ldflags="-s -w" -trimpath
```
### Compilation Speed
```bash
# Use build cache
go build -buildmode=default
# Parallel compilation
go build -p 8
```
## Profiling
### CPU Profiling
```go
import "runtime/pprof"
func profileCPU() {
f, _ := os.Create("cpu.prof")
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Code to profile
}
```
### Memory Profiling
```go
import "runtime/pprof"
func profileMemory() {
f, _ := os.Create("mem.prof")
defer f.Close()
runtime.GC()
pprof.WriteHeapProfile(f)
}
```
### Analyse Profiles
```bash
# View CPU profile
go tool pprof cpu.prof
# View memory profile
go tool pprof mem.prof
# Web interface
go tool pprof -http=:8080 cpu.prof
```
## Best Practices
### ✅ Do
- Profile before optimising
- Cache expensive operations
- Use pagination for large datasets
- Debounce frequent events
- Pool resources
- Clean up goroutines
- Optimise bundle size
- Use lazy loading
### ❌ Don't
- Don't optimise prematurely
- Don't ignore memory leaks
- Don't block the main thread
- Don't return huge datasets
- Don't skip profiling
- Don't forget cleanup
## Performance Checklist
- [ ] Frontend bundle optimised
- [ ] Images compressed
- [ ] Code splitting implemented
- [ ] Backend methods paginated
- [ ] Caching implemented
- [ ] Goroutines cleaned up
- [ ] Events debounced
- [ ] Binary size optimised
- [ ] Profiling done
- [ ] Memory leaks fixed
## Next Steps
- [Architecture](/guides/architecture) - Application architecture patterns
- [Testing](/guides/testing) - Test your application
- [Building](/guides/building) - Build optimised binaries

View file

@ -0,0 +1,261 @@
---
title: Security Best Practices
description: Secure your Wails application
sidebar:
order: 8
---
## Overview
Security is critical for desktop applications. Follow these practices to keep your application secure.
## Input Validation
### Always Validate
```go
func (s *UserService) CreateUser(email, password string) (*User, error) {
// Validate email
if !isValidEmail(email) {
return nil, errors.New("invalid email")
}
// Validate password strength
if len(password) < 8 {
return nil, errors.New("password too short")
}
// Sanitise input
email = strings.TrimSpace(email)
email = html.EscapeString(email)
// Continue...
}
```
### Sanitise HTML
```go
import "html"
func (s *Service) SaveComment(text string) error {
// Escape HTML
text = html.EscapeString(text)
// Validate length
if len(text) > 1000 {
return errors.New("comment too long")
}
return s.db.Save(text)
}
```
## Authentication
### Secure Password Storage
```go
import "golang.org/x/crypto/bcrypt"
func hashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(hash), err
}
func verifyPassword(hash, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
```
### Session Management
```go
type Session struct {
UserID int
Token string
ExpiresAt time.Time
}
func (a *AuthService) CreateSession(userID int) (*Session, error) {
token := generateSecureToken()
session := &Session{
UserID: userID,
Token: token,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
return session, a.saveSession(session)
}
```
## Data Protection
### Encrypt Sensitive Data
```go
import "crypto/aes"
import "crypto/cipher"
func encrypt(data []byte, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
return gcm.Seal(nonce, nonce, data, nil), nil
}
```
### Secure Storage
```go
// Use OS keychain for sensitive data
import "github.com/zalando/go-keyring"
func saveAPIKey(key string) error {
return keyring.Set("myapp", "api_key", key)
}
func getAPIKey() (string, error) {
return keyring.Get("myapp", "api_key")
}
```
## Network Security
### Use HTTPS
```go
func makeAPICall(url string) (*Response, error) {
// Always use HTTPS
if !strings.HasPrefix(url, "https://") {
return nil, errors.New("only HTTPS allowed")
}
return http.Get(url)
}
```
### Verify Certificates
```go
import "crypto/tls"
func secureClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
},
}
}
```
## File Operations
### Validate Paths
```go
func readFile(path string) ([]byte, error) {
// Prevent path traversal
if strings.Contains(path, "..") {
return nil, errors.New("invalid path")
}
// Check file exists in allowed directory
absPath, err := filepath.Abs(path)
if err != nil {
return nil, err
}
if !strings.HasPrefix(absPath, allowedDir) {
return nil, errors.New("access denied")
}
return os.ReadFile(absPath)
}
```
## Rate Limiting
```go
type RateLimiter struct {
requests map[string][]time.Time
mu sync.Mutex
limit int
window time.Duration
}
func (r *RateLimiter) Allow(key string) bool {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
// Clean old requests
var recent []time.Time
for _, t := range r.requests[key] {
if now.Sub(t) < r.window {
recent = append(recent, t)
}
}
if len(recent) >= r.limit {
return false
}
r.requests[key] = append(recent, now)
return true
}
```
## Best Practices
### ✅ Do
- Validate all input
- Use HTTPS for network calls
- Encrypt sensitive data
- Use secure password hashing
- Implement rate limiting
- Keep dependencies updated
- Log security events
- Use OS keychains
### ❌ Don't
- Don't trust user input
- Don't store passwords in plain text
- Don't hardcode secrets
- Don't skip certificate verification
- Don't expose sensitive data in logs
- Don't use weak encryption
- Don't ignore security updates
## Security Checklist
- [ ] All user input validated
- [ ] Passwords hashed with bcrypt
- [ ] Sensitive data encrypted
- [ ] HTTPS used for all network calls
- [ ] Rate limiting implemented
- [ ] File paths validated
- [ ] Dependencies up to date
- [ ] Security logging enabled
- [ ] Error messages don't leak info
- [ ] Code reviewed for vulnerabilities
## Next Steps
- [Architecture](/guides/architecture) - Application architecture patterns
- [Best Practices](/features/bindings/best-practices) - Bindings best practices

Some files were not shown because too many files have changed in this diff Show more