Merge origin/v3-alpha into v3-alpha-feature/android-support
36
.github/workflows/build-and-test-v3.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
567
.github/workflows/nightly-release-v3.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
13
.github/workflows/publish-npm.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
17
.github/workflows/v3-docs.yml
vendored
|
|
@ -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 branch’s 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
|
||||
|
|
@ -1,17 +1,22 @@
|
|||
# Starlight Starter Kit: Basics
|
||||
# Wails v3 Documentation
|
||||
|
||||
[](https://starlight.astro.build)
|
||||
|
||||
```sh
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
World-class documentation for Wails v3, redesigned following Netflix documentation principles.
|
||||
|
||||
[](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
|
||||
[](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
|
||||
[](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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
docs/public/showcase-images/bboard.webp
Normal file
|
After Width: | Height: | Size: 8 KiB |
BIN
docs/public/showcase-images/cfntracker.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/public/showcase-images/edex-ui.webp
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
docs/public/showcase-images/emailit.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/public/showcase-images/encrypteasy.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/public/showcase-images/filehound.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/public/showcase-images/gamestacker.webp
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
docs/public/showcase-images/hiposter.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/public/showcase-images/mchat.webp
Normal file
|
After Width: | Height: | Size: 503 KiB |
BIN
docs/public/showcase-images/minecraft-mod-updater.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/public/showcase-images/minesweeper-xp.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/public/showcase-images/modalfilemanager.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
docs/public/showcase-images/mollywallet.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
docs/public/showcase-images/october.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/public/showcase-images/optimus.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/public/showcase-images/portfall.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/public/showcase-images/resizem.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/public/showcase-images/riftshare-main.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/public/showcase-images/scriptbar.webp
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/public/showcase-images/tiny-rdm1.webp
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
docs/public/showcase-images/tiny-rdm2.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/public/showcase-images/varly2.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/public/showcase-images/wailsterm.webp
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
docs/public/showcase-images/wally.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/public/showcase-images/wombat.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
docs/public/showcase-images/ytd.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/src/assets/notes-app.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/src/assets/todo-app.png
Normal file
|
After Width: | Height: | Size: 558 KiB |
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
-->
|
||||
|
||||

|
||||

|
||||
|
||||
<!-- 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)
|
||||
|
|
|
|||
655
docs/src/content/docs/concepts/architecture.mdx
Normal 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** | <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).
|
||||
700
docs/src/content/docs/concepts/bridge.mdx
Normal 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:** <1ms
|
||||
|
||||
```
|
||||
Frontend Call → Bridge → Go Execution → Bridge → Frontend Response
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
<0.1ms <0.1ms [varies] <0.1ms <0.1ms
|
||||
```
|
||||
|
||||
**Compared to alternatives:**
|
||||
- **HTTP/REST:** 5-50ms (network stack, serialisation)
|
||||
- **IPC:** 1-10ms (process boundaries, marshalling)
|
||||
- **Wails Bridge:** <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** - <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).
|
||||
700
docs/src/content/docs/concepts/build-system.mdx
Normal 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** (<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 | <1s | Go code scanning |
|
||||
| Binding Generation | <1s | TypeScript generation |
|
||||
| Frontend Build | 5-30s | Depends on project size |
|
||||
| Go Compilation | 2-10s | Depends on code size |
|
||||
| Asset Embedding | <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).
|
||||
612
docs/src/content/docs/concepts/lifecycle.mdx
Normal 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 (<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** - <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 (<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).
|
||||
|
|
@ -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";
|
||||
275
docs/src/content/docs/contributing.mdx
Normal 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>
|
||||
|
|
@ -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 framework’s 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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
369
docs/src/content/docs/contributing/getting-started.mdx
Normal 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)
|
||||
|
|
@ -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.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
297
docs/src/content/docs/contributing/setup.mdx
Normal 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)
|
||||
465
docs/src/content/docs/contributing/standards.mdx
Normal 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
|
||||
|
|
@ -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`)
|
||||
|
|
|
|||
260
docs/src/content/docs/faq.mdx
Normal 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).
|
||||
|
|
@ -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";
|
||||
688
docs/src/content/docs/features/bindings/best-practices.mdx
Normal 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).
|
||||
643
docs/src/content/docs/features/bindings/methods.mdx
Normal 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:** <1ms
|
||||
|
||||
```
|
||||
JavaScript → Bridge → Go → Bridge → JavaScript
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
<0.1ms <0.1ms [varies] <0.1ms <0.1ms
|
||||
```
|
||||
|
||||
**Compared to alternatives:**
|
||||
- HTTP/REST: 5-50ms
|
||||
- IPC: 1-10ms
|
||||
- Wails: <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).
|
||||
741
docs/src/content/docs/features/bindings/models.mdx
Normal 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).
|
||||
790
docs/src/content/docs/features/bindings/services.mdx
Normal 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).
|
||||
|
|
@ -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";
|
||||
493
docs/src/content/docs/features/clipboard/basics.mdx
Normal 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).
|
||||
626
docs/src/content/docs/features/dialogs/custom.mdx
Normal 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).
|
||||
585
docs/src/content/docs/features/dialogs/file.mdx
Normal 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).
|
||||
477
docs/src/content/docs/features/dialogs/message.mdx
Normal 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).
|
||||
470
docs/src/content/docs/features/dialogs/overview.mdx
Normal 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).
|
||||
579
docs/src/content/docs/features/events/system.mdx
Normal 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).
|
||||
|
|
@ -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";
|
||||
640
docs/src/content/docs/features/menus/application.mdx
Normal 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).
|
||||
728
docs/src/content/docs/features/menus/context.mdx
Normal 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: {"id":123,"type":"image"}">
|
||||
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: {"id":"file-1","type":"document","name":"Report.pdf"}">
|
||||
📄 Report.pdf
|
||||
</div>
|
||||
|
||||
<!-- Image file -->
|
||||
<div class="file-item"
|
||||
style="--custom-contextmenu: image-menu;
|
||||
--custom-contextmenu-data: {"id":"file-2","type":"image","name":"Photo.jpg"}">
|
||||
🖼️ 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: {"id":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).
|
||||
562
docs/src/content/docs/features/menus/reference.mdx
Normal 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).
|
||||
713
docs/src/content/docs/features/menus/systray.mdx
Normal 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).
|
||||
|
|
@ -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()
|
||||
235
docs/src/content/docs/features/platform/dock.mdx
Normal 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
|
||||
}
|
||||
```
|
||||
466
docs/src/content/docs/features/screens/info.mdx
Normal 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).
|
||||
607
docs/src/content/docs/features/windows/basics.mdx
Normal 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).
|
||||
693
docs/src/content/docs/features/windows/events.mdx
Normal 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).
|
||||
870
docs/src/content/docs/features/windows/frameless.mdx
Normal 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).
|
||||
814
docs/src/content/docs/features/windows/multiple.mdx
Normal 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).
|
||||
852
docs/src/content/docs/features/windows/options.mdx
Normal 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).
|
||||
|
|
@ -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/>
|
||||
|
|
|
|||
199
docs/src/content/docs/guides/architecture.mdx
Normal 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
|
||||
171
docs/src/content/docs/guides/auto-updates.mdx
Normal 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
|
||||
|
|
@ -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";
|
||||
415
docs/src/content/docs/guides/build/windows.mdx
Normal 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).
|
||||
176
docs/src/content/docs/guides/building.mdx
Normal 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
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
title: CLI Reference
|
||||
description: Complete reference for the Wails CLI commands
|
||||
sidebar:
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
|
|
|||
202
docs/src/content/docs/guides/cross-platform.mdx
Normal 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
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
593
docs/src/content/docs/guides/distribution/custom-protocols.mdx
Normal 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).
|
||||
173
docs/src/content/docs/guides/e2e-testing.mdx
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
160
docs/src/content/docs/guides/installers.mdx
Normal 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
|
||||
341
docs/src/content/docs/guides/performance.mdx
Normal 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
|
||||
261
docs/src/content/docs/guides/security.mdx
Normal 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
|
||||