diff --git a/.github/file-labeler.yml b/.github/file-labeler.yml new file mode 100644 index 000000000..69494cbae --- /dev/null +++ b/.github/file-labeler.yml @@ -0,0 +1,44 @@ +# File path specific labels +v2-only: + - 'v2/**/*' + +v3-alpha: + - 'v3/**/*' + +windows: + - '**/*_windows.go' + - 'v2/internal/frontend/desktop/windows/**/*' + +macos: + - '**/*_darwin.go' + - 'v2/internal/frontend/desktop/darwin/**/*' + +linux: + - '**/*_linux.go' + - 'v2/internal/frontend/desktop/linux/**/*' + +cli: + - 'v2/cmd/**/*' + - 'v3/cmd/**/*' + - '**/cli/**/*' + - '**/commands/**/*' + +documentation: + - '**/*.md' + - 'docs/**/*' + - 'website/**/*' + - 'mkdocs-website/**/*' + +templates: + - '**/templates/**/*' + - '**/template/**/*' + +runtime: + - '**/runtime/**/*' + - 'v2/internal/runtime/**/*' + - 'v3/internal/runtime/**/*' + +bindings: + - 'v2/internal/binding/**/*' + - 'v3/internal/generator/**/*' + diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml new file mode 100644 index 000000000..0a7949051 --- /dev/null +++ b/.github/issue-labeler.yml @@ -0,0 +1,144 @@ +# Version labels +v2-only: + - '\[v2\]' + - '\(v2\)' + - 'v2:' + - 'version 2' + - 'wails v2' + - 'using v2' + - 'master branch' + +v3-alpha: + - '\[v3\]' + - '\(v3\)' + - 'v3:' + - '\[v3-alpha\]' + - '\(v3-alpha\)' + - 'version 3' + - 'wails v3' + - 'using v3' + - 'v3-alpha branch' + +# Component labels +webview2: + - 'webview2' + - 'windows' + - 'microsoft edge' + - 'edge browser' + - 'IE' + - 'Explorer' + - 'browser crashes' + +macos: + - 'macOS' + - 'mac OS' + - 'OS X' + - 'darwin' + - 'cocoa' + - 'Safari' + - 'Catalyst' + - 'Ventura' + - 'Sonoma' + - 'apple' + +linux: + - 'linux' + - 'ubuntu' + - 'debian' + - 'fedora' + - 'gtk' + - 'webkitgtk' + - 'webkit2gtk' + - 'gnome' + - 'x11' + - 'wayland' + +cli: + - 'cli' + - 'command line' + - 'wails doctor' + - 'wails init' + - 'wails build' + - 'wails dev' + - 'template' + - 'scaffolding' + +# Type labels +bug: + - 'bug' + - 'crash' + - 'broken' + - 'failure' + - 'error' + - 'failed' + - 'panic' + - 'segfault' + - 'issue' + - 'not working' + - 'problem' + +enhancement: + - 'feature' + - 'enhancement' + - 'request' + - 'add' + - 'new' + - 'improve' + - 'functionality' + - 'support for' + - 'please add' + - 'would be nice' + +documentation: + - 'docs' + - 'documentation' + - 'readme' + - 'example' + - 'tutorial' + - 'guide' + - 'explanation' + - 'clarification' + - 'instructions' + +security: + - 'security' + - 'vulnerability' + - 'exploit' + - 'hack' + - 'CVE' + - 'secure' + - 'encryption' + - 'hardening' + +performance: + - 'performance' + - 'slow' + - 'speed' + - 'memory leak' + - 'cpu usage' + - 'high memory' + - 'lag' + - 'freeze' + - 'optimization' + +# Priority labels +high-priority: + - 'urgent' + - 'critical' + - 'security' + - 'high priority' + - 'important' + - 'production' + - 'blocker' + - 'blocking' + +question: + - 'how to' + - 'how do i' + - 'can I' + - 'is it possible' + - 'question' + - 'help me' + - 'need help' + - 'assistance' + - 'confused' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d73efffa8..9f8d049ba 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,6 +6,7 @@ * YOUR PR MAY BE REJECTED IF IT DOES NOT FOLLOW THESE STEPS * ********************************************************************* +- *DO NOT* submit bugs for a source install of v3, ONLY tagged versions, e.g. v3.0.0-alpha.11 - *DO NOT* submit PRs for v3 alpha enhancements, unless you have opened a post on the discord channel. All enhancements must be discussed first. The feedback guide for v3 is here: https://v3alpha.wails.io/getting-started/feedback/ @@ -47,7 +48,7 @@ Please paste the output of `wails doctor`. If you are unable to run this command # Checklist: -- [ ] I have updated `website/src/pages/changelog.mdx` with details of this PR +- [ ] I have updated `v3/UNRELEASED_CHANGELOG.md` with details of this PR - [ ] My code follows the general coding style of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas diff --git a/.github/stale.yml b/.github/stale.yml index 805bd589d..d8bcc83ec 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,7 +1,7 @@ # Number of days of inactivity before an issue becomes stale -daysUntilStale: 30 +daysUntilStale: 45 # Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 +daysUntilClose: 10 # Issues with these labels will never be considered stale exemptLabels: - pinned @@ -9,14 +9,28 @@ exemptLabels: - onhold - inprogress - "Selected For Development" + - bug + - enhancement + - v3-alpha + - high-priority # Label to use when marking an issue as stale -staleLabel: "Wont Fix" +staleLabel: "stale" # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. + recent activity. It will be closed if no further activity occurs within the next 10 days. + + If this issue is still relevant, please add a comment to keep it open. + Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false +closeComment: > + This issue has been automatically closed due to lack of activity. + Please feel free to reopen it if it's still relevant. exemptMilestones: true exemptAssignees: true +# Only mark issues (not PRs) +only: issues +# Exempt issues created before a certain date +exemptCreatedBefore: "2024-01-01T00:00:00Z" +# Starts checking issues only after the specified date +startDate: "2025-06-01T00:00:00Z" diff --git a/.github/workflows/auto-label-issues.yml b/.github/workflows/auto-label-issues.yml new file mode 100644 index 000000000..097eba533 --- /dev/null +++ b/.github/workflows/auto-label-issues.yml @@ -0,0 +1,33 @@ +name: Auto Label Issues + +on: + issues: + types: [opened, edited, reopened] + pull_request_target: + types: [opened, edited, reopened, synchronize] + +jobs: + auto-label: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Label issues and PRs by content + uses: github/issue-labeler@v3.4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/issue-labeler.yml + enable-versioned-regex: 0 + include-title: 1 + + - name: Label issues and PRs by file paths + uses: actions/labeler@v4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/file-labeler.yml + sync-labels: true diff --git a/.github/workflows/automated-releases.yml b/.github/workflows/automated-releases.yml new file mode 100644 index 000000000..e88f54eb9 --- /dev/null +++ b/.github/workflows/automated-releases.yml @@ -0,0 +1,372 @@ +name: Automated Nightly Releases + +on: + workflow_dispatch: + inputs: + force_release: + description: 'Force release even if no changes detected' + required: false + default: false + type: boolean + dry_run: + description: 'Run in dry-run mode (no actual releases)' + required: false + default: false + type: boolean + # schedule: + # Run at 2 AM UTC every day - DISABLED for safety until ready + # - cron: '0 2 * * *' + +env: + GO_VERSION: '1.24' + +jobs: + check-permissions: + name: Check Release Permissions + runs-on: ubuntu-latest + outputs: + authorized: ${{ steps.check.outputs.authorized }} + steps: + - name: Check if user is authorized for releases + id: check + run: | + # Only allow specific users to trigger releases + AUTHORIZED_USERS="leaanthony" + + if [[ "$AUTHORIZED_USERS" == *"${{ github.actor }}"* ]]; then + echo "βœ… User ${{ github.actor }} is authorized for releases" + echo "authorized=true" >> $GITHUB_OUTPUT + else + echo "❌ User ${{ github.actor }} is not authorized for releases" + echo "authorized=false" >> $GITHUB_OUTPUT + fi + + detect-v2-changes: + name: Detect v2 Changes + runs-on: ubuntu-latest + needs: check-permissions + if: needs.check-permissions.outputs.authorized == 'true' + outputs: + has_changes: ${{ steps.changes.outputs.has_changes }} + commits_since_last: ${{ steps.changes.outputs.commits_since_last }} + last_release_tag: ${{ steps.changes.outputs.last_release_tag }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check for v2 changes since last release + id: changes + run: | + echo "πŸ” Checking for v2 changes since last release..." + + # Find the last v2 release tag + LAST_TAG=$(git tag -l "v2.*" --sort=-version:refname | head -n 1) + if [ -z "$LAST_TAG" ]; then + echo "No previous v2 tags found, assuming first release" + LAST_TAG=$(git rev-list --max-parents=0 HEAD) + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "commits_since_last=999" >> $GITHUB_OUTPUT + echo "last_release_tag=none" >> $GITHUB_OUTPUT + else + echo "Last v2 release tag: $LAST_TAG" + echo "last_release_tag=$LAST_TAG" >> $GITHUB_OUTPUT + + # Count commits since last release affecting v2 or root files + COMMITS_COUNT=$(git rev-list --count ${LAST_TAG}..HEAD -- v2/ website/ README.md CHANGELOG.md || echo "0") + echo "Commits since last v2 release: $COMMITS_COUNT" + echo "commits_since_last=$COMMITS_COUNT" >> $GITHUB_OUTPUT + + if [ "$COMMITS_COUNT" -gt 0 ] || [ "${{ github.event.inputs.force_release }}" == "true" ]; then + echo "βœ… Changes detected or forced release" + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "ℹ️ No changes detected since last release" + echo "has_changes=false" >> $GITHUB_OUTPUT + fi + fi + + detect-v3-changes: + name: Detect v3-alpha Changes + runs-on: ubuntu-latest + needs: check-permissions + if: needs.check-permissions.outputs.authorized == 'true' + outputs: + has_changes: ${{ steps.changes.outputs.has_changes }} + commits_since_last: ${{ steps.changes.outputs.commits_since_last }} + last_release_tag: ${{ steps.changes.outputs.last_release_tag }} + steps: + - name: Checkout v3-alpha branch + uses: actions/checkout@v4 + with: + ref: v3-alpha + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check for v3-alpha changes since last release + id: changes + run: | + echo "πŸ” Checking for v3-alpha changes since last release..." + + # Find the last v3 alpha release tag + LAST_TAG=$(git tag -l "v3.*-alpha.*" --sort=-version:refname | head -n 1) + if [ -z "$LAST_TAG" ]; then + echo "No previous v3-alpha tags found, assuming first release" + LAST_TAG=$(git rev-list --max-parents=0 HEAD) + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "commits_since_last=999" >> $GITHUB_OUTPUT + echo "last_release_tag=none" >> $GITHUB_OUTPUT + else + echo "Last v3-alpha release tag: $LAST_TAG" + echo "last_release_tag=$LAST_TAG" >> $GITHUB_OUTPUT + + # Count commits since last release affecting v3 or docs + COMMITS_COUNT=$(git rev-list --count ${LAST_TAG}..HEAD -- v3/ docs/ || echo "0") + echo "Commits since last v3-alpha release: $COMMITS_COUNT" + echo "commits_since_last=$COMMITS_COUNT" >> $GITHUB_OUTPUT + + if [ "$COMMITS_COUNT" -gt 0 ] || [ "${{ github.event.inputs.force_release }}" == "true" ]; then + echo "βœ… Changes detected or forced release" + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "ℹ️ No changes detected since last release" + echo "has_changes=false" >> $GITHUB_OUTPUT + fi + fi + + release-v2: + name: Create v2 Release + runs-on: ubuntu-latest + needs: [check-permissions, detect-v2-changes] + if: | + needs.check-permissions.outputs.authorized == 'true' && + needs.detect-v2-changes.outputs.has_changes == 'true' + outputs: + version: ${{ steps.release.outputs.version }} + release_notes: ${{ steps.release.outputs.release_notes }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Run v2 release script and extract notes + id: release + run: | + echo "πŸš€ Running v2 release script..." + cd v2/tools/release + + # Run release script and capture output + RELEASE_OUTPUT=$(go run release.go 2>&1) + echo "$RELEASE_OUTPUT" + + # Extract version from output or version file + NEW_VERSION=$(cat ../../cmd/wails/internal/version.txt) + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + + # Extract release notes from delimited output + RELEASE_NOTES=$(echo "$RELEASE_OUTPUT" | sed -n '/=== RELEASE NOTES FOR/,/=== END RELEASE NOTES ===/p' | sed '1d;$d') + + # Save release notes to file for multiline output + echo "$RELEASE_NOTES" > ../../../release_notes_v2.md + + # Set output (escape for GitHub Actions) + { + echo "release_notes<> $GITHUB_OUTPUT + + echo "βœ… v2 release script completed - version: $NEW_VERSION" + + - name: Create v2 git tag and release + if: github.event.inputs.dry_run != 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.release.outputs.version }}" + echo "πŸ“ Creating v2 release: $VERSION" + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # Commit the changelog changes + git add website/src/pages/changelog.mdx v2/cmd/wails/internal/version.txt + git commit -m "chore: release $VERSION + + Automated release created by GitHub Actions + + πŸ€– Generated with [Claude Code](https://claude.ai/code) + + Co-Authored-By: Claude " + + # Create and push tag + git tag -a "$VERSION" -m "Release $VERSION" + git push origin master + git push origin "$VERSION" + + # Create GitHub release with notes + gh release create "$VERSION" \ + --title "Release $VERSION" \ + --notes-file release_notes_v2.md \ + --target master + + - name: Log dry-run results for v2 + if: github.event.inputs.dry_run == 'true' + run: | + echo "πŸ§ͺ DRY RUN - Would have created v2 release:" + echo "Version: ${{ steps.release.outputs.version }}" + echo "Release Notes:" + cat release_notes_v2.md + + release-v3: + name: Create v3-alpha Release + runs-on: ubuntu-latest + needs: [check-permissions, detect-v3-changes] + if: | + needs.check-permissions.outputs.authorized == 'true' && + needs.detect-v3-changes.outputs.has_changes == 'true' + outputs: + version: ${{ steps.release.outputs.version }} + release_notes: ${{ steps.release.outputs.release_notes }} + steps: + - name: Checkout v3-alpha branch + uses: actions/checkout@v4 + with: + ref: v3-alpha + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Run v3 release script and extract notes + id: release + run: | + echo "πŸš€ Running v3-alpha release script..." + cd v3/tasks/release + + # Run release script and capture output + RELEASE_OUTPUT=$(go run release.go 2>&1) + echo "$RELEASE_OUTPUT" + + # Extract version from output or version file + NEW_VERSION=$(cat ../../internal/version/version.txt) + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + + # Extract release notes from delimited output + RELEASE_NOTES=$(echo "$RELEASE_OUTPUT" | sed -n '/=== RELEASE NOTES FOR/,/=== END RELEASE NOTES ===/p' | sed '1d;$d') + + # Save release notes to file for multiline output + echo "$RELEASE_NOTES" > ../../../release_notes_v3.md + + # Set output (escape for GitHub Actions) + { + echo "release_notes<> $GITHUB_OUTPUT + + echo "βœ… v3-alpha release script completed - version: $NEW_VERSION" + + - name: Create v3-alpha git tag and release + if: github.event.inputs.dry_run != 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.release.outputs.version }}" + echo "πŸ“ Creating v3-alpha release: $VERSION" + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # Commit the changelog changes + git add docs/src/content/docs/changelog.mdx v3/internal/version/version.txt + git commit -m "chore: release $VERSION + + Automated v3-alpha release created by GitHub Actions + + πŸ€– Generated with [Claude Code](https://claude.ai/code) + + Co-Authored-By: Claude " + + # Create and push tag + git tag -a "$VERSION" -m "Release $VERSION" + git push origin v3-alpha + git push origin "$VERSION" + + # Create GitHub release with notes + gh release create "$VERSION" \ + --title "Release $VERSION" \ + --notes-file release_notes_v3.md \ + --target v3-alpha \ + --prerelease + + - name: Log dry-run results for v3-alpha + if: github.event.inputs.dry_run == 'true' + run: | + echo "πŸ§ͺ DRY RUN - Would have created v3-alpha release:" + echo "Version: ${{ steps.release.outputs.version }}" + echo "Release Notes:" + cat release_notes_v3.md + + summary: + name: Release Summary + runs-on: ubuntu-latest + needs: [check-permissions, detect-v2-changes, detect-v3-changes, release-v2, release-v3] + if: always() && needs.check-permissions.outputs.authorized == 'true' + steps: + - name: Create release summary + run: | + echo "# πŸš€ Automated Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Repository**: ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY + echo "**Triggered by**: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY + echo "**Dry Run Mode**: ${{ github.event.inputs.dry_run || 'false' }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # v2 Summary + echo "## v2 Release" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.detect-v2-changes.outputs.has_changes }}" == "true" ]; then + if [ "${{ needs.release-v2.result }}" == "success" ]; then + echo "βœ… **v2 Release**: Created successfully" >> $GITHUB_STEP_SUMMARY + echo " - Version: ${{ needs.release-v2.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo " - Commits since last: ${{ needs.detect-v2-changes.outputs.commits_since_last }}" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **v2 Release**: Failed" >> $GITHUB_STEP_SUMMARY + fi + else + echo "⏭️ **v2 Release**: Skipped (no changes)" >> $GITHUB_STEP_SUMMARY + echo " - Commits since last: ${{ needs.detect-v2-changes.outputs.commits_since_last }}" >> $GITHUB_STEP_SUMMARY + fi + + # v3 Summary + echo "## v3-alpha Release" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.detect-v3-changes.outputs.has_changes }}" == "true" ]; then + if [ "${{ needs.release-v3.result }}" == "success" ]; then + echo "βœ… **v3-alpha Release**: Created successfully" >> $GITHUB_STEP_SUMMARY + echo " - Version: ${{ needs.release-v3.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo " - Commits since last: ${{ needs.detect-v3-changes.outputs.commits_since_last }}" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **v3-alpha Release**: Failed" >> $GITHUB_STEP_SUMMARY + fi + else + echo "⏭️ **v3-alpha Release**: Skipped (no changes)" >> $GITHUB_STEP_SUMMARY + echo " - Commits since last: ${{ needs.detect-v3-changes.outputs.commits_since_last }}" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "πŸ€– **Automated Release System** | Generated with [Claude Code](https://claude.ai/code)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/build-and-test-v3.yml b/.github/workflows/build-and-test-v3.yml index 88f576104..a1b482e11 100644 --- a/.github/workflows/build-and-test-v3.yml +++ b/.github/workflows/build-and-test-v3.yml @@ -3,13 +3,20 @@ name: Build + Test v3 on: pull_request: types: [opened, synchronize, reopened, ready_for_review] + branches: + - v3-alpha + paths: + - 'v3/**' pull_request_review: types: [submitted] + branches: + - v3-alpha jobs: check_approval: name: Check PR Approval runs-on: ubuntu-latest + if: github.base_ref == 'v3-alpha' outputs: approved: ${{ steps.check.outputs.approved }} steps: @@ -22,59 +29,11 @@ jobs: echo "approved=false" >> $GITHUB_OUTPUT fi - test_go: - name: Run Go Tests - needs: check_approval - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [windows-latest, macos-latest, ubuntu-latest] - go-version: [1.23] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install linux dependencies - uses: awalsh128/cache-apt-pkgs-action@latest - if: matrix.os == 'ubuntu-latest' - with: - packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config - version: 1.0 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - cache-dependency-path: "v3/go.sum" - - - name: Install Task - uses: arduino/setup-task@v2 - with: - version: 3.x - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Examples - working-directory: ./v3 - run: task test:examples - - - name: Run tests (mac) - if: matrix.os == 'macos-latest' - env: - CGO_LDFLAGS: -framework UniformTypeIdentifiers -mmacosx-version-min=10.13 - working-directory: ./v3 - run: go test -v ./... - - - name: Run tests (!mac) - if: matrix.os != 'macos-latest' - working-directory: ./v3 - run: go test -v ./... - test_js: name: Run JS Tests needs: check_approval runs-on: ubuntu-latest + if: github.base_ref == 'v3-alpha' strategy: matrix: node-version: [20.x] @@ -88,57 +47,158 @@ jobs: with: node-version: ${{ matrix.node-version }} + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install dependencies - run: npm install - working-directory: v2/internal/frontend/runtime + working-directory: v3/internal/runtime/desktop/@wailsio/runtime + run: | + npm ci + npx --yes esbuild@latest --version - - name: Run tests - run: npm test - working-directory: v2/internal/frontend/runtime + - name: Clean build artifacts + working-directory: v3/internal/runtime/desktop/@wailsio/runtime + run: npm run clean - test_templates: - name: Test Templates - needs: test_go + - name: Type-check runtime + working-directory: v3 + run: task runtime:check + + - name: Test runtime + working-directory: v3 + run: task runtime:test + + - name: Check that the bundled runtime builds + working-directory: v3 + run: task runtime:build + + - name: Check that the npm package builds + working-directory: v3/internal/runtime/desktop/@wailsio/runtime + run: npm run build + + - name: Store runtime build artifacts + uses: actions/upload-artifact@v4 + with: + name: runtime-build-artifacts + path: | + v3/internal/runtime/desktop/@wailsio/runtime/dist/ + v3/internal/runtime/desktop/@wailsio/runtime/types/ + v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.tsbuildinfo + + test_go: + name: Run Go Tests v3 + needs: [check_approval, test_js] runs-on: ${{ matrix.os }} + if: github.base_ref == 'v3-alpha' strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - template: - [ - svelte, - svelte-ts, - vue, - vue-ts, - react, - react-ts, - preact, - preact-ts, - lit, - lit-ts, - vanilla, - vanilla-ts, - ] - go-version: [1.23] + os: [windows-latest, ubuntu-latest, macos-latest] + go-version: [1.24] + steps: - - name: Checkout + - name: Checkout code uses: actions/checkout@v4 + - name: Install linux dependencies + uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-latest' + with: + packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config xvfb x11-xserver-utils at-spi2-core xdg-desktop-portal-gtk + version: 1.0 + - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} + cache: true cache-dependency-path: "v3/go.sum" - - name: Setup Golang caches - uses: actions/cache@v4 + - name: Install Task + uses: arduino/setup-task@v2 with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-golang- + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Retrieve runtime build artifacts + uses: actions/download-artifact@v4 + with: + name: runtime-build-artifacts + path: v3/internal/runtime/desktop/@wailsio/runtime/ + + - name: Build Examples + working-directory: v3 + run: | + echo "Starting example compilation tests..." + task test:examples + echo "Example compilation tests completed successfully" + + - name: Run tests (mac) + if: matrix.os == 'macos-latest' + env: + CGO_LDFLAGS: -framework UniformTypeIdentifiers -mmacosx-version-min=10.13 + working-directory: v3 + run: go test -v ./... + + - name: Run tests (windows) + if: matrix.os == 'windows-latest' + working-directory: v3 + run: go test -v ./... + + - name: Run tests (ubuntu) + if: matrix.os == 'ubuntu-latest' + working-directory: v3 + run: > + xvfb-run --auto-servernum + sh -c ' + dbus-update-activation-environment --systemd --all && + go test -v ./... + ' + + - name: Typecheck binding generator output + working-directory: v3 + run: task generator:test:check + + cleanup: + name: Cleanup build artifacts + if: always() + needs: [test_js, test_go] + runs-on: ubuntu-latest + steps: + - uses: geekyeggo/delete-artifact@v5 + with: + name: runtime-build-artifacts + failOnError: false + + test_templates: + name: Test Templates + needs: test_go + runs-on: ${{ matrix.os }} + if: github.base_ref == 'v3-alpha' + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + template: + - svelte + - svelte-ts + - vue + - vue-ts + - react + - react-ts + - preact + - preact-ts + - lit + - lit-ts + - vanilla + - vanilla-ts + go-version: [1.24] + steps: + - name: Checkout + uses: actions/checkout@v4 - name: Install linux dependencies uses: awalsh128/cache-apt-pkgs-action@latest @@ -147,17 +207,50 @@ jobs: packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config version: 1.0 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: "v3/go.sum" + + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Build Wails3 CLI + working-directory: v3 run: | - cd ./v3/cmd/wails3 - go install - wails3 -help + task install + wails3 doctor - name: Generate template '${{ matrix.template }}' run: | - go install github.com/go-task/task/v3/cmd/task@latest mkdir -p ./test-${{ matrix.template }} cd ./test-${{ matrix.template }} wails3 init -n ${{ matrix.template }} -t ${{ matrix.template }} cd ${{ matrix.template }} - wails3 build \ No newline at end of file + wails3 build + + build_results: + if: ${{ always() }} + runs-on: ubuntu-latest + name: v3 Build Results + needs: [test_go, test_js, test_templates] + steps: + - run: | + go_result="${{ needs.test_go.result }}" + js_result="${{ needs.test_js.result }}" + templates_result="${{ needs.test_templates.result }}" + + if [[ $go_result == "success" || $go_result == "skipped" ]] && \ + [[ $js_result == "success" || $js_result == "skipped" ]] && \ + [[ $templates_result == "success" || $templates_result == "skipped" ]]; then + echo "All required jobs succeeded or were skipped" + exit 0 + else + echo "One or more required jobs failed" + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/changelog-validation-v3.yml b/.github/workflows/changelog-validation-v3.yml new file mode 100644 index 000000000..8d4b97726 --- /dev/null +++ b/.github/workflows/changelog-validation-v3.yml @@ -0,0 +1,74 @@ +name: Changelog Validation (v3) + +on: + pull_request: + branches: [ v3-alpha ] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to validate (for manual testing)' + required: true + type: string + +jobs: + validate-changelog: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' || github.event.inputs.pr_number + + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || format('refs/pull/{0}/head', github.event.inputs.pr_number) }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' + + - name: Get PR information + id: pr_info + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "pr_number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT + echo "base_ref=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT + else + echo "pr_number=${{ github.event.inputs.pr_number }}" >> $GITHUB_OUTPUT + echo "base_ref=v3-alpha" >> $GITHUB_OUTPUT + fi + + - name: Check if changelog was modified + id: changelog_check + run: | + git fetch origin ${{ steps.pr_info.outputs.base_ref }} + if git diff --name-only origin/${{ steps.pr_info.outputs.base_ref }}..HEAD | grep -q "v3/UNRELEASED_CHANGELOG.md"; then + echo "changelog_modified=true" >> $GITHUB_OUTPUT + echo "βœ… UNRELEASED_CHANGELOG.md was modified in this PR" + else + echo "changelog_modified=false" >> $GITHUB_OUTPUT + echo "⚠️ UNRELEASED_CHANGELOG.md was not modified" + fi + + - name: Comment on PR about missing changelog + if: steps.changelog_check.outputs.changelog_modified == 'false' && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const author = context.payload.pull_request.user.login; + const message = '## ⚠️ Missing Changelog Update\n\n' + + `Hi @${author}, please update \`v3/UNRELEASED_CHANGELOG.md\` with a description of your changes.\n\n` + + 'This helps us keep track of changes for the next release.'; + + await github.rest.issues.createComment({ + issue_number: ${{ steps.pr_info.outputs.pr_number }}, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }); + diff --git a/.github/workflows/generate-sponsor-image.yml b/.github/workflows/generate-sponsor-image.yml index b8bb0c4ac..585d7e19f 100644 --- a/.github/workflows/generate-sponsor-image.yml +++ b/.github/workflows/generate-sponsor-image.yml @@ -29,7 +29,12 @@ jobs: with: commit-message: "chore: update sponsors.svg" add-paths: "website/static/img/sponsors.svg" - title: Update Sponsor Image - body: Generated new image + title: "chore: update sponsors.svg" + body: | + Auto-generated by the sponsor image workflow + + [skip ci] [skip actions] branch: update-sponsors + base: master delete-branch: true + draft: false diff --git a/.github/workflows/issue-triage-automation.yml b/.github/workflows/issue-triage-automation.yml new file mode 100644 index 000000000..4a827d527 --- /dev/null +++ b/.github/workflows/issue-triage-automation.yml @@ -0,0 +1,77 @@ +name: Issue Triage Automation + +on: + issues: + types: [opened, reopened, labeled, unlabeled] + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + issues: write + contents: read + steps: + # Request more info for unclear bug reports + - name: Request more info + uses: actions/github-script@v6 + if: | + contains(github.event.issue.labels.*.name, 'bug') && + !contains(github.event.issue.body, 'wails doctor') && + !contains(github.event.issue.body, 'reproduction') + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `πŸ‘‹ Thanks for reporting this issue! To help us investigate, could you please: + + 1. Add the output of \`wails doctor\` if not already included + 2. Provide clear steps to reproduce the issue + 3. If possible, create a minimal reproduction of the issue + + This will help us resolve your issue much faster. Thank you!` + }); + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['awaiting feedback'] + }); + + # Prioritize security issues + - name: Prioritize security issues + uses: actions/github-script@v6 + if: contains(github.event.issue.labels.*.name, 'security') + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['high-priority'] + }); + + # Tag version-specific issues for project boards + - name: Add to v2 project + uses: actions/github-script@v6 + if: | + contains(github.event.issue.labels.*.name, 'v2-only') && + !contains(github.event.issue.labels.*.name, 'v3-alpha') + with: + script: | + // Replace PROJECT_ID with your actual GitHub project ID + // This is a placeholder as the actual implementation would require + // GraphQL API calls to add to a project board + console.log('Would add to v2 project board'); + + # Tag version-specific issues for project boards + - name: Add to v3 project + uses: actions/github-script@v6 + if: contains(github.event.issue.labels.*.name, 'v3-alpha') + with: + script: | + // Replace PROJECT_ID with your actual GitHub project ID + // This is a placeholder as the actual implementation would require + // GraphQL API calls to add to a project board + console.log('Would add to v3 project board'); diff --git a/.github/workflows/nightly-release-v3.yml b/.github/workflows/nightly-release-v3.yml new file mode 100644 index 000000000..ce1c512ed --- /dev/null +++ b/.github/workflows/nightly-release-v3.yml @@ -0,0 +1,723 @@ +name: Nightly Release v3-alpha + +on: + schedule: + - cron: '0 2 * * *' # 2 AM UTC daily + workflow_dispatch: + inputs: + force_release: + description: 'Force release even if no changes detected' + required: false + default: false + type: boolean + dry_run: + description: 'Run in dry-run mode (no actual release)' + required: false + default: true + type: boolean + +jobs: + nightly-release: + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: read + actions: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: v3-alpha + fetch-depth: 0 + token: ${{ secrets.WAILS_REPO_TOKEN || github.token }} + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.24' + cache: true + cache-dependency-path: 'v3/go.sum' + + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + # Configure git to use the token for authentication + git config --global url."https://x-access-token:${{ secrets.WAILS_REPO_TOKEN || github.token }}@github.com/".insteadOf "https://github.com/" + + - name: Check for existing release tag + id: check_tag + run: | + if git describe --tags --exact-match HEAD 2>/dev/null; then + echo "has_tag=true" >> $GITHUB_OUTPUT + echo "tag=$(git describe --tags --exact-match HEAD)" >> $GITHUB_OUTPUT + else + echo "has_tag=false" >> $GITHUB_OUTPUT + echo "tag=" >> $GITHUB_OUTPUT + fi + + - name: Check for unreleased changelog content + id: changelog_check + run: | + echo "πŸ” Checking UNRELEASED_CHANGELOG.md for content..." + + # Run the release script in check mode to see if there's content + cd v3/tasks/release + + # Use the release script itself to check for content + if go run release.go --check-only 2>/dev/null; then + echo "has_unreleased_content=true" >> $GITHUB_OUTPUT + echo "βœ… Found unreleased changelog content" + else + echo "has_unreleased_content=false" >> $GITHUB_OUTPUT + echo "ℹ️ No unreleased changelog content found" + fi + + - name: Quick change detection and early exit + id: quick_check + run: | + echo "πŸ” Quick check for changes to determine if we should continue..." + + # First check if we have unreleased changelog content + if [ "${{ steps.changelog_check.outputs.has_unreleased_content }}" == "true" ]; then + echo "βœ… Found unreleased changelog content, proceeding with release" + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "should_continue=true" >> $GITHUB_OUTPUT + echo "reason=Found unreleased changelog content" >> $GITHUB_OUTPUT + exit 0 + fi + + # If no unreleased changelog content, check for git changes as fallback + echo "No unreleased changelog content found, checking for git changes..." + + # Check if current commit has a release tag + if git describe --tags --exact-match HEAD 2>/dev/null; then + CURRENT_TAG=$(git describe --tags --exact-match HEAD) + echo "Current commit has release tag: $CURRENT_TAG" + + # For tagged commits, check if there are changes since the tag + COMMIT_COUNT=$(git rev-list ${CURRENT_TAG}..HEAD --count) + if [ "$COMMIT_COUNT" -eq 0 ]; then + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "should_continue=false" >> $GITHUB_OUTPUT + echo "reason=No changes since existing tag $CURRENT_TAG and no unreleased changelog content" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "should_continue=true" >> $GITHUB_OUTPUT + fi + else + # No current tag, check against latest release + LATEST_TAG=$(git tag --list "v3.0.0-alpha.*" | sort -V | tail -1) + if [ -z "$LATEST_TAG" ]; then + echo "No previous release found, proceeding with release" + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "should_continue=true" >> $GITHUB_OUTPUT + else + COMMIT_COUNT=$(git rev-list ${LATEST_TAG}..HEAD --count) + if [ "$COMMIT_COUNT" -gt 0 ]; then + echo "Found $COMMIT_COUNT commits since $LATEST_TAG" + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "should_continue=true" >> $GITHUB_OUTPUT + else + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "should_continue=false" >> $GITHUB_OUTPUT + echo "reason=No changes since latest release $LATEST_TAG and no unreleased changelog content" >> $GITHUB_OUTPUT + fi + fi + fi + + - name: Early exit - No changes detected + if: | + steps.quick_check.outputs.should_continue == 'false' && + github.event.inputs.force_release != 'true' + run: | + echo "πŸ›‘ EARLY EXIT: ${{ steps.quick_check.outputs.reason }}" + echo "" + echo "ℹ️ No changes detected since last release and force_release is not enabled." + echo " Workflow will exit early to save resources." + echo "" + echo " To force a release anyway, run this workflow with 'force_release=true'" + echo "" + echo "## πŸ›‘ Early Exit Summary" >> $GITHUB_STEP_SUMMARY + echo "**Reason:** ${{ steps.quick_check.outputs.reason }}" >> $GITHUB_STEP_SUMMARY + echo "**Action:** Workflow exited early to save resources" >> $GITHUB_STEP_SUMMARY + echo "**Force Release:** Set 'force_release=true' to override this behavior" >> $GITHUB_STEP_SUMMARY + exit 0 + + - name: Continue with release process + if: | + steps.quick_check.outputs.should_continue == 'true' || + github.event.inputs.force_release == 'true' + run: | + echo "βœ… Proceeding with release process..." + if [ "${{ github.event.inputs.force_release }}" == "true" ]; then + echo "πŸ”¨ FORCE RELEASE: Overriding change detection" + fi + + - name: Run release script + id: release + if: | + steps.quick_check.outputs.should_continue == 'true' || + github.event.inputs.force_release == 'true' + 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 + 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<> $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: 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' + 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<> $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: 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<> $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: 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' + 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<> $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' + 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' + 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<> $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<> $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" + fi + + - name: Summary + if: always() + run: | + if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then + echo "## πŸ§ͺ DRY RUN Release Test 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 + else + echo "- **Mode:** πŸš€ Live release" >> $GITHUB_STEP_SUMMARY + echo "- **Status:** βœ… Release created successfully" >> $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 + 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 \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr-master.yml similarity index 66% rename from .github/workflows/pr.yml rename to .github/workflows/pr-master.yml index c70050276..1f9fe5622 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr-master.yml @@ -1,20 +1,23 @@ -name: PR Checks +name: PR Checks (master) on: pull_request: + branches: + - master pull_request_review: types: [submitted] - + branches: + - master jobs: check_docs: name: Check Docs - if: ${{github.repository == 'wailsapp/wails' && contains(github.head_ref,'feature/')}} + if: ${{github.repository == 'wailsapp/wails' && github.base_ref == 'master'}} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Verify Changed files - uses: tj-actions/verify-changed-files@v17 + uses: step-security/changed-files@3dbe17c78367e7d60f00d78ae6781a35be47b4a1 # v45.0.1 id: verify-changed-files with: files: | @@ -26,47 +29,28 @@ jobs: run: | echo "::warning::Feature branch does not contain any changes to the website." -# lint_go: -# name: Run Go Linters -# runs-on: ubuntu-latest -# steps: -# - name: Checkout code -# uses: actions/checkout@v4 -# -# - name: Setup Go -# uses: actions/setup-go@v4 -# with: -# go-version: "1.21" -# -# - name: Update go modules -# working-directory: ./v2 -# run: go mod tidy -# -# - name: Run Linter -# uses: golangci/golangci-lint-action@v3 -# with: -# version: v1.54 -# working-directory: ./v2 -# args: --timeout=10m0s --config ./.golangci.yml - test_go: name: Run Go Tests runs-on: ${{ matrix.os }} - if: github.event.review.state == 'approved' + if: > + github.event.review.state == 'approved' && + github.repository == 'wailsapp/wails' && + github.base_ref == 'master' && + github.event.pull_request.head.ref != 'update-sponsors' strategy: matrix: os: [ubuntu-22.04, windows-latest, macos-latest, ubuntu-24.04] - go-version: ['1.21'] + go-version: ['1.23'] steps: - name: Checkout code uses: actions/checkout@v3 - - name: Install linux dependencies ( 22.04 ) + - name: Install linux dependencies (22.04) if: matrix.os == 'ubuntu-22.04' run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config - - name: Install linux dependencies ( 24.04 ) + - name: Install linux dependencies (24.04) if: matrix.os == 'ubuntu-24.04' run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 78d932f91..88517c46c 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -1,63 +1,114 @@ on: push: branches: ['v3-alpha'] + workflow_dispatch: + +concurrency: + group: publish-npm-v3 + cancel-in-progress: true jobs: - publish: + detect: + name: Detect committed changes + if: github.event_name != 'workflow_dispatch' + outputs: + changed: ${{ steps.package-json-changes.outputs.any_modified == 'true' || steps.source-changes.outputs.any_modified == 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + persist-credentials: 'true' + + - name: Detect committed package.json changes + id: package-json-changes + uses: step-security/changed-files@3dbe17c78367e7d60f00d78ae6781a35be47b4a1 # v45.0.1 + with: + files: | + v3/internal/runtime/desktop/@wailsio/runtime/package.json + + - name: Detect committed source changes + if: >- + steps.package-json-changes.outputs.any_modified != 'true' + id: source-changes + uses: step-security/changed-files@3dbe17c78367e7d60f00d78ae6781a35be47b4a1 # v45.0.1 + with: + files: | + v3/internal/runtime/Taskfile.yaml + v3/internal/runtime/desktop/@wailsio/compiled/main.js + v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.json + v3/internal/runtime/desktop/@wailsio/runtime/src/** + v3/pkg/events/events.txt + v3/tasks/events/** + + rebuild_and_publish: + name: Rebuild and publish + needs: [detect] + if: >- + !failure() && !cancelled() + && (github.event_name == 'workflow_dispatch' || needs.detect.outputs.changed == 'true') + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 with: fetch-depth: 0 ref: 'v3-alpha' ssh-key: ${{ secrets.DEPLOY_KEY }} - - name: Configure git run: | git config --local user.email "github-actions@github.com" git config --local user.name "GitHub Actions" - - name: Setup go-task - uses: pnorton5432/setup-task@v1 + - name: Install Task + uses: arduino/setup-task@v2 with: - task-version: 3.29.1 + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-node@v3 + - name: Use Node.js 20 + uses: actions/setup-node@v4 with: node-version: "20" - - run: | - npm ci - npm run build:types - npm run build:docs + + - name: Install dependencies working-directory: v3/internal/runtime/desktop/@wailsio/runtime + run: | + npm ci + npx --yes esbuild@latest --version - - name: Verify Changed files - uses: tj-actions/verify-changed-files@v20 - id: verify-changed-files - with: - files: | - v3/internal/runtime/desktop/@wailsio/runtime/src/*.js - v3/internal/runtime/desktop/@wailsio/runtime/types/*.d.ts - v3/internal/runtime/desktop/@wailsio/runtime/docs/*.* + - name: Clean build artifacts + working-directory: v3/internal/runtime/desktop/@wailsio/runtime + run: npm run clean - - name: test action - if: steps.verify-changed-files.outputs.files_changed == 'true' - id: get-version - uses: beaconbrigade/package-json-version@v0.3.2 - with: - path: v3/internal/runtime/desktop/@wailsio/runtime + - name: Build bundled runtime + working-directory: v3 + run: task runtime:build + + - name: Test+Build npm package + working-directory: v3/internal/runtime/desktop/@wailsio/runtime + run: | + npm test + npm run build + + - name: Bump version + id: bump-version + working-directory: v3/internal/runtime/desktop/@wailsio/runtime + run: | + echo "version=$(npm --no-git-tag-version --force version prerelease)" >> "$GITHUB_OUTPUT" - name: Commit changes - if: steps.verify-changed-files.outputs.files_changed == 'true' run: | git add . - git commit -m "[skip ci] Publish @wailsio/runtime ${{ steps.get-version.outputs.version }}" + git commit -m "[skip ci] Publish @wailsio/runtime ${{ steps.bump-version.outputs.version }}" git push + fi - - uses: JS-DevTools/npm-publish@v3 - if: steps.verify-changed-files.outputs.files_changed == 'true' + - name: Publish npm package + uses: JS-DevTools/npm-publish@v3 with: package: v3/internal/runtime/desktop/@wailsio/runtime access: public - token: ${{ secrets.NPM_TOKEN }} \ No newline at end of file + token: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 453e4cb85..a59818660 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -5,6 +5,7 @@ on: branches: - main - master + - v3-alpha paths: - .github/workflows/semgrep.yml schedule: @@ -14,7 +15,7 @@ name: Semgrep jobs: semgrep: name: semgrep/ci - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} container: diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 000000000..c4ffd25fe --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,57 @@ +name: Mark and Close Stale Issues + +on: + schedule: + - cron: '0 1 * * *' # Run at 1 AM UTC every day + workflow_dispatch: # Allow manual triggering + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v9 + with: + # General settings + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 45 + days-before-close: 10 + stale-issue-label: 'stale' + operations-per-run: 250 # Increased from 50 to 250 + + # Issue specific settings + stale-issue-message: | + This issue has been automatically marked as stale because it has not had recent activity. + It will be closed if no further activity occurs within the next 10 days. + + If this issue is still relevant, please add a comment to keep it open. + Thank you for your contributions. + + close-issue-message: | + This issue has been automatically closed due to lack of activity. + Please feel free to reopen it if it's still relevant. + + # PR specific settings - We will not mark PRs as stale + days-before-pr-stale: -1 # Disable PR staling + days-before-pr-close: -1 # Disable PR closing + + # Exemptions + exempt-issue-labels: 'pinned,security,onhold,inprogress,Selected For Development,bug,enhancement,v3-alpha,high-priority' + exempt-all-issue-milestones: true + exempt-all-issue-assignees: true + + # Protection for existing issues + exempt-issue-created-before: '2024-01-01T00:00:00Z' + start-date: '2025-06-01T00:00:00Z' # Don't start checking until June 1, 2025 + + # Only process issues, not PRs + only-labels: '' + any-of-labels: '' + remove-stale-when-updated: true + + # Debug options + debug-only: false # Set to true to test without actually marking issues + ascending: true # Process older issues first diff --git a/.github/workflows/test-nightly-releases.yml b/.github/workflows/test-nightly-releases.yml new file mode 100644 index 000000000..63df09935 --- /dev/null +++ b/.github/workflows/test-nightly-releases.yml @@ -0,0 +1,216 @@ +name: Test Nightly Releases (Dry Run) + +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no actual releases)' + required: false + default: true + type: boolean + test_branch: + description: 'Branch to test against' + required: false + default: 'master' + type: string + +env: + GO_VERSION: '1.24' + +jobs: + test-permissions: + name: Test Release Permissions + runs-on: ubuntu-latest + outputs: + authorized: ${{ steps.check.outputs.authorized }} + steps: + - name: Check if user is authorized + id: check + run: | + # Test authorization logic + AUTHORIZED_USERS="leaanthony" + + if [[ "$AUTHORIZED_USERS" == *"${{ github.actor }}"* ]]; then + echo "βœ… User ${{ github.actor }} is authorized" + echo "authorized=true" >> $GITHUB_OUTPUT + else + echo "❌ User ${{ github.actor }} is not authorized" + echo "authorized=false" >> $GITHUB_OUTPUT + fi + + test-changelog-extraction: + name: Test Changelog Extraction + runs-on: ubuntu-latest + needs: test-permissions + if: needs.test-permissions.outputs.authorized == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.test_branch }} + fetch-depth: 0 + + - name: Test v2 changelog extraction + run: | + echo "πŸ§ͺ Testing v2 changelog extraction..." + CHANGELOG_FILE="website/src/pages/changelog.mdx" + + if [ ! -f "$CHANGELOG_FILE" ]; then + echo "❌ v2 changelog file not found" + exit 1 + fi + + # Extract unreleased section + awk ' + /^## \[Unreleased\]/ { found=1; next } + found && /^## / { exit } + found && !/^$/ { print } + ' $CHANGELOG_FILE > v2_release_notes.md + + echo "πŸ“ v2 changelog content (first 10 lines):" + head -10 v2_release_notes.md || echo "No content found" + echo "Total lines: $(wc -l < v2_release_notes.md)" + + - name: Test v3 changelog extraction (if accessible) + run: | + echo "πŸ§ͺ Testing v3 changelog extraction..." + + if git show v3-alpha:docs/src/content/docs/changelog.mdx > /dev/null 2>&1; then + echo "βœ… v3 changelog accessible" + + git show v3-alpha:docs/src/content/docs/changelog.mdx | awk ' + /^## \[Unreleased\]/ { found=1; next } + found && /^## / { exit } + found && !/^$/ { print } + ' > v3_release_notes.md + + echo "πŸ“ v3 changelog content (first 10 lines):" + head -10 v3_release_notes.md || echo "No content found" + echo "Total lines: $(wc -l < v3_release_notes.md)" + else + echo "⚠️ v3 changelog not accessible from current context" + fi + + test-version-detection: + name: Test Version Detection + runs-on: ubuntu-latest + needs: test-permissions + if: needs.test-permissions.outputs.authorized == 'true' + outputs: + v2_current_version: ${{ steps.versions.outputs.v2_current }} + v2_next_patch: ${{ steps.versions.outputs.v2_next_patch }} + v2_next_minor: ${{ steps.versions.outputs.v2_next_minor }} + v2_next_major: ${{ steps.versions.outputs.v2_next_major }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Test version detection logic + id: versions + run: | + echo "πŸ§ͺ Testing version detection..." + + # Test v2 version parsing + if [ -f "v2/cmd/wails/internal/version.txt" ]; then + CURRENT_V2=$(cat v2/cmd/wails/internal/version.txt | sed 's/^v//') + echo "Current v2 version: v$CURRENT_V2" + echo "v2_current=v$CURRENT_V2" >> $GITHUB_OUTPUT + + # Parse and increment + IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_V2" + MAJOR=${VERSION_PARTS[0]} + MINOR=${VERSION_PARTS[1]} + PATCH=${VERSION_PARTS[2]} + + PATCH_VERSION="v$MAJOR.$MINOR.$((PATCH + 1))" + MINOR_VERSION="v$MAJOR.$((MINOR + 1)).0" + MAJOR_VERSION="v$((MAJOR + 1)).0.0" + + echo "v2_next_patch=$PATCH_VERSION" >> $GITHUB_OUTPUT + echo "v2_next_minor=$MINOR_VERSION" >> $GITHUB_OUTPUT + echo "v2_next_major=$MAJOR_VERSION" >> $GITHUB_OUTPUT + + echo "βœ… Patch: v$CURRENT_V2 β†’ $PATCH_VERSION" + echo "βœ… Minor: v$CURRENT_V2 β†’ $MINOR_VERSION" + echo "βœ… Major: v$CURRENT_V2 β†’ $MAJOR_VERSION" + else + echo "❌ v2 version file not found" + fi + + test-commit-analysis: + name: Test Commit Analysis + runs-on: ubuntu-latest + needs: test-permissions + if: needs.test-permissions.outputs.authorized == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Test commit analysis + run: | + echo "πŸ§ͺ Testing commit analysis..." + + # Get recent commits for testing + echo "Recent commits:" + git log --oneline -10 + + # Test conventional commit detection + RECENT_COMMITS=$(git log --oneline --since="7 days ago") + echo "Commits from last 7 days:" + echo "$RECENT_COMMITS" + + # Analyze for release type + RELEASE_TYPE="patch" + if echo "$RECENT_COMMITS" | grep -q "feat!\|fix!\|BREAKING CHANGE:"; then + RELEASE_TYPE="major" + elif echo "$RECENT_COMMITS" | grep -q "feat\|BREAKING CHANGE"; then + RELEASE_TYPE="minor" + fi + + echo "βœ… Detected release type: $RELEASE_TYPE" + + test-summary: + name: Test Summary + runs-on: ubuntu-latest + needs: [test-permissions, test-changelog-extraction, test-version-detection, test-commit-analysis] + if: always() + steps: + - name: Print test results + run: | + echo "# πŸ§ͺ Nightly Release Workflow Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ needs.test-permissions.result }}" == "success" ]; then + echo "βœ… **Permissions Test**: Passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Permissions Test**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.test-changelog-extraction.result }}" == "success" ]; then + echo "βœ… **Changelog Extraction**: Passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Changelog Extraction**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.test-version-detection.result }}" == "success" ]; then + echo "βœ… **Version Detection**: Passed" >> $GITHUB_STEP_SUMMARY + echo " - Current v2: ${{ needs.test-version-detection.outputs.v2_current_version }}" >> $GITHUB_STEP_SUMMARY + echo " - Next patch: ${{ needs.test-version-detection.outputs.v2_next_patch }}" >> $GITHUB_STEP_SUMMARY + echo " - Next minor: ${{ needs.test-version-detection.outputs.v2_next_minor }}" >> $GITHUB_STEP_SUMMARY + echo " - Next major: ${{ needs.test-version-detection.outputs.v2_next_major }}" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Version Detection**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.test-commit-analysis.result }}" == "success" ]; then + echo "βœ… **Commit Analysis**: Passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Commit Analysis**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This was a dry-run test. No actual releases were created." >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/test-simple.yml b/.github/workflows/test-simple.yml new file mode 100644 index 000000000..8c4c88295 --- /dev/null +++ b/.github/workflows/test-simple.yml @@ -0,0 +1,11 @@ +name: Test Simple + +on: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Test + run: echo "Hello World" \ No newline at end of file diff --git a/.github/workflows/upload-source-documents.yml b/.github/workflows/upload-source-documents.yml index df15246fc..69d6c3e48 100644 --- a/.github/workflows/upload-source-documents.yml +++ b/.github/workflows/upload-source-documents.yml @@ -15,7 +15,7 @@ jobs: - name: Verify Changed files id: changed-files - uses: tj-actions/changed-files@v41 + uses: step-security/changed-files@3dbe17c78367e7d60f00d78ae6781a35be47b4a1 # v45.0.1 with: files: | website/**/*.mdx diff --git a/.gitignore b/.gitignore index 530d1e218..10709495a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,55 @@ v2/cmd/wails/internal/commands/initialise/templates/testtemplates/ /websitev3/site/ /v3/examples/plugins/bin/testapp +# V3 Example binaries - ignore executables that match directory names +/v3/examples/badge-custom/badge-custom +/v3/examples/badge/badge +/v3/examples/binding/binding +/v3/examples/cancel-async/cancel-async +/v3/examples/cancel-chaining/cancel-chaining +/v3/examples/clipboard/clipboard +/v3/examples/contextmenus/contextmenus +/v3/examples/dev/dev +/v3/examples/dialogs-basic/dialogs-basic +/v3/examples/dialogs/dialogs +/v3/examples/drag-n-drop/drag-n-drop +/v3/examples/environment/environment +/v3/examples/events-bug/events-bug +/v3/examples/events/events +/v3/examples/file-association/file-association +/v3/examples/frameless/frameless +/v3/examples/gin-example/gin-example +/v3/examples/gin-routing/gin-routing +/v3/examples/gin-service/gin-service +/v3/examples/hide-window/hide-window +/v3/examples/html-dnd-api/html-dnd-api +/v3/examples/ignore-mouse/ignore-mouse +/v3/examples/keybindings/keybindings +/v3/examples/menu/menu +/v3/examples/notifications/notifications +/v3/examples/panic-handling/panic-handling +/v3/examples/plain/plain +/v3/examples/raw-message/raw-message +/v3/examples/screen/screen +/v3/examples/services/services +/v3/examples/show-macos-toolbar/show-macos-toolbar +/v3/examples/single-instance/single-instance +/v3/examples/systray-basic/systray-basic +/v3/examples/systray-custom/systray-custom +/v3/examples/systray-menu/systray-menu +/v3/examples/video/video +/v3/examples/window-api/window-api +/v3/examples/window-call/window-call +/v3/examples/window-menu/window-menu +/v3/examples/window/window +/v3/examples/wml/wml + +# Common binary names in examples +/v3/examples/*/main +/v3/examples/*/app +/v3/examples/*/changeme +/v3/examples/*/testbuild-* + # Temporary called mkdocs, should be renamed to more standard -website or similar /docs/site .aider* diff --git a/docs/package-lock.json b/docs/package-lock.json index c8eb1d9af..6d5c5f596 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -15,6 +15,7 @@ "@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", @@ -1119,6 +1120,12 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "license": "MIT" + }, "node_modules/@ctrl/tinycolor": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz", @@ -2561,6 +2568,27 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "license": "MIT" }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -3489,6 +3517,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/common-ancestor-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", @@ -3510,6 +3547,15 @@ "node": ">= 0.6" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -3578,6 +3624,484 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.1.tgz", + "integrity": "sha512-dbeqFTLYEwlFg7UGtcZhCCG/2WayX72zK3Sq323CEX29CY81tYfVhw1MIdduCtpstB0cTOhJswWlM/OEB3Xp+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", + "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", + "license": "MIT", + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -3608,6 +4132,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3726,6 +4259,12 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", + "license": "(MPL-2.0 OR Apache-2.0)" + }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", @@ -3755,6 +4294,12 @@ "integrity": "sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==", "license": "ISC" }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" + }, "node_modules/emmet": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz", @@ -4742,6 +5287,18 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/import-meta-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", @@ -4758,6 +5315,15 @@ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-absolute-url": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", @@ -5006,6 +5572,36 @@ "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", "license": "MIT" }, + "node_modules/katex": { + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -5024,6 +5620,12 @@ "node": ">=6" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/linkedom": { "version": "0.14.26", "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.14.26.tgz", @@ -5098,6 +5700,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -5560,6 +6168,541 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "10.9.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.3.tgz", + "integrity": "sha512-V80X1isSEvAewIL3xhmz/rVmc27CVljcsbWxkxlWJWY/1kQa4XOABqpDl2qQLGKzpKm6WbTfUEKImBlUfFYArw==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^6.0.1", + "@types/d3-scale": "^4.0.3", + "@types/d3-scale-chromatic": "^3.0.0", + "cytoscape": "^3.28.1", + "cytoscape-cose-bilkent": "^4.1.0", + "d3": "^7.4.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.10", + "dayjs": "^1.11.7", + "dompurify": "^3.0.5 <3.1.7", + "elkjs": "^0.9.0", + "katex": "^0.16.9", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^1.3.0", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.3", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/mermaid/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mermaid/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/mermaid/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mermaid/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mermaid/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/mermaid/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/mermaid/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/mermaid/node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mermaid/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mermaid/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mermaid/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mermaid/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", @@ -6373,6 +7516,15 @@ "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==", "license": "MIT" }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -6440,6 +7592,12 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "license": "MIT" }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "license": "MIT" + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -7322,6 +8480,12 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", @@ -7383,6 +8547,30 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -7762,6 +8950,12 @@ "inline-style-parser": "0.2.4" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", @@ -7800,6 +8994,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tsconfck": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", @@ -8075,6 +9278,37 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -8441,6 +9675,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0" + }, "node_modules/which-pm": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-3.0.0.tgz", diff --git a/docs/package.json b/docs/package.json index e07456279..5d10c8ed3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -17,6 +17,7 @@ "@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", diff --git a/docs/src/components/Mermaid.astro b/docs/src/components/Mermaid.astro new file mode 100644 index 000000000..65b7e3ca9 --- /dev/null +++ b/docs/src/components/Mermaid.astro @@ -0,0 +1,59 @@ +--- +export interface Props { + title?: string; +} + +const { title = "" } = Astro.props; +--- + + + +
+
{title}
+
Loading diagram...
+
+ Source +
+
+
diff --git a/docs/src/content/docs/blog/2024-12-03-alpha10-and-new-release-strategy.md b/docs/src/content/docs/blog/2024-12-03-alpha10-and-new-release-strategy.md new file mode 100644 index 000000000..0cba0baa7 --- /dev/null +++ b/docs/src/content/docs/blog/2024-12-03-alpha10-and-new-release-strategy.md @@ -0,0 +1,52 @@ +--- +title: Alpha 10 Released - A New Chapter for Wails +description: Announcing Wails v3 Alpha 10 and our new daily release strategy +date: 2024-12-03 +author: The Wails Team +--- + +# Alpha 10 Released - A New Chapter for Wails + +We're thrilled to announce the release of **Wails v3 Alpha 10** - and it's big! While our release cadence may have seemed slow recently, there's been an incredible amount of work happening behind the scenes. Today marks not just another release, but a shift in how we approach the development of Wails. + +## Why the Wait? + +Like many development teams, we fell into the trap of trying to achieve perfection before each release. We wanted to squash every bug, polish every feature, and ensure everything was just right. But here's the thing - software is never truly bug-free, and waiting for that mythical state only delays getting improvements into your hands. + +## Our New Release Strategy + +Starting today, we're adopting a **daily release strategy**. Any new changes merged during the day will be released at the end of that day. This approach will: + +- Get bug fixes and features to you faster +- Provide more frequent feedback loops +- Speed up our journey to Beta +- Make the development process more transparent + +This means you might see more frequent, smaller releases rather than occasional large ones. We believe this will benefit everyone in the Wails community. + +## How You Can Help + +We've been overwhelmed by the number of people wanting to contribute to Wails! To make it easier for contributors to get involved, we're implementing a new system: + +- Firstly, we are opening up bug reporting for alpha releases. The frequent releases allow us to do this. +- Issues marked with **"Ready for Work"** are open for community contributions +- These issues have clear requirements and are ready to be tackled +- Most importantly, **we need people testing PRs** - this is one of the most valuable contributions you can make + +Testing doesn't require deep knowledge of the codebase, but it provides immense value by ensuring changes work across different environments and use cases. + +## What's Next? + +With our new daily release strategy, expect to see rapid progress toward Beta. We're committed to maintaining momentum and getting Wails v3 to a stable release as quickly as possible without sacrificing quality. + +And who knows? There might be a few surprises along the way... πŸ˜‰ + +## Thank You + +To everyone who has contributed code, tested releases, reported bugs, or simply used Wails - thank you. Your support and feedback drive this project forward. + +Here's to more frequent releases, faster iteration, and an exciting journey ahead! + +--- + +*Want to get involved? Check out our [GitHub repository](https://github.com/wailsapp/wails) for issues marked "ready for work", or join our community to help test the latest changes.* \ No newline at end of file diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index f63babaa4..f25ad4e81 100644 --- a/docs/src/content/docs/changelog.mdx +++ b/docs/src/content/docs/changelog.mdx @@ -25,14 +25,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Breaking Changes +## v3.0.0-alpha.17 - 2025-07-31 +## Fixed +- Fixed notification parsing on Windows @popaprozac in [PR](https://github.com/wailsapp/wails/pull/4450) + +## v3.0.0-alpha.16 - 2025-07-25 + +## Added +- Add Windows `getAccentColor` implementation by [@almas-x](https://github.com/almas-x) in [PR](https://github.com/wailsapp/wails/pull/4427) + +## v3.0.0-alpha.15 - 2025-07-25 + +## Added +- Add Windows `getAccentColor` implementation by [@almas-x](https://github.com/almas-x) in [PR](https://github.com/wailsapp/wails/pull/4427) + +## v3.0.0-alpha.14 - 2025-07-25 + +## Added +- Windows dark theme menus + menubar. By @leaanthony in [a29b4f0861b1d0a700e9eb213c6f1076ec40efd5](https://github.com/wailsapp/wails/commit/a29b4f0861b1d0a700e9eb213c6f1076ec40efd5) +- Rename built-in services for clearer JS/TS bindings by @popaprozac in [PR](https://github.com/wailsapp/wails/pull/4405) + +## v3.0.0-alpha.12 - 2025-07-15 + +### Added +- `app.Env.GetAccentColor` to get the accent color of a user's system. Works on MacOS. by [@etesam913](https://github.com/etesam913) +- Add `window.ToggleFrameless()` api by [@atterpac](https://github.com/atterpac) in [#4137](https://github.com/wailsapp/wails/pull/4137) + +### Fixed +- Fixed doctor command to check for Windows SDK dependencies by [@kodumulo](https://github.com/kodumulo) in [#4390](https://github.com/wailsapp/wails/issues/4390) + + +## v3.0.0-alpha.11 - 2025-07-12 + +## Added + - Add distribution-specific build dependencies for Linux by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/4345) + - Added bindings guide by @atterpac in [PR](https://github.com/wailsapp/wails/pull/4404) + +## v3.0.0-alpha.10 - 2025-07-06 + +### Breaking Changes +- **Manager API Refactoring**: Reorganized application API from flat structure to organized managers for better code organization and discoverability by [@leaanthony](https://github.com/leaanthony) in [#4359](https://github.com/wailsapp/wails/pull/4359) + - `app.NewWebviewWindow()` β†’ `app.Window.New()` + - `app.CurrentWindow()` β†’ `app.Window.Current()` + - `app.GetAllWindows()` β†’ `app.Window.GetAll()` + - `app.WindowByName()` β†’ `app.Window.GetByName()` + - `app.EmitEvent()` β†’ `app.Event.Emit()` + - `app.OnApplicationEvent()` β†’ `app.Event.OnApplicationEvent()` + - `app.OnWindowEvent()` β†’ `app.Event.OnWindowEvent()` + - `app.SetApplicationMenu()` β†’ `app.Menu.SetApplicationMenu()` + - `app.OpenFileDialog()` β†’ `app.Dialog.OpenFile()` + - `app.SaveFileDialog()` β†’ `app.Dialog.SaveFile()` + - `app.MessageDialog()` β†’ `app.Dialog.Message()` + - `app.InfoDialog()` β†’ `app.Dialog.Info()` + - `app.WarningDialog()` β†’ `app.Dialog.Warning()` + - `app.ErrorDialog()` β†’ `app.Dialog.Error()` + - `app.QuestionDialog()` β†’ `app.Dialog.Question()` + - `app.NewSystemTray()` β†’ `app.SystemTray.New()` + - `app.GetSystemTray()` β†’ `app.SystemTray.Get()` + - `app.ShowContextMenu()` β†’ `app.ContextMenu.Show()` + - `app.RegisterKeybinding()` β†’ `app.KeyBinding.Register()` + - `app.UnregisterKeybinding()` β†’ `app.KeyBinding.Unregister()` + - `app.GetPrimaryScreen()` β†’ `app.Screen.GetPrimary()` + - `app.GetAllScreens()` β†’ `app.Screen.GetAll()` + - `app.BrowserOpenURL()` β†’ `app.Browser.OpenURL()` + - `app.Environment()` β†’ `app.Env.GetAll()` + - `app.ClipboardGetText()` β†’ `app.Clipboard.Text()` + - `app.ClipboardSetText()` β†’ `app.Clipboard.SetText()` - Renamed Service methods: `Name` -> `ServiceName`, `OnStartup` -> `ServiceStartup`, `OnShutdown` -> `ServiceShutdown` by [@leaanthony](https://github.com/leaanthony) - Moved `Path` and `Paths` methods to `application` package by [@leaanthony](https://github.com/leaanthony) - The application menu is now macOS only by [@leaanthony](https://github.com/leaanthony) ### Added +- **Organized Testing Infrastructure**: Moved Docker test files to dedicated `test/docker/` directory with optimized images and enhanced build reliability by [@leaanthony](https://github.com/leaanthony) in [#4359](https://github.com/wailsapp/wails/pull/4359) +- **Improved Resource Management Patterns**: Added proper event handler cleanup and context-aware goroutine management in examples by [@leaanthony](https://github.com/leaanthony) in [#4359](https://github.com/wailsapp/wails/pull/4359) - Support aarch64 AppImage builds by [@AkshayKalose](https://github.com/AkshayKalose) in [#3981](https://github.com/wailsapp/wails/pull/3981) - Add diagnostics section to `wails doctor` by [@leaanthony](https://github.com/leaanthony) - Add window to context when calling a service method by [@leaanthony](https://github.com/leaanthony) @@ -53,10 +120,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `//wails:ignore` directive to prevent binding generation for chosen service methods by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045) - Add `//wails:internal` directive on services and models to allow for types that are exported in Go but not in JS/TS by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045) - Add binding generator support for constants of alias type to allow for weakly typed enums by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045) +- Add binding generator tests for Go 1.24 features by [@fbbdev](https://github.com/fbbdev) in [#4068](https://github.com/wailsapp/wails/pull/4068) - Add support for macOS 15 "Sequoia" to `OSInfo.Branding` for improved OS version detection in [#4065](https://github.com/wailsapp/wails/pull/4065) +- Add `PostShutdown` hook for running custom code after the shutdown process completes by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- Add `FatalError` struct to support detection of fatal errors in custom error handlers by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- Standardise and document service startup and shutdown order by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- Add test harness for application startup/shutdown sequence and service startup/shutdown tests by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- Add `RegisterService` method for registering services after the application has been created by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- Add `MarshalError` field in application and service options for custom error handling in binding calls by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- Add cancellable promise wrapper that propagates cancellation requests through promise chains by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100) +- Add the ability to tie binding call cancellation to an `AbortSignal` by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100) +- Support `data-wml-*` attributes for WML alongside the usual `wml-*` attributes by [@leaanthony](https://github.com/leaanthony) +- Add `Configure` method on all services for late configuration/dynamic reconfiguration by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- `fileserver` service sends a 503 Service Unavailable response when unconfigured by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- `kvstore` service provides an in-memory key-value store by default when unconfigured by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- Add `Load` method on `kvstore` service to reload data from file after config changes by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- Add `Clear` method on `kvstore` service to delete all keys by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- Add type `Level` in `log` service to provide JS-side log-level constants by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- Add `Log` method on `log` service to specify log-level dynamically by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- `sqlite` service provides an in-memory DB by default when unconfigured by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- Add method `Close` on `sqlite` service to close the DB manually by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- Add cancellation support for query methods on `sqlite` service by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- Add prepared statement support to `sqlite` service with JS bindings by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- Gin support by [Lea Anthony](https://github.com/leaanthony) in [PR](https://github.com/wailsapp/wails/pull/3537) based on the original work of [@AnalogJ](https://github.com/AnalogJ) in PR[https://github.com/wailsapp/wails/pull/3537] +- Fix auto save and password auto save always enabled by [@oSethoum](https://github.com/osethoum) in [#4134](https://github.com/wailsapp/wails/pull/4134) +- Add `SetMenu()` on window to allow for setting a menu on a window by [@leaanthony](https://github.com/leaanthony) +- Add Notification support by [@popaprozac](https://github.com/popaprozac) in [#4098](https://github.com/wailsapp/wails/pull/4098) +- ο£Ώ Add File Association support for mac by [@wimaha](https://github.com/wimaha) in [#4177](https://github.com/wailsapp/wails/pull/4177) +- Add `wails3 tool version` for semantic version bumping by [@leaanthony](https://github.com/leaanthony) +- Add badging support for macOS and Windows by [@popaprozac](https://github.com/popaprozac) in [#](https://github.com/wailsapp/wails/pull/4234) ### Fixed +- Fixed nil pointer dereference in processURLRequest for Mac by [@etesam913](https://github.com/etesam913) in [#4366](https://github.com/wailsapp/wails/pull/4366) +- Fixed a linux bug preventing filtered dialogs by [@bh90210](https://github.com/bh90210) in [#4287](https://github.com/wailsapp/wails/pull/4287) - Fixed Windows+Linux Edit Menu issues by [@leaanthony](https://github.com/leaanthony) in [#3f78a3a](https://github.com/wailsapp/wails/commit/3f78a3a8ce7837e8b32242c8edbbed431c68c062) - Updated the minimum system version in macOS .plist files from 10.13.0 to 10.15.0 by [@AkshayKalose](https://github.com/AkshayKalose) in [#3981](https://github.com/wailsapp/wails/pull/3981) - Window ID skip issue by [@leaanthony](https://github.com/leaanthony) @@ -81,6 +178,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Suppressed warnings for services that define lifecycle or http methods but no other bound methods by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045) - Fixed non-React templates failing to display Hello World footer when using light system colour scheme by [@marcus-crane](https://github.com/marcus-crane) in [#4056](https://github.com/wailsapp/wails/pull/4056) - Fixed hidden menu items on macOS by [@leaanthony](https://github.com/leaanthony) +- Fixed handling and formatting of errors in message processors by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- ο£Ώ Fixed skipped service shutdown when quitting application by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- ο£Ώ Ensure menu updates occur on the main thread by [@leaanthony](https://github.com/leaanthony) +- The dragging and resizing mechanism is now more robust and matches expected platform behaviour more closely by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100) +- Fixed [#4097](https://github.com/wailsapp/wails/issues/4097) Webpack/angular discards runtime init code by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100) +- Fixed initially-hidden menu items by [@IanVS](https://github.com/IanVS) in [#4116](https://github.com/wailsapp/wails/pull/4116) +- Fixed assetFileServer not serving `.html` files when non-extension request when `[request]` doesn't exist but `[request].html` does +- Fixed icon generation paths by [@robin-samuel](https://github.com/robin-samuel) in [#4125](https://github.com/wailsapp/wails/pull/4125) +- Fixed `fullscreen`, `unfullscreen`, `unminimise` and `unmaximise` events not being emitted by [@oSethoum](https://github.com/osethoum) in [#4130](https://github.com/wailsapp/wails/pull/4130) +- Fixed NSIS Error because of incorrect prefix on default version in config by [@robin-samuel](https://github.com/robin-samuel) in [#4126](https://github.com/wailsapp/wails/pull/4126) +- Fixed Dialogs runtime function returning escaped paths on Windows by [TheGB0077](https://github.com/TheGB0077) in [#4188](https://github.com/wailsapp/wails/pull/4188) +- Fixed Webview2 detection path in HKCU by [@leaanthony](https://github.com/leaanthony). +- Fixed input issue with macOS by [@leaanthony](https://github.com/leaanthony). +- Fixed Windows icon generation task file name by [@yulesxoxo](https://github.com/yulesxoxo) in [#4219](https://github.com/wailsapp/wails/pull/4219). +- Fixed transparency issue for frameless windows by [@leaanthony](https://github.com/leaanthony) based on work by @kron. +- Fixed focus calls when window is disabled or minimised by [@leaanthony](https://github.com/leaanthony) based on work by @kron. +- Fixed system trays not showing after taskbar restarts by [@leaanthony](https://github.com/leaanthony) based on work by @kron. +- Fixed fallbackResponseWriter not implementing Flush() in [#4245](https://github.com/wailsapp/wails/pull/4245) +- Fixed fallbackResponseWriter not implementing Flush() by [@superDingda] in [#4236](https://github.com/wailsapp/wails/issues/4236) +- Fixed macOS window close with pending async Go-bound function call crashes by [@joshhardy](https://github.com/joshhardy) in [#4354](https://github.com/wailsapp/wails/pull/4354) +- Fixed Windows Efficiency mode startup race condition by [@leaanthony](https://github.com/leaanthony) +- Fixed Windows icon handle cleanup by [@leaanthony](https://github.com/leaanthony). +- Fixed `OpenFileManager` on Windows by [@PPTGamer](https://github.com/PPTGamer) in [#4375](https://github.com/wailsapp/wails/pull/4375). ### Changed @@ -94,9 +214,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - In JS/TS bindings, `internal.js/ts` model files have been removed; all models can now be found in `models.js/ts` by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045) - In JS/TS bindings, named types are never rendered as aliases for other named types; the old behaviour is now restricted to aliases by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045) - In JS/TS bindings, in class mode, struct fields whose type is a type parameter are marked optional and never initialised automatically by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045) +- Remove ESLint from templates by by [@IanVS](https://github.com/IanVS) in [#4059](https://github.com/wailsapp/wails/pull/4059) - Update copyright date to 2025 by [@IanVS](https://github.com/IanVS) in [#4037](https://github.com/wailsapp/wails/pull/4037) - Add docs for event.Sender by [@IanVS](https://github.com/IanVS) in [#4075](https://github.com/wailsapp/wails/pull/4075) - Go 1.24 support by [@leaanthony](https://github.com/leaanthony) +- `ServiceStartup` hooks are now invoked when `App.Run` is called, not in `application.New` by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- `ServiceStartup` errors are now returned from `App.Run` instead of terminating the process by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- Binding and dialog calls from JS now reject with error objects instead of strings by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066) +- Improved systray menu positioning on Windows by [@leaanthony](https://github.com/leaanthony) +- The JS runtime has been ported to TypeScript by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100) +- The runtime initialises as soon as it is imported, no need to wait for the window to load by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100) +- The runtime does not export an init method anymore. A side effects import can be used to initialise it by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100) +- Bound methods now return a `CancellablePromise` that rejects with a `CancelError` if cancelled. The actual result of the call is discarded by [@fbbdev](https://github.com/fbbdev) in [#4100](https://github.com/wailsapp/wails/pull/4100) +- Built-in service types are now consistently called `Service` by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- Built-in service creation functions with options are now consistently called `NewWithConfig` by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- `Select` method on `sqlite` service is now named `Query` for consistency with Go APIs by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) +- Templates: moved runtime to "dependencies", organized package.json files by [@IanVS](https://github.com/IanVS) in [#4133](https://github.com/wailsapp/wails/pull/4133) +- Creates and ad-hoc signs app bundles in dev to enable certain macOS APIs by [@popaprozac](https://github.com/popaprozac) in [#4171](https://github.com/wailsapp/wails/pull/4171) ## v3.0.0-alpha.9 - 2025-01-13 @@ -362,9 +496,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 🐧 add task `run:linux` by [@marcus-crane](https://github.com/marcus-crane) in [#3146](https://github.com/wailsapp/wails/pull/3146) -- Export `SetIcon` method by @almas1992 in +- Export `SetIcon` method by [@almas-x](https://github.com/almas-x) in [PR](https://github.com/wailsapp/wails/pull/3147) -- Improve `OnShutdown` by @almas1992 in +- Improve `OnShutdown` by [@almas-x](https://github.com/almas-x) in [PR](https://github.com/wailsapp/wails/pull/3189) - Restore `ToggleMaximise` method in `Window` interface by [@fbbdev](https://github.com/fbbdev) in @@ -435,7 +569,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed bug for linux in doctor in the event user doesn't have proper drivers installed. Added by [@pylotlight](https://github.com/pylotlight) in [PR](https://github.com/wailsapp/wails/pull/3032) -- Fix dpi scaling on start up (windows). Changed by @almas1992 in +- Fix dpi scaling on start up (windows). Changed by [@almas-x](https://github.com/almas-x) in [PR](https://github.com/wailsapp/wails/pull/3145) - Fix replace line in `go.mod` to use relative paths. Fixes Windows paths with spaces - @leaanthony. diff --git a/docs/src/content/docs/contributing/_category_.json b/docs/src/content/docs/contributing/_category_.json new file mode 100644 index 000000000..5f76b677d --- /dev/null +++ b/docs/src/content/docs/contributing/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Technical Documentation", + "link": { + "type": "generated-index", + "description": "Deep dive into Wails v3 internals for developers who want to understand or contribute to the codebase." + }, + "order": 50, + "collapsed": false +} diff --git a/docs/src/content/docs/contributing/architecture.mdx b/docs/src/content/docs/contributing/architecture.mdx new file mode 100644 index 000000000..6490a35bc --- /dev/null +++ b/docs/src/content/docs/contributing/architecture.mdx @@ -0,0 +1,165 @@ +--- +title: Wails v3 Architecture +description: Deep-dive diagrams and explanations of every moving part inside Wails v3 +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 +let you ship native applications powered by modern web tech. + +This page presents the *big picture* in four diagrams: + +1. **Overall Architecture** – how every subsystem connects +2. **Runtime Flow** – what happens when JS calls Go and vice-versa +3. **Development vs Production** – two modes of the asset server +4. **Platform Implementations** – where OS-specific code lives + +--- + +## 1 Β· Overall Architecture + + +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 + + CLI -->|init| TMP + CLI -->|generate| GEN + CLI -->|dev| ASSETDEV + CLI -->|build| PKG + + GEN -->|Go & TS stubs| BACKEND + GEN -->|bindings.json| FRONTEND + + ASSETDEV <-->|HTTP| FRONTEND + + BACKEND <--> BRIDGE <--> FRONTEND + BRIDGE <--> RT + RT <-->|serve assets| ASSETDEV + + +--- + +## 2 Β· Runtime Call Flow + + +sequenceDiagram + participant JS as JavaScript (frontend) + participant Bridge as Bridge (WebView callback) + participant MP as Message Processor (Go) + participant Go as Bound Go Function + + 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") + + +Key points: + +* **No HTTP / IPC** – the bridge uses the native WebView’s in-memory channel +* **Method IDs** – deterministic FNV-hash enables O(1) lookup in Go +* **Promises** – errors propagate as rejections with stack & code + +--- + +## 3 Β· Development vs Production Asset Flow + + +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 + + VITE <-->|proxy / HMR| ASDEV + ASDEV <-->|http| FRONTENDDEV + + EMBED --> ASPROD + ASPROD <-->|in-memory| FRONTENDPROD + + +* In **dev** the server proxies unknown paths to the framework’s live-reload + server and serves static assets from disk. +* In **prod** the same API is backed by `go:embed`, producing a zero-dependency + binary. + +--- + +## 4 Β· Platform-Specific Runtime Split + + +classDiagram + class runtime::Window { + +Show() + +Hide() + +Center() + } + + runtime::Window <|-- Window_darwin + runtime::Window <|-- Window_linux + runtime::Window <|-- Window_windows + + 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" + + +Every feature follows this pattern: + +1. **Common interface** in `pkg/application` +2. **Message processor** entry in `pkg/application/messageprocessor_*.go` +3. **Implementation** per OS under `internal/runtime/*.go` guarded by build tags + +Missing functionality on an OS should return `ErrCapability` and register +availability via `internal/capabilities`. + +--- + +## Summary + +These diagrams outline **where the code lives**, **how data moves**, and +**which layers own which responsibilities**. +Keep them handy while exploring the detailed pages that follow – they are your +map to the Wails v3 source tree. diff --git a/docs/src/content/docs/contributing/asset-server.mdx b/docs/src/content/docs/contributing/asset-server.mdx new file mode 100644 index 000000000..e49cd6700 --- /dev/null +++ b/docs/src/content/docs/contributing/asset-server.mdx @@ -0,0 +1,203 @@ +--- +title: Asset Server +description: How Wails v3 serves and embeds your web assets in development and production +sidebar: + order: 4 +--- + +## Overview + +Every Wails application ships a **single native executable** that combines: + +1. Your *Go* backend +2. A *Web* frontend (HTML + JS + CSS) + +The **Asset Server** is the glue that makes this possible. +It has **two operating modes** that are selected at compile-time via Go build +tags: + +| Mode | Tag | Purpose | +|------|-----|---------| +| **Development** | `//go:build dev` | Fast iteration with hot-reload | +| **Production** | `//go:build !dev` | Zero-dependency, embedded assets | + +The implementation lives in +`v3/internal/assetserver/` with clear file splits: + +``` +assetserver_dev.go # ⬅️ runtime dev server +assetserver_production.go # ⬅️ embedded server +assetserver_darwin.go # OS-specific helpers (same for linux/windows) +asset_fileserver.go # Shared static file logic +content_type_sniffer.go # MIME type detection +ringqueue.go # Tiny LRU for mime cache +``` + +--- + +## Development Mode + +### Lifecycle + +1. `wails3 dev` boots and **spawns your frontend dev server** + (Vite, SvelteKit, React-SWC …) by running the task defined in + `frontend/Taskfile.yml` (usually `npm run dev`). +2. Wails starts the **Dev Asset Server** listening on `localhost:` and + tells the Go runtime to load `http://:` as the window URL. +3. Incoming requests are handled by `assetserver_dev.go`: + + ``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” /runtime/... β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Browser β”‚ ── native bridge ───▢ β”‚ Runtime β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ JS β”‚ / (index.html) proxy / -> Vite + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ◀─────────────┐ + AssetServer β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Vite Dev β”‚ + β”‚ Server β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + ``` + +4. Static files (`/assets/logo.svg`) are **served directly from disk** via + `asset_fileserver.go` (for speed) while anything unknown is **proxied** to + the framework dev server, giving you *instant* hot-module replacement. + +### Features + +* **Live Reload** – Vite/Snowpack/… injects HMR WebSocket; Wails only has to + proxy it. +* **Source Map Support** – because assets are not bundled, your browser devtools + map errors back to original source. +* **No Go Re-compile** – Only the frontend rebuilds; Go code stays running until + you change `.go` files. + +### Switching Frameworks + +The dev proxy is **framework-agnostic**: + +* The `wails.json` template injects two env vars: + `FRONTEND_DEV_HOST` & `FRONTEND_DEV_PORT`. +* Taskfiles for each template emit those vars before running their dev servers. +* `assetserver_dev.go` simply proxies to that target. + +Add a new template β†’ define its dev task β†’ Asset Server just works. + +--- + +## Production Mode + +When you run `wails3 build` the pipeline: + +1. Runs the frontend **production build** (`npm run build`) producing + `/frontend/dist/**`. +2. **Embeds** that folder into `go:embed` FS at compile time (see + `bundled_assetserver.go` generated file). +3. Compiles the Go binary with `-tags production` (implicit). + +### Request Handling + +```go +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // 1. Try embedded static assets (exact path) + // 2. Fallback to index.html for SPA routing + // 3. Sniff content-type if extension unknown + // 4. Set strong cache headers +} +``` + +* **MIME Detection** – If the build tool produced extension-less files (e.g. + `/assets/manifest`) `content_type_sniffer.go` inspects the first 512 bytes and + caches the result in a tiny lock-free LRU. +* **Ring Queue Caching** – Frequently accessed assets (logo, CSS) are kept + in-memory for the lifetime of the app, removing the need for disk or embed FS + lookups. +* **Security Headers** – Disallows `file://` navigation, enables `nosniff`. + +Because everything is embedded, the shipped binary has **no external +dependencies** (even on Windows). + +--- + +## Bridging Dev ↔ Prod + +Both modes expose the **same public interface**: + +```go +type AssetServer interface { + URL() string // dev: http://localhost:34115, prod: wails://app + Open() error // start listening + Close() // graceful shutdown +} +``` + +`pkg/application` happily uses whichever implementation was compiled in, meaning +**your application code does not change** between `dev` and `build`. + +--- + +## How Frontend Frameworks Integrate + +### Templates + +Each official template (React, Vue, Svelte, Solid…) contains: + +* `frontend/Taskfile.yml` +* `frontend/vite.config.ts` (or equivalent) + +They export two tasks: + +| Task | Purpose | +|------|---------| +| `dev` | Starts the framework dev server on a **random free port** and prints it to stdout (`PORT=5173`). | +| `build` | Produces static assets into `dist/` + manifest for cache-busting. | + +`internal/commands/dev.go` parses that stdout, sets `FRONTEND_DEV_*` env vars +and launches the **Dev Asset Server**. + +Frameworks remain fully decoupled from Go: + +* No need to import Wails JS SDK at build time – the runtime injects it at + window creation. +* Any framework with an HTTP dev server can plug in. + +--- + +## Extending / Customising + +Need custom headers, auth, or gzip? + +1. Implement `type Middleware func(http.Handler) http.Handler` +2. Register via `internal/assetserver/options.go` +3. For prod, remember to add the same middleware in `assetserver_production.go`. + +--- + +## Key Source Files + +| File | Role | +|------|------| +| `assetserver_dev.go` | Reverse proxy + disk file server | +| `assetserver_production.go` | Embedded FS handler | +| `options.go` | Config struct parsed from `pkg/options/assetserver` | +| `build_dev.go` / `build_production.go` | Build-tag wrappers selecting correct implementation | +| `bundled_assetserver.go` | Generated embed data (only present in production builds) | + +--- + +## Gotchas & Debugging + +* **White Screen in Prod** – usually SPA routing: ensure `History API Fallback` + is enabled in dev and `index.html` fallback works in prod. +* **404 in Dev** – mis-matched `FRONTEND_DEV_PORT`; run with + `WAILSDEV_VERBOSE=1` to print every proxied request. +* **Large Assets** – they are embedded; consider + [`assetserver.WithExternalDir("/path")`](https://pkg.go.dev) to load from disk. + +--- + +You now know how the Wails **Asset Server** feeds your web code to the native +window in both **development** and **production**. +Master this layer and you can debug loading issues, add middlewares, or even +swap in a completely different frontend tool-chain with confidence. diff --git a/docs/src/content/docs/contributing/binding-system.mdx b/docs/src/content/docs/contributing/binding-system.mdx new file mode 100644 index 000000000..071dc82f1 --- /dev/null +++ b/docs/src/content/docs/contributing/binding-system.mdx @@ -0,0 +1,240 @@ +--- +title: Binding System +description: How Wails v3 lets Go and JavaScript call each other with zero boilerplate +sidebar: + order: 5 +--- + +> β€œBindings” are the **type-safe contract** that lets you write: + +```go +msg, err := chatService.Send("Hello") +``` + +in Go *and* + +```ts +const msg = await window.backend.ChatService.Send("Hello") +``` + +in TypeScript **without manual glue code**. +This document breaks down *how* that magic happens, from **static analysis** at +build time, through **code generation**, to the **runtime bridge** that moves +bytes over the WebView. + +--- + +## 1. 30-Second Overview + +| 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)
β€’ `frontend/src/wailsjs/**.ts` (TS defs)
β€’ `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: + +``` +wails3 generate ─┬─> generator.Collect() // parse Go packages + β”œβ”€> generator.Analyse() // build semantic model + └─> generator.Render() // write files + `go fmt` +``` + +--- + +## 2. Static Analysis + +### Entry Point + +``` +internal/generator/analyse.go +``` + +`Analyse(cfg generator.Config)`: + +1. Recursively loads provided Go packages with `go/packages`. +2. Walks the *type* and *value* specs of every AST file. +3. Filters **exported** symbols (capitalised) outside `internal/` packages. +4. Records: + * Receiver type (`struct`, `interface`) + * Method name + * Parameter list (name, type, pointer, variadic) + * Return tuple (values + error presence) +5. Normalises types to a **serialisable set** + (`int`, `float64`, `string`, `[]byte`, slices, maps, structs, pointers). + +Unsupported types produce a **compile-time error** so mistakes are caught early. + +### Model + +```go +type Method struct { + ID uint32 // stable hash for runtime lookup + Name string + Params []Param + Results []Result + Receiver *Struct // nil for package funcs +} +``` + +A deterministic **FNV-1a** hash (`internal/hash/fnv.go`) of +`.()` becomes the *method ID* used at runtime. + +--- + +## 3. Code Generation + +### Templates + +`internal/generator/render/` contains text/template files: + +| Template | Target | +|----------|--------| +| `go_bindings.tmpl` | `pkg/application/bindings.go` | +| `ts_bindings.tmpl` | `frontend/wailsjs/go/models.d.ts` | +| `ts_index.tmpl` | `frontend/wailsjs/index.ts` | + +Add or adjust templates here to customise generation. + +### Go Output + +`bindings.go` registers a lookup table: + +```go +var bindings = []application.BoundMethod{ + {ID: 0x7A1201D3, Name: "ChatService.Send", Call: chatServiceSend}, +} + +func chatServiceSend(ctx context.Context, in []byte) ([]byte, error) { + var req struct{ Msg string } + if err := json.Unmarshal(in, &req); err != nil { return nil, err } + res, err := chatService.Send(req.Msg) + return json.Marshal(res), err +} +``` + +Key points: + +* **Zero reflection** at runtime β†’ performance. +* Marshal/Unmarshal is **per-method** with generated struct wrappers. + +### TypeScript Output + +```ts +export namespace backend { + export namespace ChatService { + function Send(msg: string): Promise; + } +} +``` + +* Emitted as **ES modules** so any bundler can tree-shake. +* Method IDs are embedded in an auto-generated `bindings.json` for the JS + runtime. + +--- + +## 4. Runtime Invocation Protocol + +### JavaScript Side + +```ts +import { invoke } from "@wailsio/runtime"; + +await invoke(0x7a1201d3 /* ChatService.Send */, ["Hello"]); +``` + +Implementation: `runtime/desktop/@wailsio/runtime/invoke.ts` + +1. Packs `{t:"c", id:, p:[...args]}` into JSON. +2. Calls `window.external.invoke(payload)` (WebView2) or equivalent. +3. Returns a `Promise` that resolves/rejects based on the reply. + +### Go Side + +1. `messageprocessor_call.go` receives the JSON. +2. Looks up `methodID` in the `bindings` slice. +3. Executes the generated stub (`chatServiceSend`). +4. Serialises `{result, error}` back to JS. + +### Error Mapping + +| Go | JavaScript | +|----|------------| +| `error == nil` | `Promise` resolves with result | +| `errors.New(...)` | `Promise` rejects with `{message, stack, code}` | + +The mapping code lives in `runtime/desktop/@wailsio/runtime/errors.ts`. + +--- + +## 5. Calling JavaScript from Go + +*Browser β†’ Go* is covered above. +*Go β†’ Browser* uses **Events** or **Eval**: + +```go +window.Eval(`alert("Hi")`) +app.Publish("chat:new-message", msg) +``` + +Binding generation is one-way (Go methods). +For JS-exposed functions use `runtime.EventsOn` in JS and `application.Publish` +from Go. + +--- + +## 6. Extending & Troubleshooting + +### Adding Custom Serialisers + +* Implement `generator.TypeConverter` interface. +* Register in `generator.Config.Converters`. +* Update JS runtime deserialisation if needed. + +### Unsupported Type Error + +``` +error: field "Client" uses unsupported type: chan struct{} +``` + +β†’ wrap the channel in a struct with an exposed API or redesign. + +### Version Skew + +Bindings are regenerated on **every** `wails3 dev` / `wails3 build`. +If IDE intellisense shows stale stubs, delete `frontend/wailsjs` and rebuild. + +### Performance Tips + +* Prefer **value** receivers for small structs to reduce allocations. +* Avoid large byte slices over the bridge; use `application.FileServer` instead. +* Batch multiple quick calls into one method to minimise bridge latency. + +--- + +## 7. Key Files Map + +| Concern | File | +|---------|------| +| Static analysis entry | `internal/generator/analyse.go` | +| Render pipeline | `internal/generator/generate.go` | +| Template assets | `internal/generator/render/*.tmpl` | +| Go binding table | `pkg/application/bindings.go` (generated) | +| Call processor | `pkg/application/messageprocessor_call.go` | +| JS runtime | `runtime/desktop/@wailsio/runtime/invoke.ts` | +| Errors mapping | `runtime/desktop/@wailsio/runtime/errors.ts` | + +Keep this cheat-sheet handy when you trace a bridge bug. + +--- + +## 8. Recap + +1. **Generator** scans your Go code β†’ semantic model. +2. **Templates** emit **Go stubs** + **TypeScript definitions**. +3. **Message Processor** executes stubs at runtime. +4. **JS Runtime** wraps it all in idiomatic promises. + +All without reflection, without IPC servers, and without you writing a single +line of boilerplate. That’s the Wails v3 binding system. Go forth and bind! diff --git a/docs/src/content/docs/contributing/build-packaging.mdx b/docs/src/content/docs/contributing/build-packaging.mdx new file mode 100644 index 000000000..78878200e --- /dev/null +++ b/docs/src/content/docs/contributing/build-packaging.mdx @@ -0,0 +1,211 @@ +--- +title: Build & Packaging Pipeline +description: What happens under the hood when you run `wails3 build`, how cross-platform binaries are produced, and how installers are generated for each OS. +sidebar: + order: 6 +--- + +`wails3 build` is a **single command** that drives a _multi-stage_ pipeline: + +1. **Frontend production build** (Vite / React / …) +2. **Asset embedding** into Go sources +3. **Native compilation** for the target OS/arch +4. **Post-processing** (icon injection, version info, codesign) +5. **Packaging** into a distributable artefact (AppImage, DMG, MSI, …) + +This document follows every stage, shows where the code lives, and explains how +to customise or debug it. + +--- + +## 1. Entry Point: `internal/commands/build-assets.go` + +``` +wails3 build # cmd/wails3/main.go +└─ internal/commands.Build() # build-assets.go +``` + +High-level tasks are delegated to **Taskfile** targets so the same logic runs in +CI or locally. + +| Stage | Taskfile target | Go implementation | +|-------|-----------------|-------------------| +| Frontend build | `frontend:build` | `internal/commands/task.go` | +| Embed assets | `embed:assets` | generated `bundled_assetserver.go` | +| Go compile | `build:go` | `tool_buildinfo.go`, `tool_package.go` | +| Package | `package:*` | `internal/packager`, `internal/commands/*` | + +--- + +## 2. Frontend Production Build + +1. The CLI changes into `frontend/` and executes the `build` task found in the + project `Taskfile.yml` (`npm run build` by default). +2. Output is written to `frontend/dist/`. +3. A **content hash** manifest is produced (`manifest.json`) so cache-busting + works out of the box. + +If the task fails the pipeline aborts early, returning the exit code of your +build tool. + +--- + +## 3. Embedding Assets + +Implemented in `internal/assetserver/build_production.go`: + +* Walks `frontend/dist` and generates a `go:embed` file + `bundled_assetserver.go` (ignored by git). +* Adds the `production` build tag. + +Result: the final binary contains every HTML/JS/CSS byte, so the executable is +self-contained. + +--- + +## 4. Native Compilation + +### Build Flags + +| Flag | Purpose | +|------|---------| +| `-tags production` | Select prod asset server & runtime stubs | +| `-ldflags "-s -w"` | Strip symbol table & DWARF (smaller binaries) | +| `-X internal/buildinfo.BuildTime=$(date)` | Embed reproducible metadata | + +`internal/commands/tool_buildinfo.go` collects version, git commit, and build +time then injects them using `go build -ldflags`. + +### Cross Compilation Matrix + +| OS | Arch | Build Tag | CGO | Notes | +|----|------|-----------|-----|-------| +| Windows | amd64 / arm64 | `windows` | cgo enabled for WebView2 | Generates `.syso` via `internal/commands/syso.go` | +| macOS | amd64 / arm64 | `darwin` | cgo enabled (Objective-C) | Codesign & notarisation tasks available | +| Linux | amd64 / arm64 | `linux` | pure Go (webkit option) | GTK/WebKitGTK headers required for CGO build | + +`wails3 build -platform darwin/arm64` overrides GOOS/GOARCH. +If CGO is needed on the host that can't compile the target (e.g. building +Windows from Linux), the CLI falls back to **Docker** images (`ghcr.io/wailsapp/cross-compiler`). + +--- + +## 5. Post-Processing + +### Icons & Resources + +* **Windows** – `syso.go` merges your PNG/ICO into a `.syso` that `go build` + links automatically. Also writes `manifest.xml` enabling High-DPI support. +* **macOS** – `icons.go` turns `icon.png` β†’ `.icns`, injects into + `MyApp.app/Contents/Resources`. +* **Linux** – `.desktop` files are generated in `/usr/share/applications` + by each packager backend. + +### Code Signing (Optional) + +* macOS: `codesign` + `xcrun altool --notarize-app` +* Windows: `signtool.exe` +* Linux: Not common (repository GPG handled externally) + +Task targets: `sign:mac`, `sign:windows`. + +--- + +## 6. Packaging Back-Ends + +### Linux + +| Artefact | Code Path | Tooling | +|----------|-----------|---------| +| **AppImage** (default) | `internal/commands/appimage.go` | `linuxdeploy` + `linuxdeploy-plugin-gtk` | +| **deb / rpm / pacman** | `internal/packager` | `fpm` (invoked via Go) | + +Flags: + +``` +wails3 build -package deb # produces myapp_1.0.0_amd64.deb +wails3 build -package rpm +``` + +### macOS + +* **DMG** – `internal/commands/packager.go` calls `appdmg` to generate a + drag-&-drop installer. +* **ZIP** – fallback if `appdmg` is missing. +* Sets CFBundle identifiers and version plist automatically. + +### Windows + +* **MSI** – `internal/commands/packager.go` wraps **WiX** candle & light tools. + Heat autogenerates the component tree from the built `.exe`. + +Extra resources: + +* `internal/commands/windows_resources/` contains templated **.wxs** fragments. +* Version info injected via `rsrc` utility. + +--- + +## 7. Build Artefact Layout + +After a successful build the CLI prints: + +``` +build/bin/ +β”œβ”€β”€ myapp-linux-amd64 +└── myapp-linux-amd64.AppImage + +build/package/ +└── myapp_1.0.0_amd64.deb +``` + +The exact files depend on `-platform` and `-package` flags. + +--- + +## 8. Customising the Pipeline + +| Need | Approach | +|------|----------| +| Run a linter before build | Add a `lint` task in **Taskfile.yml** and make `build` depend on it. | +| Extra linker flags | `wails3 build -ldflags "-H windowsgui"` | +| Skip packaging | `wails3 build -skip-package` (keeps raw binary). | +| Bring your own packager | Implement `internal/packager/.go`, register in `packager.go`. | + +All Taskfile targets use environment variables exported by the CLI, so you can +reference values like `$WAILS_VERSION` or `$WAILS_PLATFORM` inside custom tasks. + +--- + +## 9. Troubleshooting + +| Symptom | Likely Cause | Fix | +|---------|--------------|-----| +| **`ld: framework not found WebKit` (mac)** | Xcode CLI tools missing | `xcode-select --install` | +| **Blank window in prod build** | Frontend build failed or SPA routing | Check `frontend/dist/index.html` exists and `History API Fallback` is set. | +| **`wixl` not found** (Linux MSI cross-build) | WiX toolset not installed in Docker image | Use `--docker` build or install WiX manually. | +| **Duplicated symbol `_WinMain`** | Mixed `windowsgui` flag and syso | Remove custom `-ldflags` or let Wails manage resources. | + +Verbose mode: `wails3 build -verbose` dumps every command executed. + +--- + +## 10. Key Source Map + +| Concern | File | +|---------|------| +| Task runner glue | `internal/commands/task_wrapper.go` | +| Build dispatcher | `internal/commands/build-assets.go` | +| AppImage builder | `internal/commands/appimage.go` | +| Generic packager interface | `internal/packager/packager.go` | +| Windows resource generator | `internal/commands/syso.go` | +| Build info injector | `internal/commands/tool_buildinfo.go` | +| Version constants | `internal/version/version.go` | + +Keep this table handy when you trace a build failure. + +--- + +You now have the full picture from **source code** to **installer**. Armed with +this knowledge you can tweak the pipeline, add new packaging targets, or debug +cross-compilation issues without fear. Happy shipping! diff --git a/docs/src/content/docs/contributing/codebase-layout.mdx b/docs/src/content/docs/contributing/codebase-layout.mdx new file mode 100644 index 000000000..74cfc7e92 --- /dev/null +++ b/docs/src/content/docs/contributing/codebase-layout.mdx @@ -0,0 +1,210 @@ +--- +title: Codebase Layout +description: How the Wails v3 repository is organised and how the pieces fit together +sidebar: + order: 2 +--- + +Wails v3 lives in a **monorepo** that contains the framework runtime, CLI, +examples, documentation, and build tool-chain. +This page walks through the *directory structure* that matters to anyone digging +into the internals. + +## Top-Level View + +``` +wails/ +β”œβ”€β”€ v3/ # ⬅️ Everything specific to Wails v3 lives here +β”œβ”€β”€ v2/ # Legacy v2 implementation (can be ignored for v3 work) +β”œβ”€β”€ docs/ # Astro-powered v3 docs site (this page!) +β”œβ”€β”€ website/ # Docusaurus v2 site and marketing pages (main site) +β”œβ”€β”€ scripts/ # Misc helper scripts (e.g. sponsor image generator) +β”œβ”€β”€ mkdocs-website/ # Deprecated v3 docs prototype +└── *.md # Project-wide meta files (CHANGELOG, LICENSE, …) +``` + +From here on, we zoom into the **`v3/`** tree. + +## `v3/` Root + +``` +v3/ +β”œβ”€β”€ cmd/ # Compilable commands (currently only wails3 CLI) +β”œβ”€β”€ examples/ # Hands-on sample apps testing every feature +β”œβ”€β”€ internal/ # Framework implementation (not public API) +β”œβ”€β”€ pkg/ # Public Go packages ‑ the API surface +β”œβ”€β”€ tasks/ # Taskfile-based release utilities +β”œβ”€β”€ templates/ # RFC-style proposals (WEP) + common assets +β”œβ”€β”€ go.mod +└── go.sum +``` + +### Mental Model + +1. **`pkg/`** exposes *what application developers import* +2. **`internal/`** contains *how the magic is implemented* +3. **`cmd/wails3`** drives *project lifecycle & builds* +4. **`examples/`** double as *living tests* and *reference code* + +Everything else supports those three pillars. + +--- + +## `cmd/` – Commands + +| Path | Notes | +|------|-------| +| `v3/cmd/wails3` | The **CLI entrypoint**. A tiny `main.go` delegates all logic to packages in `internal/commands`. | +| `internal/commands/*` | Sub-commands (init, dev, build, doctor, …). Each lives in its own file for easy discoverability. | +| `internal/commands/task_wrapper.go` | Bridges between CLI flags and the Taskfile build pipeline. | + +The CLI owns: + +* **Project scaffolding** (`init`, template generation) +* **Dev server orchestration** (`dev`, live-reload) +* **Production builds & packaging** (`build`, `package`, platform wrappers) +* **Diagnostics** (`doctor`) + +--- + +## `internal/` – The Engine Room + +``` +internal/ +β”œβ”€β”€ assetserver/ # Serving & embedding web assets +β”œβ”€β”€ buildinfo/ # Reproducible build metadata +β”œβ”€β”€ commands/ # CLI mechanics (see above) +β”œβ”€β”€ runtime/ # Desktop runtime (per-platform) +β”œβ”€β”€ generator/ # Static analysis & binding generator +β”œβ”€β”€ templates/ # Project templates (frontend stacks) +β”œβ”€β”€ packager/ # Linux AppImage/deb/rpm, Windows MSI, macOS DMG +β”œβ”€β”€ capabilities/ # Host OS capability probing +β”œβ”€β”€ dbus/ # Linux system tray integration +β”œβ”€β”€ service/ # IPC micro-services framework +└── ... # [other helper sub-packages] +``` + +### Key Sub-Packages + +| 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:
β€’ Dev: serves from disk & proxies Vite
β€’ 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`. | + +Supporting utilities (eg. `s/`, `hash/`, `flags/`) keep internal concerns decoupled. + +--- + +## `pkg/` – Public API + +``` +pkg/ +β”œβ”€β”€ application/ # Core API: windows, menus, dialogs, events, … +β”œβ”€β”€ runtime/ # JS-side helpers (bridge, events, screen, ...) +β”œβ”€β”€ options/ # Strongly-typed configuration structs +β”œβ”€β”€ menu/ # Declarative menu DSL +└── ... # Platform helpers (mac/, windows/, assetserver/, …) +``` + +`pkg/application` bootstraps a Wails program: + +```go +func main() { + app := application.New(application.Options{ + Name: "MyApp", + Assets: application.NewAssetsFS(assetsFS), + }) + window := app.NewWebviewWindow(&application.WebviewWindowOptions{ + Title: "Hello", + Width: 1024, + Height: 768, + }) + app.Run() +} +``` + +Under the hood it: + +1. Spins up `internal.runtime.*` +2. Sets up an `assetserver` +3. Registers bindings generated by `internal.generator` +4. Enters the OS main thread + +--- + +## `examples/` – Living Specs + +Each sub-folder is a **self-contained application** exercising one feature. +Patterns you’ll see mirrored in real code: + +* Binding services (`examples/binding/`) +* Multi-window (`examples/window/`) +* System tray (`examples/systray-*`) +* Packaging (`examples/file-association/`) + +When in doubt, start here and drill into implementation. + +--- + +## `templates/` – Scaffolding Blueprints + +`internal/templates` ships **base templates** (Go layout) and **frontend skins** +(React, Svelte, Vue, …). +At `wails3 init -t react`, the CLI: + +1. Copies `_common` Go files +2. Merges desired frontend pack +3. Runs `npm install` + `task deps` + +Editing templates **does not** affect existing apps, only future `init`s. + +--- + +## `tasks/` – Release Automation + +Taskfiles wrap complex cross-compilation, version bumping, and changelog +generation. They are consumed programmatically by `internal/commands/task.go` +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 +``` + +*CLI β†’ generator β†’ runtime* forms the core path from **source** to **running +desktop app**. + +--- + +## Orientation Tips + +| Need to understand… | Look at… | +|---------------------|----------| +| Platform shims | `internal/runtime/*_DARWIN.go`, `*_windows.go`, … | +| Bridge protocol | `pkg/application/messageprocessor_*.go` | +| Asset workflow | `internal/assetserver`, `v3/templates/base/assets` | +| Packaging flow | `internal/commands/appimage.go`, `internal/packager` | +| Template engine | `internal/templates/templates.go` | +| Static analysis | `internal/generator/*` | + +--- + +You now have a **mental map** of the repository. +Use it with `ripgrep`, your IDE’s β€œGo to File/Symbol”, and the example apps to +navigate deeper into any feature. Happy hacking! diff --git a/docs/src/content/docs/contributing/extending-wails.mdx b/docs/src/content/docs/contributing/extending-wails.mdx new file mode 100644 index 000000000..ffff66b77 --- /dev/null +++ b/docs/src/content/docs/contributing/extending-wails.mdx @@ -0,0 +1,246 @@ +--- +title: Extending Wails +description: Practical guide to adding new features and platforms to Wails v3 +sidebar: + order: 8 +--- + +> Wails is designed to be **hackable**. +> Every major subsystem lives in Go code you can read, modify, and ship. +> This page shows _where_ to start and _how_ to stay cross-platform when you: + +* Add a **Service** (file server, KV store, custom IPC…) +* Create a **new CLI command** (`wails3 `) +* Extend the **Runtime** (window API, dialogs, events) +* Introduce a **Platform capability** (Wayland, Apple Vision OS…) +* Keep **cross-platform compatibility** without drowning in `//go:build` tags + +--- + +## 1. Adding a Service + +Services are background Go components exposed to apps via the `application.Context`. + +``` +internal/service/ +β”œβ”€β”€ template/ # Boilerplate for new services +β”‚ β”œβ”€β”€ template.go +β”‚ └── README.md +└── service.go # Registration + lifecycle interfaces +``` + +### 1.1 Define the Service + +```go +package chat + +type Service struct { + messages []string +} + +func New() *Service { return &Service{} } + +func (s *Service) Send(msg string) string { + s.messages = append(s.messages, msg) + return "ok" +} +``` + +### 1.2 Implement `service.Service` + +```go +func (s *Service) Init(ctx *application.Context) error { return nil } +func (s *Service) Shutdown() error { return nil } +``` + +### 1.3 Register Generator + +*Add to* `internal/generator/collect/services.go` + +```go +services.Register("chat", chat.New) +``` + +> Now `wails3 generate` emits bindings so JS can call +> `window.backend.chat.Send("hi")`. + +### 1.4 Ship an Example + +Copy `v3/examples/services/` and adjust. + +--- + +## 2. Writing a New CLI Command + +CLI logic lives under `internal/commands/`. +Each command is **one file** that mounts itself in `init()`. + +### 2.1 Create the File + +`v3/internal/commands/hello.go` + +```go +package commands + +import ( + "github.com/spf13/cobra" +) + +func init() { + Add(&cobra.Command{ + Use: "hello", + Short: "Prints Hello World", + RunE: func(cmd *cobra.Command, args []string) error { + cmd.Println("Hello Wails!") + return nil + }, + }) +} +``` + +`Add()` registers with the root in `cmd/wails3/main.go`. + +### 2.2 Re-compile CLI + +``` +go install ./v3/cmd/wails3 +wails3 hello +``` + +If your command needs **Taskfile** integration, reuse helpers in +`internal/commands/task_wrapper.go`. + +--- + +## 3. Modifying the Runtime + +Common reasons: + +* New window feature (`SetOpacity`, `Shake`, …) +* Extra dialog (`ColorPicker`) +* System-level API (screen brightness) + +### 3.1 Public API + +Add to `pkg/application/window.go`: + +```go +func (w *WebviewWindow) SetOpacity(o float32) { + w.ctx.Call("window:setOpacity", o) +} +``` + +### 3.2 Message Processor + +Create `messageprocessor_window_opacity.go`: + +```go +const MsgSetOpacity = "window:setOpacity" + +func init() { register(MsgSetOpacity, handleSetOpacity) } + +func handleSetOpacity(ctx *Context, params json.RawMessage) ([]byte, error) { + var req struct { + ID uint64 `json:"id"` + Opacity float32 `json:"o"` + } + json.Unmarshal(params, &req) + return nil, runtime.SetOpacity(req.ID, req.Opacity) +} +``` + +### 3.3 Platform Implementation + +``` +internal/runtime/ + webview_window_darwin.go + webview_window_linux.go + webview_window_windows.go +``` + +Add `SetOpacity()` to each, guarded by build tags. +If a platform can’t support it, return `ErrCapability`. + +### 3.4 Capability Flag + +Expose via `internal/capabilities`. + +```go +const CapOpacity = "window:opacity" +``` + +Runtime files `*_darwin.go` etc. should `registerCapability(CapOpacity)` when +successfully initialised. + +Apps can query: + +```go +if application.HasCapability(application.CapOpacity) { ... } +``` + +--- + +## 4. Adding New Platform Capabilities + +Example: **Wayland** clipboard on Linux. + +1. Fork `internal/runtime/clipboard_linux.go`. +2. Split file: + * `clipboard_linux_x11.go` β†’ `//go:build linux && !wayland` + * `clipboard_linux_wayland.go` β†’ `//go:build linux && wayland` +3. Add CLI flag `--tags wayland` in `internal/commands/dev.go` + (`goEnv.BuildTags += ",wayland"`). +4. Document in **Asset Server** & README. + +> Keep default build tags minimal; reserve opt-in tags for niche features. + +--- + +## 5. Cross-Platform Compatibility Checklist + +| βœ… Step | Why | +|---------|-----| +| Provide **every** public method in all platform files (even if stub) | Keeps build green on all OS/arch | +| Gate platform specifics behind **capabilities** | Let apps degrade gracefully | +| Use **pure Go** first, CGO only when needed | Simplifies cross-compiles | +| Test with `task matrix:test` | Runs `go test ./...` on linux/mac/windows in Docker | +| Document new build tags in `docs/getting-started/installation.mdx` | Users must know flags | + +--- + +## 6. Debug Builds & Iteration Speed + +* `wails3 dev -tags debug` adds extra log hooks (`logger_dev*.go`). +* Use `task reload` to rebuild runtime without restarting the whole app. +* Race detector: `wails3 dev -race` (see `pkg/application/RACE.md`). + +--- + +## 7. Upstream Contributions + +1. Open an **issue** to discuss the idea & design. +2. Follow the techniques above to implement. +3. Add: + * Unit tests (`*_test.go`) + * Example (`v3/examples//`) + * Docs (this file or API reference) +4. Ensure `go vet`, `golangci-lint`, and CI matrix pass. + +--- + +### Quick Links + +| Area | Location | +|------|----------| +| Services framework | `internal/service/` | +| CLI core | `internal/commands/` | +| Runtime per-OS | `internal/runtime/` | +| Capability helpers| `internal/capabilities/` | +| Taskfile DSL | `tasks/Taskfile.yml` | +| Dev matrix tests | `tasks/events/generate.go` | + +--- + +You now have a **roadmap** for bending Wails to your willβ€”add services, sprinkle +CLI magic, hack the runtime, or bring entirely new OS features. +Happy extending! diff --git a/docs/src/content/docs/contributing/index.mdx b/docs/src/content/docs/contributing/index.mdx new file mode 100644 index 000000000..a6dc96497 --- /dev/null +++ b/docs/src/content/docs/contributing/index.mdx @@ -0,0 +1,128 @@ +--- +title: Technical Overview +description: High-level architecture and roadmap to the Wails v3 codebase +sidebar: + order: 1 +--- + +import { Card, CardGrid } from "@astrojs/starlight/components"; +import Mermaid from "../../components/Mermaid.astro"; + +## Welcome to the Wails v3 Technical Documentation + +This section is **not** about community guidelines or how to open a pull-request. +Instead, it dives into **how Wails v3 is built** so that you can quickly orient +yourself in the codebase and start hacking with confidence. + +Whether you plan to patch the runtime, extend the CLI, craft new templates, or +simply understand the internals, the pages that follow provide the technical +context you need. + +--- + +## High-Level Architecture + + + + The heart of every Wails app is Go code compiled into a native executable. + It owns application logic, system integration and performance-critical + operations. + + + + UI is written with standard web tech (React, Vue, Svelte, Vanilla, …) + rendered by a lightweight system WebView (WebKit on Linux/macOS, WebView2 on + Windows). + + + + A zero-copy, in-memory bridge enables **Go⇄JavaScript** calls with automatic + type conversion, event propagation and error forwarding. + + + + `wails3` orchestrates project creation, live-reload dev server, asset + bundling, cross-compilation and packaging (deb, rpm, AppImage, msi, dmg…). + + + +--- + +## Architectural Overview + + +```mermaid +flowchart TD + subgraph Developer Environment + CLI[wails3 CLI
Init Β· Dev Β· Build Β· Package] + end + + subgraph Build-Time + GEN[Binding System
(Static Analysis & Codegen)] + ASSET[Asset Server
(Dev Proxy Β· Embed FS)] + PKG[Build & Packaging
Pipeline] + end + + subgraph Runtime + RUNTIME[Desktop Runtime
(Window Β· Events Β· Dialogs)] + BIND[Bridge
(Message Processor)] + end + + subgraph Application + GO[Go Backend
(App Logic)] + WEB[Web Frontend
(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 +``` +
+ +The diagram shows the **end-to-end flow**: + +1. **CLI** drives generation, dev server, compilation, and packaging. +2. **Binding System** produces glue code that lets the **Web Frontend** call into the **Go Backend**. +3. During development the **Asset Server** proxies to the framework dev server; in production it serves embedded files. +4. At runtime the **Desktop Runtime** manages windows and OS APIs, while the **Bridge** shuttles messages between Go and JavaScript. + +--- + +## What This Documentation Covers + +| Topic | Why It Matters | +| ----- | -------------- | +| **Codebase Layout** | Map of `/v3` directories and how modules interact. | +| **Runtime Internals** | Window management, system APIs, message processor, and platform shims. | +| **Asset & Dev Server** | How web assets are served in dev and embedded in production. | +| **Build & Packaging Pipeline** | Taskfile-based workflow, cross-platform compilation, and installer generation. | +| **Binding System** | Static analysis pipeline that generates type-safe Go⇄TS bindings. | +| **Template System** | Generator architecture that powers `wails init -t `. | +| **Testing & CI** | Unit/integration test harness, GitHub Actions, race detector guidance. | +| **Extending Wails** | Adding services, templates, or CLI sub-commands. | + +Each subsequent page drills into these areas with concrete code samples, +diagrams, and references to the relevant source files. + +--- + +:::note +Prerequisites: You should be comfortable with **Go 1.23+**, basic TypeScript, +and modern frontend build tools. If you are new to Go, consider skimming the +official tour first. +::: + +Happy exploring β€” and welcome to the Wails v3 internals! diff --git a/docs/src/content/docs/contributing/runtime-internals.mdx b/docs/src/content/docs/contributing/runtime-internals.mdx new file mode 100644 index 000000000..aa4ae7ceb --- /dev/null +++ b/docs/src/content/docs/contributing/runtime-internals.mdx @@ -0,0 +1,175 @@ +--- +title: Runtime Internals +description: Deep-dive into how Wails v3 boots, runs, and talks to the OS +sidebar: + order: 3 +--- + +The **runtime** is the layer that transforms ordinary Go functions into a +cross-platform desktop application. +This document explains the moving parts you will meet when tracing through the +source code. + +--- + +## 1. Application Lifecycle + +| Phase | Code Path | What Happens | +|-------|-----------|--------------| +| **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.
2. Boots the **runtime** (`internal/runtime`).
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 +application object itself is initialised once. + +--- + +## 2. Window Management + +### Public API + +```go +win := app.NewWebviewWindow(&application.WebviewWindowOptions{ + Title: "Dashboard", + Width: 1280, + Height: 720, +}) +win.Show() +``` + +`NewWebviewWindow` delegates to `internal/runtime/webview_window_*.go` where +platform-specific constructors live: + +``` +internal/runtime/ +β”œβ”€β”€ webview_window_darwin.go +β”œβ”€β”€ webview_window_linux.go +└── webview_window_windows.go +``` + +Each file: + +1. Creates a native webview (WKWebView, WebKitGTK, WebView2). +2. Registers a **Message Processor** callback. +3. Maps Wails events (`WindowResized`, `Focus`, `DropFile`, …) to runtime + event IDs. + +Windows are tracked by `screenmanager.go` which offers **query APIs** (all +displays, DPI, active window) and centralises resize / move bookkeeping. + +--- + +## 3. Message Processing Pipeline + +The bridge between JavaScript and Go is implemented by the **Message +Processor** family in `pkg/application/messageprocessor_*.go`. + +Flow: + +1. **JavaScript** calls `window.runtime.Invoke("Greet", "Alice")`. +2. The runtime serialises the request: + `{"t":"c","id":"123","m":"Greet","p":["Alice"]}` + (`t` = type, `c` = call). +3. **Go** receives this JSON via the webview callback. +4. `messageprocessor_call.go` looks up the bound method in the + generated table (`application.bindings.go`) and executes it. +5. The result or error is marshalled back to JS where a `Promise` resolves. + +Specialised processors: + +| File | Purpose | +|------|---------| +| `messageprocessor_window.go` | Window actions (hide, maximize, …) | +| `messageprocessor_dialog.go` | Native dialogs (`OpenFile`, `MessageBox`, …) | +| `messageprocessor_clipboard.go` | Clipboard read/write | +| `messageprocessor_events.go` | Event subscribe / emit | +| `messageprocessor_browser.go` | Browser navigation, devtools | + +Processors are **stateless** – they pull everything they need from the +`ApplicationContext` passed with each message. + +--- + +## 4. Events System + +Events are namespaced strings dispatched across three layers: + +1. **Application events**: global lifecycle (`application:ready`, + `application:shutdown`). +2. **Window events**: per-window (`window:focus`, `window:resize`). +3. **Custom events**: user-defined (`chat:new-message`). + +Implementation details: + +* Constants are generated from `internal/events/defaults.go`. +* Go side: + `app.On("window:focus", func(e application.WindowEvent) {...})` +* JS side: + `window.runtime.EventsOn("chat:new-message", cb)` + +Under the hood both map to the same **EventBus** in +`pkg/application/events.go`. +Subscribers are reference-counted; when a window closes, its callbacks are +auto-unregistered to avoid leaks. + +--- + +## 5. Platform-Specific Implementations + +Conditional compilation keeps the public API identical while hiding OS wrinkles. + +| Concern | Darwin | Linux | Windows | +|---------|--------|-------|---------| +| Main Thread | `mainthread_darwin.go` (Cgo to Foundation) | `mainthread_linux.go` (GTK) | `mainthread_windows.go` (Win32 `AttachThreadInput`) | +| Dialogs | `dialogs_darwin.*` (NSAlert) | `dialogs_linux.go` (GtkFileChooser) | `dialogs_windows.go` (IFileOpenDialog) | +| Clipboard | `clipboard_darwin.go` | `clipboard_linux.go` | `clipboard_windows.go` | +| Tray Icons | `systemtray_darwin.*` | `systemtray_linux.go` (DBus) | `systemtray_windows.go` (Shell_NotifyIcon) | + +Key principles: + +* **No Cgo on Windows/Linux** unless unavoidable (performance, portability). +* Use **build tags** (`//go:build darwin && !production`) to keep files readable. +* Expose **capabilities** via `internal/capabilities` so higher layers can + degrade gracefully. + +--- + +## 6. File Guide + +| File | Why You’d Touch It | +|------|--------------------| +| `internal/runtime/runtime_*.go` | Change global startup logic, add debug hooks. | +| `internal/runtime/webview_window_*.go` | Implement a new window hint or behaviour. | +| `pkg/application/messageprocessor_*.go` | Add a new bridge command callable from JS. | +| `pkg/application/events_*.go` | Extend built-in event definitions. | +| `internal/assetserver/*` | Tweak dev/production asset handling. | + +--- + +## 7. Debugging Tips + +* Launch with `WAILS_LOG_LEVEL=debug` to print every message crossing the bridge. +* Use `wails3 dev -verbose` to see live reload & asset requests. +* On macOS run under `lldb --` to catch Objective-C exceptions early. +* For Windows Chromium issues, enable WebView2 debug logs: + `set WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222` + +--- + +## 8. Extending the Runtime + +1. Define a **capability flag** in `internal/capabilities`. +2. Implement the feature in each platform file using build tags. +3. Add public API in `pkg/application`. +4. Register a new message type or event if JS needs to call it. +5. Update at least one example in `v3/examples/` exercising the feature. + +Follow this checklist and you'll keep the cross-platform contract intact. + +--- + +You now have a guided tour of the runtime internals. Combine this knowledge with +the **Codebase Layout** map and the **Asset Server** docs to navigate confidently +and make impactful contributions. Happy coding! diff --git a/docs/src/content/docs/contributing/template-system.mdx b/docs/src/content/docs/contributing/template-system.mdx new file mode 100644 index 000000000..ee03824d0 --- /dev/null +++ b/docs/src/content/docs/contributing/template-system.mdx @@ -0,0 +1,222 @@ +--- +title: Template System +description: How Wails v3 scaffolds new projects, how templates are organised, and how to build your own. +sidebar: + order: 7 +--- + +Wails ships with a **template system** that lets `wails3 init` produce a ready-to-run +project for **any** modern frontend stack (React, Svelte, Vue, Solid, Vanilla +JS …). + +This page covers: + +1. Template directory layout +2. How the CLI chooses & renders templates +3. Creating a new template step-by-step +4. Updating or overriding existing templates +5. Troubleshooting and best practices + +--- + +## 1. Where Templates Live + +``` +v3/ +└── internal/templates/ + β”œβ”€β”€ _common/ # Files copied into EVERY project (main.go, .gitignore) + β”œβ”€β”€ base/ # Backend-only β€œplain Go” template + β”œβ”€β”€ react/ # React + Vite + β”œβ”€β”€ react-ts/ + β”œβ”€β”€ svelte/ + β”œβ”€β”€ vue/ + β”œβ”€β”€ ... (others) + └── templates.go # Registry + helper functions +``` + +* **`_common/`** – universal boilerplate (Go modules, Taskfile, README stub, + VS Code settings). +* **Framework folders** – contain **frontend/** and any stack-specific config. +* Folder names match the **template ID** you pass to the CLI + (`wails3 init -t react-ts`). + +> The whole directory is compiled into the CLI binary via `go:embed`, so users +> can scaffold projects offline. + +--- + +## 2. How `wails3 init` Uses Templates + +Call chain: + +``` +cmd/wails3/init.go + β”‚ + β–Ό +internal/commands/init.go ← parses flags, target dir + β”‚ + β–Ό +internal/templates.Load() ← templates.go + β”‚ + β–Ό +Template.CopyTo(dest) ← filesystem copy + text substitutions +``` + +### Substitutions + +Placeholders wrapped in `{{ }}` are replaced at copy time: + +| Placeholder | Example | Source | +|-------------|---------|--------| +| `{{ProjectName}}` | `myapp` | CLI flag/dir name | +| `{{ModulePath}}` | `github.com/me/myapp` | if `--module` provided | +| `{{WailsVersion}}`| `v3.0.0` | compiled constant | + +You can add **any** placeholder; just ensure it gets a value in +`internal/commands/init.go`. + +### Post-Copy Hooks + +Each template may ship a **Taskfile** with a `deps` task. +After copy, the CLI runs: + +``` +task --taskfile Taskfile.yml deps +``` + +Typical work: + +* `go mod tidy` +* `npm install` +* git init & initial commit (optional) + +Hook behaviour defined in `_common/Taskfile.yml` but override-able per template. + +--- + +## 3. Creating a New Template + +> Example: Add **Qwik-TS** template + +### 3.1 Folder & ID + +``` +internal/templates/qwik-ts/ +``` + +The folder name = template ID. Keep it **kebab-case**. + +### 3.2 Minimal File Set + +``` +qwik-ts/ +β”œβ”€β”€ README.md # Shown in CLI with -list +β”œβ”€β”€ frontend/ # Your web project +β”‚ β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ package.json +β”‚ └── vite.config.ts +β”œβ”€β”€ Taskfile.yml # deps & dev tasks +└── go/ # Optional extra Go files +``` + +Start by copying `react-ts` and pruning files. + +### 3.3 Update Placeholders + +Search/replace: + +* `myapp` β†’ `{{ProjectName}}` +* `github.com/you/myapp` β†’ `{{ModulePath}}` + +### 3.4 Register (optional) + +If the folder exists, **templates.go auto-detects it** via `embed`. +Only add explicit code if you need **custom validation**: + +```go +func (t *Template) Validate() error { + // check Node β‰₯ 20 for this template +} +``` + +### 3.5 Test + +```bash +wails3 init -n demo -t qwik-ts +cd demo +wails3 dev +``` + +Ensure: + +* Dev server starts +* `wailsjs` bindings generate +* Hot-reload works + +--- + +## 4. Modifying Existing Templates + +1. Edit files under `internal/templates//`. +2. Run `go generate ./v3/...` **or** just `go run ./v3/cmd/wails3 –help`; the + embed directive recompiles automatically. +3. Bump any **dependency versions** in `frontend/package.json` & `Taskfile.yml`. +4. Update `README.md` inside the template β€” users see it via + `wails3 init -t react --help`. + +### Common Tweaks + +| Task | Where | +|------|-------| +| Change dev server port | `frontend/Taskfile.yml` (`vite dev --port {{DevPort}}`) | +| Add environment variables | same Taskfile or `.env` | +| Replace JS package manager | Switch `npm` β†’ `pnpm` in Taskfile | + +--- + +## 5. Template Authoring Tips + +* **Keep frontend generic** – avoid referencing Wails-specific globals; runtime + is injected automatically. +* **No compiled artefacts** – exclude `node_modules`, `dist`, `.DS_Store` via + `.tmplignore`. +* **Document prerequisites** – Node version, extra CLI tools, etc. +* **Avoid breaking changes** – create a *new* template ID if overhaul is big. + +--- + +## 6. Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `unknown template id` | Folder name mismatch | Check `wails3 init -list` | +| Placeholders not replaced | Missing entry in `Data` map | Edit `internal/commands/init.go` | +| Dev server opens blank page | Wrong `DevPort` env | Ensure Taskfile echoes port to stdout | +| Frontend fails to build in prod | Forgotten vite `base` path | Set `base: "./"` in `vite.config.ts` | + +Add `--verbose` to `wails3 init` to print every copied file and substitution. + +--- + +## 7. Key Source File Map + +| File | Responsibility | +|------|----------------| +| `internal/templates/templates.go` | Embeds template FS, returns `[]Template` | +| `internal/templates//**` | Actual template content | +| `internal/commands/init.go` | CLI glue: picks template, fills placeholders | +| `internal/commands/generate_template.go` | Utility to *export* a live project back into a template (handy for updates) | + +--- + +## 8. Recap + +* Templates live in **`internal/templates/`** and are baked into the CLI. +* `wails3 init -t ` copies `_common` + selected template, performs + substitutions, then runs a **deps** Taskfile hook. +* Creating a template is as simple as **making a folder**, adding files, and + using placeholders. +* The system is **extensible** and **self-contained** β€” perfect for sharing + custom stacks with your team or the community. + +Happy templating! diff --git a/docs/src/content/docs/contributing/testing-ci.mdx b/docs/src/content/docs/contributing/testing-ci.mdx new file mode 100644 index 000000000..7393e818e --- /dev/null +++ b/docs/src/content/docs/contributing/testing-ci.mdx @@ -0,0 +1,216 @@ +--- +title: Testing & Continuous Integration +description: How Wails v3 ensures quality with unit tests, integration suites, race detection, and GitHub Actions CI. +sidebar: + order: 9 +--- + +Robust desktop frameworks demand rock-solid testing. +Wails v3 employs a **layered strategy**: + +| Layer | Goal | Tooling | +|-------|------|---------| +| Unit tests | Fast feedback for isolated functions | `go test ./...` | +| Integration / Example tests | Validate whole features across packages | Example apps + headless assertions | +| Race detection | Catch data races in the runtime & bridge | `go test -race`, `wails3 dev -race` | +| CI matrix | Cross-OS confidence on every PR | GitHub Actions | + +This document explains **where tests live**, **how to run them**, and **what happens in CI**. + +--- + +## 1. Directory Conventions + +``` +v3/ +β”œβ”€β”€ internal/.../_test.go # Unit tests for internal packages +β”œβ”€β”€ pkg/.../_test.go # Public API tests +β”œβ”€β”€ examples/... # Example apps double as integration tests +β”œβ”€β”€ tasks/events/... # Generated code tests run in CI +└── pkg/application/RACE.md # Race test guidance & known safe suppressions +``` + +Guidelines: + +* **Keep unit tests next to code** (`foo.go` ↔ `foo_test.go`) +* Use the **black-box style** for `pkg/` packages (`package application_test`) +* Integration helpers go in `internal/testutil/` (shared mocks, fixtures) + +--- + +## 2. Unit Tests + +### Writing Tests + +```go +func TestColourParseHex(t *testing.T) { + c, err := colour.Parse("#FF00FF") + require.NoError(t, err) + assert.Equal(t, 255, c.R) +} +``` + +Recommendations: + +* Use [`stretchr/testify`](https://github.com/stretchr/testify) – already vendor-imported. +* Follow **table-driven** style for branches. +* Stub platform quirks behind build tags (`_test_windows.go` etc.) when needed. + +### Running Locally + +```bash +cd v3 +go test ./... -cover +``` + +Taskfile shortcut: + +``` +task test # same as above + vet + lint +``` + +--- + +## 3. Integration & Example Tests + +Every folder under `v3/examples/` is a **self-contained app**. +Three techniques turn them into tests: + +| Technique | File | What it checks | +|-----------|------|----------------| +| `go test ./...` inside example | `*_test.go` spins up `wails dev` in headless mode | Build & startup | +| CLI golden tests | `internal/commands/appimage_test.go` | Generated artefact hashes | +| Scripted e2e | `tasks/events/generate.go` | Emits Go that launches example, asserts runtime logs | + +Run all with: + +``` +task integration +``` + +> Headless mode uses **Xvfb** on Linux runners; macOS & Windows start minimized. + +--- + +## 4. Race Detection + +Data races are fatal in GUI runtimes. + +### Dedicated Doc + +See `v3/pkg/application/RACE.md` for: + +* Known benign races and suppression rationale +* How to interpret stack-traces crossing Cgo boundaries + +### Local Race Suite + +``` +go test -race ./... +``` + +or + +``` +wails3 dev -race +``` + +*The latter rebuilds runtime with race flags and launches a demo window. +Close the window; any races print after exit.* + +CI runs the race suite on **linux/amd64** (fastest) for every PR. + +--- + +## 5. GitHub Actions Workflows + +### 5.1 `build-and-test-v3.yml` + +Location: `.github/workflows/build-and-test-v3.yml` + +Key features: + +* **Matrix** over `os: [ubuntu-latest, windows-latest, macos-latest]` +* Caches Go & npm deps for speed +* Steps: + 1. `setup-go` @ 1.23 + 2. `go vet ./...` + 3. `go test ./... -v -coverprofile=cover.out` + 4. Build CLI + sample app (`wails3 build -skip-package`) + 5. Upload coverage to Codecov +* Conditional **race** job on Ubuntu: + ```yaml + - name: Race Detector + if: matrix.os == 'ubuntu-latest' + run: go test -race ./... + ``` + +### 5.2 Static Analysis + +* `semgrep.yml` – security/lint scanning (`.github/workflows/semgrep.yml`) +* `qodana.yaml` – JetBrains Qodana cloud inspection (optional in PR) + +### 5.3 Release Pipeline + +`runtime.yml` builds and publishes the JS runtime to npm on tag push; out of testing scope but referenced by build matrix. + +--- + +## 6. Local CI Parity + +Run the **exact** CI task set via Taskfile: + +``` +task ci +``` + +It executes: + +1. `lint` (golangci-lint, govet) +2. `test` (unit + race) +3. `integration` +4. `build` (for host platform) + +--- + +## 7. Troubleshooting Failing Tests + +| Symptom | Likely Cause | Fix | +|---------|--------------|-----| +| **Race in `runtime_darwin.go`** | Forgot to lock `mainthread.Call` | Use `mainthread.Sync` helper | +| **Windows headless hangs** | Console window waiting for input | Pass `-verbose` and ensure no dialogs open | +| **CI example build fails** | Template changed without bumping example deps | `task update-examples` regenerates lockfiles | +| **Coverpkg errors** | Integration test importing `main` | Switch to build tags `//go:build integration` | + +--- + +## 8. Adding New Tests + +1. **Unit** – create `*_test.go`, run `go test ./...` +2. **Integration** – update or add example app + test harness +3. **CI** – commit; the workflow auto-discovers tests + If OS-specific, add skip tags: + +```go +//go:build !windows +``` + +--- + +## 9. Key File Map + +| What | Path | +|------|------| +| Unit test example | `internal/colour/colour_test.go` | +| Integration harness | `internal/commands/appimage_test.go` | +| Race guide | `pkg/application/RACE.md` | +| Taskfile test targets | `v3/Taskfile.yaml` | +| CI workflow | `.github/workflows/build-and-test-v3.yml` | + +--- + +Quality isn’t an afterthought in Wails v3. +With unit tests, integration suites, race detection, and a cross-platform CI +matrix you can contribute confidently, knowing your changes run green on every +OS we support. +Happy testing! diff --git a/docs/src/content/docs/guides/cli.mdx b/docs/src/content/docs/guides/cli.mdx index 89035e7e5..97206ec14 100644 --- a/docs/src/content/docs/guides/cli.mdx +++ b/docs/src/content/docs/guides/cli.mdx @@ -174,13 +174,13 @@ wails3 generate icons [flags] ``` #### Flags -| Flag | Description | Default | -|------------|------------------------------|-----------------------| -| `-input` | Input PNG file | Required | -| `-windows` | Windows output filename | | -| `-mac` | macOS output filename | | -| `-sizes` | Icon sizes (comma-separated) | `256,128,64,48,32,16` | -| `-example` | Generate example icon | `false` | +| Flag | Description | Default | +|--------------------|------------------------------|-----------------------| +| `-input` | Input PNG file | Required | +| `-windowsfilename` | Windows output filename | | +| `-macfilename` | macOS output filename | | +| `-sizes` | Icon sizes (comma-separated) | `256,128,64,48,32,16` | +| `-example` | Generate example icon | `false` | ### `generate syso` Generates Windows .syso file. @@ -325,6 +325,32 @@ Shows build information about the application. wails3 tool buildinfo ``` +### `tool version` +Bumps a semantic version based on the provided flags. + +```bash +wails3 tool version [flags] +``` + +#### Flags +| Flag | Description | Default | +|---------------|--------------------------------------------------|---------| +| `-v` | Current version to bump | | +| `-major` | Bump major version | `false` | +| `-minor` | Bump minor version | `false` | +| `-patch` | Bump patch version | `false` | +| `-prerelease` | Bump prerelease version (e.g., alpha.5 to alpha.6) | `false` | + +The command follows the precedence order: major > minor > patch > prerelease. It preserves the "v" prefix if present in the input version, as well as any prerelease and metadata components. + +Example usage: +```bash +wails3 tool version -v 1.2.3 -major # Output: 2.0.0 +wails3 tool version -v v1.2.3 -minor # Output: v1.3.0 +wails3 tool version -v 1.2.3-alpha -patch # Output: 1.2.4-alpha +wails3 tool version -v v3.0.0-alpha.5 -prerelease # Output: v3.0.0-alpha.6 +``` + ### `tool package` Generates Linux packages (deb, rpm, archlinux). diff --git a/docs/src/content/docs/guides/custom-protocol-association.mdx b/docs/src/content/docs/guides/custom-protocol-association.mdx new file mode 100644 index 000000000..4d5bf1eca --- /dev/null +++ b/docs/src/content/docs/guides/custom-protocol-association.mdx @@ -0,0 +1,167 @@ +--- +title: Custom Protocol Schemes (Deep Linking) +description: Guide to implementing custom URL schemes for deep linking in Wails applications across macOS, Windows, and Linux. +--- + +import { Aside } from '@astrojs/starlight/components'; + +# Custom Protocol Schemes (Deep Linking) + +Custom protocol schemes (also known as custom URL schemes or deep linking) allow your Wails application to be launched or brought to the foreground by clicking a URL with a scheme you define (e.g., `myapp://some/data`). This is useful for various purposes, such as: + +- OAuth authentication flows. +- Inter-application communication. +- Launching your app with a specific context or to perform a particular action. + +Wails provides a unified way to handle these custom URL invocations across macOS, Windows, and Linux through the `events.Common.ApplicationLaunchedWithUrl` event. + +## Defining Your Protocols + +First, you need to define the custom protocol schemes your application will use. This is done in your `wails.json` project configuration file. Wails reads this file during the build process (`wails build`) to configure the necessary platform-specific assets like `Info.plist` for macOS, NSIS installer scripts for Windows, and `.desktop` files for Linux. + +**Example: `wails.json`** + +```json title="wails.json" +{ + "name": "My App", + "description": "An amazing Wails app!", + "info": { + "companyName": "My Company", + "productName": "My Product", + // ... other info fields ... + "protocols": [ + { + "scheme": "myapp", + "description": "My Application Custom Protocol" + }, + { + "scheme": "anotherprotocol", + "description": "Another protocol for specific actions" + } + ] + } + // ... other wails.json fields ... +} +``` + +This `info.protocols` array is what Wails uses to generate the necessary entries in platform-specific files. For example, in template files, you might access this via a path like `{{.Info.Protocols}}`. + + + +## Handling the Event in Your Application + +When your application is launched or activated via a custom URL, Wails emits an `events.Common.ApplicationLaunchedWithUrl` event. You can listen for this event and retrieve the URL that triggered the launch. + +```go title="main.go" +import ( + "log" + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/events" +) + +func main() { + app := application.New(application.Options{ + Name: "My App", // Ensure this matches relevant info from wails.json if needed + Description: "An amazing Wails app!", + // ... other runtime options ... + }) + + app.OnApplicationEvent(events.Common.ApplicationLaunchedWithUrl, func(e *application.ApplicationEvent) { + launchedURL := e.Context().URL() // Retrieve the URL from the event context + log.Printf("Application launched with URL: %s", launchedURL) + + // TODO: Process the URL (e.g., navigate, perform action, etc.) + // Example: app.EmitEvent("frontend:ShowURL", launchedURL) + }) + + // ... rest of your main function ... + err := app.Run() + if err != nil { + log.Fatal(err) + } +} +``` + + + +## Platform-Specific Setup and Behavior + +While Wails aims for a unified event, the underlying mechanism for custom protocol registration and URL delivery varies by operating system. + +### macOS + +- **Setup:** Wails automatically configures your application's `Info.plist` file during the build process. It adds `CFBundleURLTypes` entries based on the `info.protocols` defined in your `wails.json` file. + ```xml title="Info.plist (excerpt generated by Wails)" + CFBundleURLTypes + + + CFBundleURLName + My Application Custom Protocol + CFBundleURLSchemes + + myapp + + + + + ``` +- **How it Works:** When a URL like `myapp://` is opened, macOS uses LaunchServices to find the application registered for that scheme and sends it an Apple Event (`kAEGetURL`). Wails intercepts this event and translates it into the common `events.Common.ApplicationLaunchedWithUrl` Wails event, providing the URL via `e.Context().URL()`. + +### Windows + +- **Setup:** Custom protocol schemes on Windows are registered in the Windows Registry. Wails facilitates this through its NSIS installer template. + - When you build your application with the `-nsis` flag, Wails uses the `v3/internal/commands/updatable_build_assets/windows/nsis/wails_tools.nsh.tmpl` file. + - This template contains macros like `CUSTOM_PROTOCOL_ASSOCIATE` and `wails.associateCustomProtocols` which use the `info.protocols` from your `wails.json` (passed as `{{.Info.Protocols}}` to the template) to create the necessary registry entries during installation. + ```nsis title="wails_tools.nsh.tmpl (excerpt)" + !macro wails.associateCustomProtocols + ; Create custom protocols associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + {{end}} + !macroend + ``` +- **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. + + + +### Linux + +- **Setup:** On Linux, custom protocol handling is typically managed via `.desktop` files and the MIME type system. + - Wails uses a `.desktop` file template (e.g., `v3/internal/commands/updatable_build_assets/linux/desktop.tmpl`) which is populated during the build using information from `wails.json`. + ```desktop title="desktop.tmpl (excerpt)" + [Desktop Entry] + Name={{.ProductName}} + Exec=/usr/local/bin/{{.BinaryName}} %u + MimeType={{range $index, $protocol := .Info.Protocols}}x-scheme-handler/{{$protocol.Scheme}};{{end}} + ``` + The `Exec` line uses `%u` which gets replaced by the URL. The `MimeType` line registers your application as a handler for `x-scheme-handler/your-scheme` for each protocol defined in `wails.json` (via `{{.Info.Protocols}}`). + - When packaging for Linux (e.g., using `nfpm`), this `.desktop` file is installed to `/usr/share/applications/`. + - A `postinstall.sh` script (e.g., `v3/internal/commands/build_assets/linux/nfpm/scripts/postinstall.sh`) is used to update the system's application and MIME databases: + ```sh title="postinstall.sh (excerpt)" + #!/bin/sh + update-desktop-database -q /usr/share/applications + update-mime-database -n /usr/share/mime + ``` +- **How it Works:** When a URL like `myapp://` is opened, the desktop environment uses the MIME database to find the associated `.desktop` file and executes the command specified in its `Exec` line, substituting `%u` with the URL. Your application receives this URL as a command-line argument. + - The Wails runtime for Linux (`v3/pkg/application/application_linux.go`) checks `os.Args` on startup. If it detects an argument that looks like a URL, it emits the `events.Common.ApplicationLaunchedWithUrl` event. + +## Testing Your Custom Protocols + +- **macOS:** Open Terminal and type `open "your-scheme://your/data"`. +- **Linux:** Open a terminal and type `xdg-open "your-scheme://your/data"` (requires `xdg-utils` to be installed and the app to be properly packaged and registered). +- **Windows:** After installation via NSIS: + - You can try running `start your-scheme://your/data` from Command Prompt or PowerShell. + - Create a simple HTML file with a link `Test Link` and open it in a browser. + + + +By following this guide, you can effectively use custom protocol schemes to enhance your Wails application's interactivity and integration with other applications or web services. diff --git a/docs/src/content/docs/guides/events-reference.mdx b/docs/src/content/docs/guides/events-reference.mdx new file mode 100644 index 000000000..c03f5408c --- /dev/null +++ b/docs/src/content/docs/guides/events-reference.mdx @@ -0,0 +1,400 @@ +--- +title: Events Guide +description: A practical guide to using events in Wails v3 for application communication and lifecycle management +--- + +**NOTE: This guide is a work in progress** + +# Events Guide + +Events are the heartbeat of communication in Wails applications. They allow different parts of your application to talk to each other without being tightly coupled. This guide will walk you through everything you need to know about using events effectively in your Wails application. + +## Understanding Wails Events + +Think of events as messages that get broadcast throughout your application. Any part of your application can listen for these messages and react accordingly. This is particularly useful for: + +- **Responding to window changes**: Know when your window is minimized, maximized, or moved +- **Handling system events**: React to theme changes or power events +- **Custom application logic**: Create your own events for features like data updates or user actions +- **Cross-component communication**: Let different parts of your app communicate without direct dependencies + +## Event Naming Convention + +All Wails events follow a namespace pattern to clearly indicate their origin: + +- `common:` - Cross-platform events that work on Windows, macOS, and Linux +- `windows:` - Windows-specific events +- `mac:` - macOS-specific events +- `linux:` - Linux-specific events + +For example: +- `common:WindowFocus` - Window gained focus (works everywhere) +- `windows:APMSuspend` - System is suspending (Windows only) +- `mac:ApplicationDidBecomeActive` - App became active (macOS only) + +## Getting Started with Events + +### Listening to Events (Frontend) + +The most common use case is listening for events in your frontend code: + +```javascript +import { Events } from '@wailsio/runtime'; + +// Listen for when the window gains focus +Events.On('common:WindowFocus', () => { + console.log('Window is now focused!'); + // Maybe refresh some data or resume animations +}); + +// Listen for theme changes +Events.On('common:ThemeChanged', (event) => { + console.log('Theme changed:', event.data); + // Update your app's theme accordingly +}); + +// Listen for custom events from your Go backend +Events.On('my-app:data-updated', (event) => { + console.log('Data updated:', event.data); + // Update your UI with the new data +}); +``` + +### Emitting Events (Backend) + +From your Go code, you can emit events that your frontend can listen to: + +```go +package main + +import ( + "github.com/wailsapp/wails/v3/pkg/events" + "github.com/wailsapp/wails/v3/pkg/application" +) + +func (a *App) UpdateData() { + // Do some data processing... + + // Notify the frontend + a.app.EmitEvent("my-app:data-updated", map[string]interface{}{ + "timestamp": time.Now(), + "count": 42, + }) +} +``` + +### Removing Event Listeners + +Always clean up your event listeners when they're no longer needed: + +```javascript +// Store the handler reference +const focusHandler = () => { + console.log('Window focused'); +}; + +// Add the listener +Events.On('common:WindowFocus', focusHandler); + +// Later, remove it when no longer needed +Events.Off('common:WindowFocus', focusHandler); + +// Or remove all listeners for an event +Events.Off('common:WindowFocus'); +``` + +## Common Use Cases + +### 1. Pause/Resume on Window Focus + +Many applications need to pause certain activities when the window loses focus: + +```javascript +let animationRunning = true; + +Events.On('common:WindowLostFocus', () => { + animationRunning = false; + pauseBackgroundTasks(); +}); + +Events.On('common:WindowFocus', () => { + animationRunning = true; + resumeBackgroundTasks(); +}); +``` + +### 2. Responding to Theme Changes + +Keep your app in sync with the system theme: + +```javascript +Events.On('common:ThemeChanged', (event) => { + const isDarkMode = event.data.isDark; + + if (isDarkMode) { + document.body.classList.add('dark-theme'); + document.body.classList.remove('light-theme'); + } else { + document.body.classList.add('light-theme'); + document.body.classList.remove('dark-theme'); + } +}); +``` + +### 3. Handling File Drops + +Make your app accept dragged files: + +```javascript +Events.On('common:WindowFilesDropped', (event) => { + const files = event.data.files; + + files.forEach(file => { + console.log('File dropped:', file); + // Process the dropped files + handleFileUpload(file); + }); +}); +``` + +### 4. Window Lifecycle Management + +Respond to window state changes: + +```javascript +Events.On('common:WindowClosing', () => { + // Save user data before closing + saveApplicationState(); + + // You could also prevent closing by returning false + // from a registered window close handler +}); + +Events.On('common:WindowMaximise', () => { + // Adjust UI for maximized view + adjustLayoutForMaximized(); +}); + +Events.On('common:WindowRestore', () => { + // Return UI to normal state + adjustLayoutForNormal(); +}); +``` + +### 5. Platform-Specific Features + +Handle platform-specific events when needed: + +```javascript +// Windows-specific power management +Events.On('windows:APMSuspend', () => { + console.log('System is going to sleep'); + saveState(); +}); + +Events.On('windows:APMResumeSuspend', () => { + console.log('System woke up'); + refreshData(); +}); + +// macOS-specific app lifecycle +Events.On('mac:ApplicationWillTerminate', () => { + console.log('App is about to quit'); + performCleanup(); +}); +``` + +## Creating Custom Events + +You can create your own events for application-specific needs: + +### Backend (Go) + +```go +// Emit a custom event when data changes +func (a *App) ProcessUserData(userData UserData) error { + // Process the data... + + // Notify all listeners + a.app.EmitEvent("user:data-processed", map[string]interface{}{ + "userId": userData.ID, + "status": "completed", + "timestamp": time.Now(), + }) + + return nil +} + +// Emit periodic updates +func (a *App) StartMonitoring() { + ticker := time.NewTicker(5 * time.Second) + go func() { + for range ticker.C { + stats := a.collectStats() + a.app.EmitEvent("monitor:stats-updated", stats) + } + }() +} +``` + +### Frontend (JavaScript) + +```javascript +// Listen for your custom events +Events.On('user:data-processed', (event) => { + const { userId, status, timestamp } = event.data; + + showNotification(`User ${userId} processing ${status}`); + updateUIWithNewData(); +}); + +Events.On('monitor:stats-updated', (event) => { + updateDashboard(event.data); +}); +``` + +## Event Reference + +### Common Events (Cross-platform) + +These events work on all platforms: + +| Event | Description | When to Use | +|-------|-------------|-------------| +| `common:ApplicationStarted` | Application has fully started | Initialize your app, load saved state | +| `common:WindowRuntimeReady` | Wails runtime is ready | Start making Wails API calls | +| `common:ThemeChanged` | System theme changed | Update app appearance | +| `common:WindowFocus` | Window gained focus | Resume activities, refresh data | +| `common:WindowLostFocus` | Window lost focus | Pause activities, save state | +| `common:WindowMinimise` | Window was minimized | Pause rendering, reduce resource usage | +| `common:WindowMaximise` | Window was maximized | Adjust layout for full screen | +| `common:WindowRestore` | Window restored from min/max | Return to normal layout | +| `common:WindowClosing` | Window is about to close | Save data, cleanup resources | +| `common:WindowFilesDropped` | Files dropped on window | Handle file imports | +| `common:WindowDidResize` | Window was resized | Adjust layout, rerender charts | +| `common:WindowDidMove` | Window was moved | Update position-dependent features | + +### Platform-Specific Events + +#### Windows Events + +Key events for Windows applications: + +| Event | Description | Use Case | +|-------|-------------|----------| +| `windows:SystemThemeChanged` | Windows theme changed | Update app colors | +| `windows:APMSuspend` | System suspending | Save state, pause operations | +| `windows:APMResumeSuspend` | System resumed | Restore state, refresh data | +| `windows:APMPowerStatusChange` | Power status changed | Adjust performance settings | + +#### macOS Events + +Important macOS application events: + +| Event | Description | Use Case | +|-------|-------------|----------| +| `mac:ApplicationDidBecomeActive` | App became active | Resume operations | +| `mac:ApplicationDidResignActive` | App became inactive | Pause operations | +| `mac:ApplicationWillTerminate` | App will quit | Final cleanup | +| `mac:WindowDidEnterFullScreen` | Entered fullscreen | Adjust UI for fullscreen | +| `mac:WindowDidExitFullScreen` | Exited fullscreen | Restore normal UI | + +#### Linux Events + +Core Linux window events: + +| Event | Description | Use Case | +|-------|-------------|----------| +| `linux:SystemThemeChanged` | Desktop theme changed | Update app theme | +| `linux:WindowFocusIn` | Window gained focus | Resume activities | +| `linux:WindowFocusOut` | Window lost focus | Pause activities | + +## Best Practices + +### 1. Use Event Namespaces + +When creating custom events, use namespaces to avoid conflicts: + +```javascript +// Good - namespaced events +Events.Emit('myapp:user:login'); +Events.Emit('myapp:data:updated'); +Events.Emit('myapp:network:connected'); + +// Avoid - generic names that might conflict +Events.Emit('login'); +Events.Emit('update'); +``` + +### 2. Clean Up Listeners + +Always remove event listeners when components unmount: + +```javascript +// React example +useEffect(() => { + const handler = (event) => { + // Handle event + }; + + Events.On('common:WindowResize', handler); + + // Cleanup + return () => { + Events.Off('common:WindowResize', handler); + }; +}, []); +``` + +### 3. Handle Platform Differences + +Check platform availability when using platform-specific events: + +```javascript +import { Platform } from '@wailsio/runtime'; + +if (Platform.isWindows) { + Events.On('windows:APMSuspend', handleSuspend); +} else if (Platform.isMac) { + Events.On('mac:ApplicationWillTerminate', handleTerminate); +} +``` + +### 4. Don't Overuse Events + +While events are powerful, don't use them for everything: + +- βœ… Use events for: System notifications, lifecycle changes, broadcast updates +- ❌ Avoid events for: Direct function returns, single component updates, synchronous operations + +## Debugging Events + +To debug event issues: + +```javascript +// Log all events (development only) +if (isDevelopment) { + const originalOn = Events.On; + Events.On = function(eventName, handler) { + console.log(`[Event Registered] ${eventName}`); + return originalOn.call(this, eventName, function(event) { + console.log(`[Event Fired] ${eventName}`, event); + return handler(event); + }); + }; +} +``` + +## Source of Truth + +The complete list of available events can be found in the Wails source code: +- Frontend events: [`v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.ts`](https://github.com/wailsapp/wails/blob/main/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.ts) +- Backend events: [`v3/pkg/events/events.go`](https://github.com/wailsapp/wails/blob/main/v3/pkg/events/events.go) + +Always refer to these files for the most up-to-date event names and availability. + +## Summary + +Events in Wails provide a powerful, decoupled way to handle communication in your application. By following the patterns and practices in this guide, you can build responsive, platform-aware applications that react smoothly to system changes and user interactions. + +Remember: start with common events for cross-platform compatibility, add platform-specific events when needed, and always clean up your event listeners to prevent memory leaks. \ No newline at end of file diff --git a/docs/src/content/docs/guides/file-associations.mdx b/docs/src/content/docs/guides/file-associations.mdx index 3394b576f..eebc29364 100644 --- a/docs/src/content/docs/guides/file-associations.mdx +++ b/docs/src/content/docs/guides/file-associations.mdx @@ -62,6 +62,7 @@ fileAssociations: | description | Description shown in file properties | Windows | | iconName | Name of the icon file (without extension) in the build folder | All | | role | Application's role for this file type (e.g., `Editor`, `Viewer`) | macOS | +| mimeType | MIME type for the file (e.g., `image/jpeg`) | macOS | ## Listening for File Open Events @@ -105,6 +106,8 @@ Let's walk through setting up file associations for a simple text editor: Run `wails3 generate icons --help` for more information. ::: + - For macOS add copy statement like `cp build/darwin/documenticon.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources` in the `create:app:bundle:` task. + 2. ### Configure File Associations Edit the `build/config.yml` file to add your file associations: diff --git a/docs/src/content/docs/guides/gin-routing.mdx b/docs/src/content/docs/guides/gin-routing.mdx new file mode 100644 index 000000000..b9f59cb77 --- /dev/null +++ b/docs/src/content/docs/guides/gin-routing.mdx @@ -0,0 +1,584 @@ +--- +title: Using Gin for Routing +description: A comprehensive guide to integrating Gin web framework with Wails v3 applications +--- + +This guide demonstrates how to integrate the [Gin web framework](https://github.com/gin-gonic/gin) with Wails v3. Gin is a high-performance HTTP web framework written in Go that makes it easy to build web applications and APIs. + +## Introduction + +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 + +## Setting Up Gin with Wails + +To integrate Gin with Wails, you need to create a Gin router and configure it as the asset handler in your Wails application. Here's a step-by-step guide: + +### 1. Install Dependencies + +First, ensure you have the Gin package installed: + +```bash +go get -u github.com/gin-gonic/gin +``` + +### 2. Create a Middleware for Gin + +Create a middleware function that will handle the integration between Wails and Gin: + +```go +// 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 strings.HasPrefix(r.URL.Path, "/wails") { + next.ServeHTTP(w, r) + return + } + // Let Gin handle everything else + ginEngine.ServeHTTP(w, r) + }) + } +} +``` + +This middleware passes all HTTP requests to the Gin router. + +### 3. Configure Your Gin Router + +Set up your Gin router with routes, middlewares, and handlers: + +```go +// Create a new Gin router +ginEngine := gin.New() // Using New() instead of Default() to add custom middleware + +// Add middlewares +ginEngine.Use(gin.Recovery()) +ginEngine.Use(LoggingMiddleware()) // Your custom middleware + +// Define routes +ginEngine.GET("/", func(c *gin.Context) { + // Serve your main page +}) + +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), + }) +}) +``` + +### 4. Integrate with Wails Application + +Configure your Wails application to use the Gin router as its asset handler: + +```go +// 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), + }, +}) + +// Create window +app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + Title: "Wails + Gin Example", + Width: 900, + Height: 700, + Centered: true, + URL: "/", // This will load the route handled by Gin +}) +``` + +## 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: + +```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 + } + 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") +}) +``` + +## Custom Middleware + +Gin allows you to create custom middleware for various purposes. Here's an example of a logging middleware: + +```go +// 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, + ) + } +} +``` + +## Handling API Requests + +Gin makes it easy to create RESTful APIs. Here's how to define API endpoints: + +```go +// GET endpoint +ginEngine.GET("/api/users", func(c *gin.Context) { + c.JSON(http.StatusOK, users) +}) + +// POST endpoint with JSON binding +ginEngine.POST("/api/users", func(c *gin.Context) { + var newUser User + if err := c.ShouldBindJSON(&newUser); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + // Process the new user... + c.JSON(http.StatusCreated, newUser) +}) + +// Path parameters +ginEngine.GET("/api/users/:id", func(c *gin.Context) { + id := c.Param("id") + // Find user by ID... + c.JSON(http.StatusOK, user) +}) + +// Query parameters +ginEngine.GET("/api/search", func(c *gin.Context) { + query := c.DefaultQuery("q", "") + limit := c.DefaultQuery("limit", "10") + // Perform search... + c.JSON(http.StatusOK, results) +}) +``` + +## Event Communication + +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. + +### Serving the Wails Runtime + +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: + +```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.EmitEvent(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 + + + + + + + +

+    
+    
+
+
+```
+
+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.OnEvent("my-event", func(event *application.CustomEvent) {
+    log.Printf("Received event from frontend: %v", event.Data)
+    // Process the event...
+})
+```
+
+### Emitting Events from JavaScript
+
+```html
+    
+```
+
+## Advanced Configuration
+
+### Customising Gin's Mode
+
+Gin has three modes: debug, release, and test. For production applications, you should use release mode:
+
+```go
+// Set Gin to release mode
+gin.SetMode(gin.ReleaseMode)
+
+// Create a new Gin router
+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:
+
+```go
+import "github.com/gorilla/websocket"
+
+var upgrader = websocket.Upgrader{
+    ReadBufferSize:  1024,
+    WriteBufferSize: 1024,
+    CheckOrigin: func(r *http.Request) bool {
+        return true // Allow all connections
+    },
+}
+
+// In your route handler:
+ginEngine.GET("/ws", func(c *gin.Context) {
+    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
+    if err != nil {
+        log.Println(err)
+        return
+    }
+    defer conn.Close()
+    
+    // Handle WebSocket connection...
+})
+```
+
+## 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.OnEvent("gin-button-clicked", func(event *application.CustomEvent) {
+		log.Printf("Received event from frontend: %v", event.Data)
+	})
+
+	// Create window
+	app.NewWebviewWindowWithOptions(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.
+- **Separate Concerns:** Keep your API logic separate from your UI logic.
+- **Error Handling:** Implement proper error handling in both Gin routes and frontend code.
+- **Security:** Be mindful of security considerations, especially when handling user input.
+- **Performance:** Use Gin's release mode in production for better performance.
+- **Testing:** Write tests for your Gin routes using Gin's testing utilities.
+
+## 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.
+
+For more information, refer to the [Gin documentation](https://github.com/gin-gonic/gin) and the [Wails documentation](https://wails.io).
diff --git a/docs/src/content/docs/guides/gin-services.mdx b/docs/src/content/docs/guides/gin-services.mdx
new file mode 100644
index 000000000..73c56afba
--- /dev/null
+++ b/docs/src/content/docs/guides/gin-services.mdx
@@ -0,0 +1,552 @@
+---
+title: Using Gin for Services
+description: A guide to integrating the Gin web framework with Wails v3 Services
+---
+
+# Using Gin for Services
+
+The Gin web framework is a popular choice for building HTTP services in Go. With Wails v3, you can easily integrate Gin-based services into your application, providing a powerful way to handle HTTP requests, implement RESTful APIs, and serve web content.
+
+This guide will walk you through creating a Gin-based service that can be mounted at a specific route in your Wails application. We'll build a complete example that demonstrates how to:
+
+1. Create a Gin-based service
+2. Implement the Wails Service interface
+3. Set up routes and middleware
+4. Integrate with the Wails event system
+5. Interact with the service from the frontend
+
+## Prerequisites
+
+Before you begin, make sure you have:
+
+- Wails v3 installed
+- Basic knowledge of Go and the Gin framework
+- Familiarity with HTTP concepts and RESTful APIs
+
+You'll need to add the Gin framework to your project:
+
+```bash
+go get github.com/gin-gonic/gin
+```
+
+## Creating a Gin-Based Service
+
+Let's start by creating a Gin service that implements the Wails Service interface. Our service will manage a collection of users and provide API endpoints for retrieving and creating user records.
+
+### 1. Define Your Data Models
+
+First, define the data structures your service will work with:
+
+```go
+package services
+
+import (
+	"context"
+	"net/http"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	"github.com/wailsapp/wails/v3/pkg/application"
+)
+
+// User represents a user in the system
+type User struct {
+	ID        int       `json:"id"`
+	Name      string    `json:"name"`
+	Email     string    `json:"email"`
+	CreatedAt time.Time `json:"createdAt"`
+}
+
+// EventData represents data sent in events
+type EventData struct {
+	Message   string `json:"message"`
+	Timestamp string `json:"timestamp"`
+}
+```
+
+### 2. Create Your Service Structure
+
+Next, define the service structure that will hold your Gin router and any state your service needs to maintain:
+
+```go
+// GinService implements a Wails service that uses Gin for HTTP handling
+type GinService struct {
+	ginEngine *gin.Engine
+	users     []User
+	nextID    int
+	mu        sync.RWMutex
+	app       *application.App
+}
+
+// NewGinService creates a new GinService instance
+func NewGinService() *GinService {
+	// Create a new Gin router
+	ginEngine := gin.New()
+
+	// Add middlewares
+	ginEngine.Use(gin.Recovery())
+	ginEngine.Use(LoggingMiddleware())
+
+	service := &GinService{
+		ginEngine: ginEngine,
+		users: []User{
+			{ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now().Add(-72 * time.Hour)},
+			{ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now().Add(-48 * time.Hour)},
+			{ID: 3, Name: "Charlie", Email: "charlie@example.com", CreatedAt: time.Now().Add(-24 * time.Hour)},
+		},
+		nextID: 4,
+	}
+
+	// Define routes
+	service.setupRoutes()
+
+	return service
+}
+```
+
+### 3. Implement the Service Interface
+
+Implement the required methods for the Wails Service interface:
+
+```go
+// ServiceName returns the name of the service
+func (s *GinService) ServiceName() string {
+	return "Gin API Service"
+}
+
+// ServiceStartup is called when the service starts
+func (s *GinService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
+	// Store the application instance for later use
+	s.app = application.Get()
+
+	// Register an event handler that can be triggered from the frontend
+	s.app.OnEvent("gin-api-event", func(event *application.CustomEvent) {
+		// Log the event data
+		s.app.Logger.Info("Received event from frontend", "data", event.Data)
+
+		// Emit an event back to the frontend
+		s.app.EmitEvent("gin-api-response", map[string]interface{}{
+			"message": "Response from Gin API Service",
+			"time":    time.Now().Format(time.RFC3339),
+		})
+	})
+
+	return nil
+}
+
+// ServiceShutdown is called when the service shuts down
+func (s *GinService) ServiceShutdown(ctx context.Context) error {
+	// Clean up resources if needed
+	return nil
+}
+```
+
+### 3. Implement the http.Handler Interface
+
+To make your service mountable at a specific route, implement the `http.Handler` interface. This single method, `ServeHTTP`, is the gateway for all HTTP requests to your service. It delegates the request handling to the Gin router, allowing you to use all of Gin's powerful features for routing and middleware.
+
+```go
+// ServeHTTP implements the http.Handler interface
+func (s *GinService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// All requests go to the Gin router
+	s.ginEngine.ServeHTTP(w, r)
+}
+```
+
+### 4. Set Up Your Routes
+
+Define your API routes in a separate method for better organisation. This approach keeps your code clean and makes it easier to understand the structure of your API. The Gin router provides a fluent API for defining routes, including support for route groups, which help organise related endpoints.
+
+```go
+// setupRoutes configures the API routes
+func (s *GinService) setupRoutes() {
+	// Basic info endpoint
+	s.ginEngine.GET("/info", func(c *gin.Context) {
+		c.JSON(http.StatusOK, gin.H{
+			"service": "Gin API Service",
+			"version": "1.0.0",
+			"time":    time.Now().Format(time.RFC3339),
+		})
+	})
+
+	// Users group
+	users := s.ginEngine.Group("/users")
+	{
+		// Get all users
+		users.GET("", func(c *gin.Context) {
+			s.mu.RLock()
+			defer s.mu.RUnlock()
+			c.JSON(http.StatusOK, s.users)
+		})
+
+		// Get user by ID
+		users.GET("/:id", func(c *gin.Context) {
+			id, err := strconv.Atoi(c.Param("id"))
+			if err != nil {
+				c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
+				return
+			}
+
+			s.mu.RLock()
+			defer s.mu.RUnlock()
+
+			for _, user := range s.users {
+				if user.ID == id {
+					c.JSON(http.StatusOK, user)
+					return
+				}
+			}
+
+			c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
+		})
+
+		// Create a new user
+		users.POST("", func(c *gin.Context) {
+			var newUser User
+			if err := c.ShouldBindJSON(&newUser); err != nil {
+				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+				return
+			}
+
+			s.mu.Lock()
+			defer s.mu.Unlock()
+
+			// Set the ID and creation time
+			newUser.ID = s.nextID
+			newUser.CreatedAt = time.Now()
+			s.nextID++
+
+			// Add to the users slice
+			s.users = append(s.users, newUser)
+
+			c.JSON(http.StatusCreated, newUser)
+
+			// Emit an event to notify about the new user
+			s.app.EmitEvent("user-created", newUser)
+		})
+
+		// Delete a user
+		users.DELETE("/:id", func(c *gin.Context) {
+			id, err := strconv.Atoi(c.Param("id"))
+			if err != nil {
+				c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
+				return
+			}
+
+			s.mu.Lock()
+			defer s.mu.Unlock()
+
+			for i, user := range s.users {
+				if user.ID == id {
+					// Remove the user from the slice
+					s.users = append(s.users[:i], s.users[i+1:]...)
+					c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
+					return
+				}
+			}
+
+			c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
+		})
+	}
+}
+```
+
+### 5. Create Custom Middleware
+
+You can create custom Gin middleware to enhance your service. Middleware functions in Gin are executed in the order they are added to the router and can perform tasks such as logging, authentication, and error handling. This example shows a simple logging middleware that records request details.
+
+```go
+// LoggingMiddleware is a Gin middleware that logs request details
+func LoggingMiddleware() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Start timer
+		start := time.Now()
+
+		// Process request
+		c.Next()
+
+		// Calculate latency
+		latency := time.Since(start)
+
+		// Log request details
+		log.Printf("[GIN] %s %s %d %s", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), latency)
+	}
+}
+```
+
+## Registering Your Service
+
+To use your Gin-based service in a Wails application, you need to register it with the application and specify the route where it should be mounted. This is done when creating the Wails application instance. The route you specify becomes the base path for all endpoints defined in your Gin router.
+
+```go
+app := application.New(application.Options{
+    Name:        "Gin Service Demo",
+    Description: "A demo of using Gin in Wails services",
+    Mac: application.MacOptions{
+        ApplicationShouldTerminateAfterLastWindowClosed: true,
+    },
+    LogLevel: slog.LevelDebug,
+    Services: []application.Service{
+        application.NewServiceWithOptions(services.NewGinService(), application.ServiceOptions{
+            Route: "/api",
+        }),
+    },
+    Assets: application.AssetOptions{
+        Handler: application.BundledAssetFileServer(assets),
+    },
+})
+```
+
+In this example, the Gin service is mounted at the `/api` route. This means that if your Gin router has an endpoint defined as `/info`, it will be accessible at `/api/info` in your application. This approach allows you to organise your API endpoints logically and avoid conflicts with other parts of your application.
+
+## Integrating with the Wails Event System
+
+One of the powerful features of using Gin with Wails Services is the ability to seamlessly integrate with the Wails event system. This allows for real-time communication between your backend service and the frontend.
+
+In your service's `ServiceStartup` method, you can register event handlers to process events from the frontend:
+
+```go
+s.app.OnEvent("gin-api-event", func(event *application.CustomEvent) {
+    // Log the event data
+    s.app.Logger.Info("Received event from frontend", "data", event.Data)
+
+    // Emit an event back to the frontend
+    s.app.EmitEvent("gin-api-response", map[string]interface{}{
+        "message": "Response from Gin API Service",
+        "time":    time.Now().Format(time.RFC3339),
+    })
+})
+```
+
+You can also emit events to the frontend from your Gin routes or other parts of your service:
+
+```go
+// After creating a new user
+s.app.EmitEvent("user-created", newUser)
+```
+
+## Frontend Integration
+
+To interact with your Gin service from the frontend, you'll need to import the Wails runtime, make HTTP requests to your API endpoints, and use the Wails event system for real-time communication.
+
+For production use, it's recommended to use the `@wailsio/runtime` package instead of directly importing `/wails/runtime.js`. This ensures type safety, better IDE support, version management through npm, and compatibility with modern JavaScript tooling.
+
+Install the package:
+```bash
+npm install @wailsio/runtime
+```
+
+Then use it in your code:
+```javascript
+import * as wails from '@wailsio/runtime';
+
+// Event emission
+wails.Events.Emit('gin-api-event',  eventData);
+```
+
+Here's an example of how to set up frontend integration:
+
+```html
+
+
+
+    
+    
+    Gin Service Example
+    
+
+
+    

Gin Service Example

+ +
+

API Endpoints

+

Try the Gin API endpoints mounted at /api:

+ + + + + + + +
+
Results will appear here...
+
+
+ +
+

Event Communication

+

Trigger an event to communicate with the Gin service:

+ + + +
+
Event responses will appear here...
+
+
+ + + + + + +``` + +## Closing Thoughts + +Integrating the Gin web framework with Wails v3 Services provides a powerful and flexible approach to building modular, maintainable web applications. By leveraging Gin's routing and middleware capabilities alongside the Wails event system, you can create rich, interactive applications with clean separation of concerns. + +The complete example code for this guide can be found in the Wails repository under `v3/examples/gin-service`. diff --git a/docs/src/content/docs/guides/menus.mdx b/docs/src/content/docs/guides/menus.mdx index 58a9cd313..02ee9de30 100644 --- a/docs/src/content/docs/guides/menus.mdx +++ b/docs/src/content/docs/guides/menus.mdx @@ -7,10 +7,10 @@ Wails v3 provides a powerful menu system that allows you to create both applicat ### Creating a Menu -To create a new menu, use the `NewMenu()` method from your application instance: +To create a new menu, use the `New()` method from the Menus manager: ```go -menu := application.NewMenu() +menu := app.Menus.New() ``` ### Adding Menu Items @@ -59,10 +59,10 @@ submenu.Add("Save") #### Combining menus A menu can be added into another menu by appending or prepending it. ```go -menu := application.NewMenu() +menu := app.Menus.New() menu.Add("First Menu") -secondaryMenu := application.NewMenu() +secondaryMenu := app.Menus.New() secondaryMenu.Add("Second Menu") // insert 'secondaryMenu' after 'menu' @@ -88,7 +88,7 @@ In some cases it'll be better to construct a whole new menu if you are working w This will clear all items on an existing menu and allows you to add items again. ```go -menu := application.NewMenu() +menu := app.Menus.New() menu.Add("Waiting for update...") // after certain logic, the menu has to be updated @@ -107,7 +107,7 @@ so be sure to manage your menus carefully. If you want to clear and release a menu, use the `Destroy()` method: ```go -menu := application.NewMenu() +menu := app.Menus.New() menu.Add("Waiting for update...") // after certain logic, the menu has to be destroyed @@ -265,7 +265,7 @@ These roles can be used to add individual menu items: Here's an example showing how to use both complete menus and individual roles: ```go -menu := application.NewMenu() +menu := app.Menus.New() // Add complete menu structures menu.AddRole(application.AppMenu) // macOS only @@ -287,8 +287,8 @@ Application menus are the menus that appear at the top of your application windo ### Application Menu Behaviour -When you set an application menu using `app.SetMenu()`, it becomes the main menu on macOS. -Menus are set on a pre-window basis for Windows/Linux. +When you set an application menu using `app.Menus.Set()`, it becomes the main menu on macOS. +Menus are set on a per-window basis for Windows/Linux. ```go app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ @@ -306,19 +306,19 @@ func main() { app := application.New(application.Options{}) // Create application menu - appMenu := application.NewMenu() + appMenu := app.Menus.New() fileMenu := appMenu.AddSubmenu("File") fileMenu.Add("New").OnClick(func(ctx *application.Context) { // This will be available in all windows unless overridden - window := app.CurrentWindow() + window := app.Windows.Current() window.SetTitle("New Window") }) // Set as application menu - this is for macOS - app.SetMenu(appMenu) + app.Menus.Set(appMenu) // Window with custom menu on Windows - customMenu := application.NewMenu() + customMenu := app.Menus.New() customMenu.Add("Custom Action") app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ Title: "Custom Menu", @@ -366,6 +366,10 @@ You can control when the default context menu appears using the `--default-conte ``` +:::note +This feature will only work as expected after the runtime [has been initialised](../../learn/runtime#initialisation). +::: + #### Nested Context Menu Behavior When using the `--default-contextmenu` property on nested elements, the following rules apply: @@ -411,7 +415,8 @@ When creating a custom context menu, you provide a unique identifier (name) that ```go // Create a context menu with identifier "imageMenu" -contextMenu := application.NewContextMenu("imageMenu") +contextMenu := application.NewContextMenu() +app.ContextMenus.Add("imageMenu", contextMenu) ``` The name parameter ("imageMenu" in this example) serves as a unique identifier that will be used to: @@ -470,7 +475,8 @@ Here's a complete example of implementing a custom context menu for an image gal ```go // Backend: Create the context menu -imageMenu := application.NewContextMenu("imageMenu") +imageMenu := application.NewContextMenu() +app.ContextMenus.Add("imageMenu", imageMenu) // Add relevant operations imageMenu.Add("View Full Size").OnClick(func(ctx *application.Context) { diff --git a/docs/src/content/docs/guides/msix-packaging.mdx b/docs/src/content/docs/guides/msix-packaging.mdx new file mode 100644 index 000000000..8b9ac57a1 --- /dev/null +++ b/docs/src/content/docs/guides/msix-packaging.mdx @@ -0,0 +1,138 @@ +--- +title: MSIX Packaging (Windows) +description: Guide for creating MSIX packages with Wails v3 +--- + +# MSIX Packaging (Windows) + +Wails v3 can generate modern **MSIX** installers for Windows applications, providing a cleaner, safer and Store-ready alternative to traditional **NSIS** or plain `.exe` bundles. + +This guide walks through: + +* Prerequisites & tool installation +* Building your app as an MSIX package +* Signing the package +* Command-line reference +* Troubleshooting + +--- + +## 1. Prerequisites + +| Requirement | Notes | +|-------------|-------| +| **Windows 10 1809+ / Windows 11** | MSIX is only supported on Windows. | +| **Windows SDK** (for `MakeAppx.exe` & `signtool.exe`) | Install from the [Windows SDK download page](https://developer.microsoft.com/windows/downloads/windows-sdk/). | +| **Microsoft MSIX Packaging Tool** (optional) | Available from the Microsoft Store – provides a GUI & CLI. | +| **Code-signing certificate** (recommended) | A `.pfx` file generated by your CA or `New-SelfSignedCertificate`. | + +> **Tip:** Wails ships a Task that opens the download pages for you: + +```bash +# installs MakeAppx / signtool (via Windows SDK) and the MSIX Packaging Tool +wails3 task windows:install:msix:tools +``` + +--- + +## 2. Building an MSIX package + +### 2.1 Quick CLI + +```bash +# Production build + MSIX +wails3 tool msix \ + --name MyApp \ # executable name + --executable build/bin/MyApp.exe \ + --arch x64 \ # x64, x86 or arm64 + --out build/bin/MyApp-x64.msix +``` + +The command will: + +1. Create a temporary layout (`AppxManifest.xml`, `Assets/`). +2. Call **MakeAppx.exe** (default) or **MsixPackagingTool.exe** if `--use-msix-tool` is passed. +3. Optionally sign the package (see Β§3). + +### 2.2 Using the generated Taskfile + +When you ran `wails init`, Wails created `build/windows/Taskfile.yml`. +Packaging with MSIX is a one-liner: + +```bash +# default=nsis, override FORMAT +wails3 task windows:package FORMAT=msix +``` + +Output goes to `build/bin/MyApp-.msix`. + +--- + +## 3. Signing the package + +Windows will refuse unsigned MSIX packages unless you enable developer-mode, so signing is strongly recommended. + +```bash +wails3 tool msix \ + --cert build/cert/CodeSign.pfx \ + --cert-password "pfx-password" \ + --publisher "CN=MyCompany" \ + --out build/bin/MyApp.msix +``` + +* If you pass `--cert`, Wails automatically runs `signtool sign …`. +* `--publisher` sets the `Publisher` field inside `AppxManifest.xml`. + It **must** match the subject of your certificate. + +--- + +## 4. Command-line reference + +| Flag | Default | Description | +|------|---------|-------------| +| `--config` | `wails.json` | Project config with **Info** & `fileAssociations`. | +| `--name` | β€” | Executable name inside the package (no spaces). | +| `--executable` | β€” | Path to the built `.exe`. | +| `--arch` | `x64` | `x64`, `x86`, or `arm64`. | +| `--out` | `.msix` | Output path / filename. | +| `--publisher` | `CN=` | Publisher string in the manifest. | +| `--cert` | ― | Path to `.pfx` certificate for signing. | +| `--cert-password` | ― | Password for the `.pfx`. | +| `--use-msix-tool` | `false` | Use **MsixPackagingTool.exe** instead of **MakeAppx.exe**. | +| `--use-makeappx` | `true` | Force MakeAppx even if the MSIX Tool is installed. | + +--- + +## 5. File associations + +Wails automatically injects file associations declared in `wails.json` into the package manifest: + +```json +"fileAssociations": [ + { "ext": "wails", "name": "Wails Project", "description": "Wails file", "role": "Editor" } +] +``` + +After installation, Windows will offer your app as a handler for these extensions. + +--- + +## 6. Troubleshooting + +| Problem | Solution | +|---------|----------| +| `MakeAppx.exe not found` | Install the Windows SDK and restart the terminal. | +| `signtool.exe not found` | Same as above – both live in the SDK’s *bin* folder. | +| *Package cannot be installed because publisher mismatch* | The certificate subject (CN) must match `--publisher`. | +| *The certificate is not trusted* | Import the certificate into **Trusted Root Certification Authorities** or use a publicly trusted code-signing cert. | +| Need GUI | Install **MSIX Packaging Tool** from the store and run `MsixPackagingTool.exe`. The template generated by Wails is fully compatible. | + +--- + +## 7. Next steps + +* [Windows Installer (NSIS) guide](./windows-installer.mdx) – legacy format. +* [Cross-platform update mechanism](../updates.mdx) – coming soon. +* Join the community on Discord to share feedback! + +Happy packaging! diff --git a/docs/src/content/docs/guides/windows-uac.mdx b/docs/src/content/docs/guides/windows-uac.mdx new file mode 100644 index 000000000..5bb380179 --- /dev/null +++ b/docs/src/content/docs/guides/windows-uac.mdx @@ -0,0 +1,151 @@ +--- +title: Windows UAC Configuration +sidebar: + order: 11 +--- + +import {Badge} from '@astrojs/starlight/components'; + +Relevant Platforms: +
+ +Windows User Account Control (UAC) determines the execution privileges of your Wails application. By default, Wails v3 applications include explicit UAC configuration in their Windows manifest, ensuring consistent behavior across different machines. + +## UAC Execution Levels + +Windows applications can request different execution levels through their manifest file. Wails v3 automatically includes UAC configuration with a default execution level that you can customize based on your application's needs. + +### Available Execution Levels + +| Level | Description | Use Case | +|-------|-------------|----------| +| `asInvoker` | Runs with the same privileges as the parent process | Default for most applications | +| `highestAvailable` | Runs with the highest privileges available to the user | Applications that may need elevated access | +| `requireAdministrator` | Always requires administrator privileges | System utilities, installers | + +### Default Configuration + +Wails v3 applications include a default UAC configuration in their Windows manifest: + +```xml + + + + + + + +``` + +This configuration ensures your application: +- Runs with the same privileges as the launching process +- Does not require elevation by default +- Works consistently across different machines +- Does not trigger UAC prompts for normal users + +## Customizing UAC Configuration + +Since Wails v3 encourages users to customize their build assets, you can modify the UAC configuration by editing your Windows manifest template directly. + +### Locating the Manifest Template + +The Windows manifest template is located at: +``` +build/windows/wails.exe.manifest +``` + +### Modifying the Execution Level + +To change the execution level, edit the `level` attribute in the `requestedExecutionLevel` element: + +```xml title="build/windows/wails.exe.manifest" + + + + + + + +``` + +### Examples + +#### Standard Application (Default) +Most applications should use the default `asInvoker` level: + +```xml + +``` + +#### System Utility +Applications that need elevated access when available: + +```xml + +``` + +#### Administrative Tool +Applications that always require administrator privileges: + +```xml + +``` + +## UI Access + +The `uiAccess` attribute controls whether your application can interact with higher-privilege UI elements. In most cases, this should remain `false`. + +Set to `true` only if your application needs to: +- Send input to other applications +- Drive the UI of other applications +- Access UI elements of higher-privilege processes + +:::caution[UI Access Requirements] +Setting `uiAccess="true"` requires your application to be: +- Digitally signed with a certificate from a trusted certificate authority +- Installed in a secure location (Program Files or Windows\System32) +::: + +## Building with Custom UAC Settings + +After modifying your manifest template, build your application normally: + +```bash +wails3 build +``` + +The build process will automatically embed your custom UAC configuration into the executable. + +## Verifying UAC Configuration + +You can verify that your UAC settings are properly embedded using the `go-winres` tool: + +```bash +go-winres extract --in your-app.exe --out extracted-resources/ +``` + +Then examine the extracted manifest file to confirm your UAC configuration is present. + +:::tip[Manifest Persistence] +Unlike some other frameworks, Wails v3's UAC configuration is embedded directly into the executable during compilation, ensuring it persists when the application is copied to other machines. +::: + +## Troubleshooting + +### UAC Prompts Not Appearing +If you set `requireAdministrator` but don't see UAC prompts: +- Verify the manifest is properly embedded in your executable +- Check that you're not running from an already-elevated process +- Ensure the manifest syntax is valid XML + +### Application Not Starting +If your application fails to start after UAC changes: +- Check the manifest syntax for XML errors +- Verify the execution level value is valid +- Try reverting to `asInvoker` to isolate the issue + +### Inconsistent Behavior Across Machines +If UAC behavior differs between machines: +- Ensure the manifest is embedded in the executable (not external) +- Check that the executable wasn't modified after building +- Verify Windows UAC settings are enabled on the target machine \ No newline at end of file diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index af6dcec89..3b71e86be 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -64,11 +64,11 @@ import CardAnimation from '../../components/CardAnimation.astro'; go install github.com/wailsapp/wails/v3/cmd/wails3@latest # Create a new project - wails init -n myproject + wails3 init -n myproject # Run your project cd myproject - wails dev + wails3 dev ``` diff --git a/docs/src/content/docs/learn/advanced-binding.mdx b/docs/src/content/docs/learn/advanced-binding.mdx new file mode 100644 index 000000000..82fbb7dec --- /dev/null +++ b/docs/src/content/docs/learn/advanced-binding.mdx @@ -0,0 +1,342 @@ +--- +title: Advanced Binding Techniques +sidebar: + order: 22 +--- + +import { FileTree } from "@astrojs/starlight/components"; + +This guide covers advanced techniques for customizing and optimizing the binding generation process in Wails v3. + +## Customizing Generated Code with Directives + +### Injecting Custom Code + +The `//wails:inject` directive allows you to inject custom JavaScript/TypeScript code into the generated bindings: + +```go +//wails:inject console.log("Hello from Wails!"); +type MyService struct {} + +func (s *MyService) Greet(name string) string { + return "Hello, " + name +} +``` + +This will inject the specified code into the generated JavaScript/TypeScript file for the `MyService` service. + +You can also use conditional injection to target specific output formats: + +```go +//wails:inject j*:console.log("Hello JS!"); // JavaScript only +//wails:inject t*:console.log("Hello TS!"); // TypeScript only +``` + +### Including Additional Files + +The `//wails:include` directive allows you to include additional files with the generated bindings: + +```go +//wails:include js/*.js +package mypackage +``` + +This directive is typically used in package documentation comments to include additional JavaScript/TypeScript files with the generated bindings. + +### Marking Internal Types and Methods + +The `//wails:internal` directive marks a type or method as internal, preventing it from being exported to the frontend: + +```go +//wails:internal +type InternalModel struct { + Field string +} + +//wails:internal +func (s *MyService) InternalMethod() {} +``` + +This is useful for types and methods that are only used internally by your Go code and should not be exposed to the frontend. + +### Ignoring Methods + +The `//wails:ignore` directive completely ignores a method during binding generation: + +```go +//wails:ignore +func (s *MyService) IgnoredMethod() {} +``` + +This is similar to `//wails:internal`, but it completely ignores the method rather than marking it as internal. + +### Custom Method IDs + +The `//wails:id` directive specifies a custom ID for a method, overriding the default hash-based ID: + +```go +//wails:id 42 +func (s *MyService) CustomIDMethod() {} +``` + +This can be useful for maintaining compatibility when refactoring code. + +## Working with Complex Types + +### Nested Structs + +The binding generator handles nested structs automatically: + +```go +type Address struct { + Street string + City string + State string + Zip string +} + +type Person struct { + Name string + Address Address +} + +func (s *MyService) GetPerson() Person { + return Person{ + Name: "John Doe", + Address: Address{ + Street: "123 Main St", + City: "Anytown", + State: "CA", + Zip: "12345", + }, + } +} +``` + +The generated JavaScript/TypeScript code will include classes for both `Person` and `Address`. + +### Maps and Slices + +Maps and slices are also handled automatically: + +```go +type Person struct { + Name string + Attributes map[string]string + Friends []string +} + +func (s *MyService) GetPerson() Person { + return Person{ + Name: "John Doe", + Attributes: map[string]string{ + "hair": "brown", + "eyes": "blue", + }, + Friends: []string{"Jane", "Bob", "Alice"}, + } +} +``` + +In JavaScript, maps are represented as objects and slices as arrays. In TypeScript, maps are represented as `Record` and slices as `T[]`. + +### Generic Types + +The binding generator supports generic types: + +```go +type Result[T any] struct { + Data T + Error string +} + +func (s *MyService) GetResult() Result[string] { + return Result[string]{ + Data: "Hello, World!", + Error: "", + } +} +``` + +The generated TypeScript code will include a generic class for `Result`: + +```typescript +export class Result { + "Data": T; + "Error": string; + + constructor(source: Partial> = {}) { + if (!("Data" in source)) { + this["Data"] = null as any; + } + if (!("Error" in source)) { + this["Error"] = ""; + } + + Object.assign(this, source); + } + + static createFrom(source: string | object = {}): Result { + let parsedSource = typeof source === "string" ? JSON.parse(source) : source; + return new Result(parsedSource as Partial>); + } +} +``` + +### Interfaces + +The binding generator can generate TypeScript interfaces instead of classes using the `-i` flag: + +```bash +wails3 generate bindings -ts -i +``` + +This will generate TypeScript interfaces for all models: + +```typescript +export interface Person { + Name: string; + Attributes: Record; + Friends: string[]; +} +``` + +## Optimizing Binding Generation + +### Using Names Instead of IDs + +By default, the binding generator uses hash-based IDs for method calls. You can use the `-names` flag to use names instead: + +```bash +wails3 generate bindings -names +``` + +This will generate code that uses method names instead of IDs: + +```javascript +export function Greet(name) { + let $resultPromise = $Call.ByName("Greet", name); + return $resultPromise; +} +``` + +This can make the generated code more readable and easier to debug, but it may be slightly less efficient. + +### Bundling the Runtime + +By default, the generated code imports the Wails runtime from the `@wailsio/runtime` npm package. You can use the `-b` flag to bundle the runtime with the generated code: + +```bash +wails3 generate bindings -b +``` + +This will include the runtime code directly in the generated files, eliminating the need for the npm package. + +### Disabling Index Files + +If you don't need the index files, you can use the `-noindex` flag to disable their generation: + +```bash +wails3 generate bindings -noindex +``` + +This can be useful if you prefer to import services and models directly from their respective files. + +## Real-World Examples + +### Authentication Service + +Here's an example of an authentication service with custom directives: + +```go +package auth + +//wails:inject console.log("Auth service initialized"); +type AuthService struct { + // Private fields + users map[string]User +} + +type User struct { + Username string + Email string + Role string +} + +type LoginRequest struct { + Username string + Password string +} + +type LoginResponse struct { + Success bool + User User + Token string + Error string +} + +// Login authenticates a user +func (s *AuthService) Login(req LoginRequest) LoginResponse { + // Implementation... +} + +// GetCurrentUser returns the current user +func (s *AuthService) GetCurrentUser() User { + // Implementation... +} + +// Internal helper method +//wails:internal +func (s *AuthService) validateCredentials(username, password string) bool { + // Implementation... +} +``` + +### Data Processing Service + +Here's an example of a data processing service with generic types: + +```go +package data + +type ProcessingResult[T any] struct { + Data T + Error string +} + +type DataService struct {} + +// Process processes data and returns a result +func (s *DataService) Process(data string) ProcessingResult[map[string]int] { + // Implementation... +} + +// ProcessBatch processes multiple data items +func (s *DataService) ProcessBatch(data []string) ProcessingResult[[]map[string]int] { + // Implementation... +} + +// Internal helper method +//wails:internal +func (s *DataService) parseData(data string) (map[string]int, error) { + // Implementation... +} +``` + +### Conditional Code Injection + +Here's an example of conditional code injection for different output formats: + +```go +//wails:inject j*:/** +//wails:inject j*: * @param {string} arg +//wails:inject j*: * @returns {Promise} +//wails:inject j*: */ +//wails:inject j*:export async function CustomMethod(arg) { +//wails:inject t*:export async function CustomMethod(arg: string): Promise { +//wails:inject await InternalMethod("Hello " + arg + "!"); +//wails:inject } +type Service struct{} +``` + +This injects different code for JavaScript and TypeScript outputs, providing appropriate type annotations for each language. diff --git a/docs/src/content/docs/learn/application-menu.mdx b/docs/src/content/docs/learn/application-menu.mdx index df344a02c..f7e659cbf 100644 --- a/docs/src/content/docs/learn/application-menu.mdx +++ b/docs/src/content/docs/learn/application-menu.mdx @@ -10,12 +10,49 @@ Application menus provide the main menu bar interface for your application. They ## Creating an Application Menu -Create a new application menu using the `NewMenu` method: +Create a new application menu using the `New` method from the Menu manager: ```go -menu := app.NewMenu() +menu := app.Menu.New() ``` +## Setting the Menu + +The way to set the menu varies on the platform: + + + + + On macOS, there is only one menu bar per application. Set the menu using the `Set` method of the Menu manager: + + ```go + app.Menu.Set(menu) + ``` + + + + + + On Windows, there is a menu bar per window. Set the menu using the `SetMenu` method of the window: + + ```go + app.Window.Current().SetMenu(menu) + ``` + + + + + + On Linux, the menu bar is typically per window. Set the menu using the `SetMenu` method of the window: + + ```go + app.Window.Current().SetMenu(menu) + ``` + + + + + ## Menu Roles Wails provides predefined menu roles that automatically create platform-appropriate menu structures: @@ -70,7 +107,7 @@ Menu items can control the application windows: ```go viewMenu := menu.AddSubmenu("View") viewMenu.Add("Toggle Fullscreen").OnClick(func(ctx *application.Context) { - window := app.CurrentWindow() + window := app.Window.Current() if window.Fullscreen() { window.SetFullscreen(false) } else { @@ -153,7 +190,7 @@ Always test menu functionality across all target platforms to ensure consistent ::: :::tip[Pro Tip] -Consider using the `application.CurrentWindow()` method in menu handlers to affect the active window, rather than storing window references. +Consider using the `app.Window.Current()` method in menu handlers to affect the active window, rather than storing window references. ::: ## Complete Example @@ -174,7 +211,7 @@ func main() { }) // Create main menu - menu := app.NewMenu() + menu := app.Menu.New() // Add platform-specific application menu if runtime.GOOS == "darwin" { @@ -208,7 +245,7 @@ func main() { }) // Set the menu - app.SetMenu(menu) + app.Menu.Set(menu) // Create main window app.NewWebviewWindow() diff --git a/docs/src/content/docs/learn/badges.mdx b/docs/src/content/docs/learn/badges.mdx new file mode 100644 index 000000000..3effaf7cc --- /dev/null +++ b/docs/src/content/docs/learn/badges.mdx @@ -0,0 +1,195 @@ +--- +title: Badges +--- + +import { Tabs, TabItem } from "@astrojs/starlight/components"; + +## Introduction + +Wails provides a cross-platform badge service for desktop applications. This service allows you to display badges on your application tile or dock icon, which is useful for indicating unread messages, notifications, or other status information. + +## Basic Usage + +### Creating the Service + +First, initialize the badge service: + +```go +import "github.com/wailsapp/wails/v3/pkg/application" +import "github.com/wailsapp/wails/v3/pkg/services/badge" + +// Create a new badge service +badgeService := badge.New() + +// Register the service with the application +app := application.New(application.Options{ + Services: []application.Service{ + application.NewService(badgeService), + }, +}) +``` + +### Creating the Service with Custom 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/badge" +import "image/color" + +// Create a badge service with custom options +options := badge.Options{ + 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 +} + +badgeService := badge.NewWithOptions(options) + +// Register the service with the application +app := application.New(application.Options{ + Services: []application.Service{ + application.NewService(badgeService), + }, +}) +``` + +## Badge Operations + +### Setting a Badge + +Set a badge on the application tile/dock icon: + +```go +// Set a default badge +badgeService.SetBadge("") + +// Set a numeric badge +badgeService.SetBadge("3") + +// Set a text badge +badgeService.SetBadge("New") +``` + +### Setting a Custom Badge + +Set a badge on the application tile/dock icon with one-off options applied: + +#### Go +```go +options := badge.Options{ + 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 +badgeService.SetCustomBadge("", options) + +// Set a numeric badge +badgeService.SetCustomBadge("3", options) + +// Set a text badge +badgeService.SetCustomBadge("New", options) +``` + +### Removing a Badge + +Remove the badge from the application icon: + +```go +badgeService.RemoveBadge() +``` + +## Platform Considerations + + + + + On macOS, badges: + + - Are displayed directly on the dock icon + - Support text values + - Automatically handle dark/light mode appearance + - Use the standard macOS dock badge styling + - Automatically handle label overflow + - Do not support customization options (any options passed to NewWithOptions will be ignored) + - Will display "●" as the default badge if an empty label is provided + + + + + + On Windows, badges: + + - Are displayed as an overlay icon in the taskbar + - Support text values + - Allow customization of colors, font, and font sizes + - Adapt to Windows theme settings + - Require the application to have a window + - Use smaller font size automatically for badges with multiple characters + - Do not handle label overflow + - Support the following 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) + + + + + + On Linux: + + - Badge functionality is not available + + + + +## Best Practices + +1. Use badges sparingly: + - Too many badge updates can distract users + - Reserve badges for important notifications + +2. Keep badge text short: + - Numeric badges are most effective + - On macOS, text badges should be brief + +3. For Windows 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 badge service with default options | +| `NewWithOptions(options Options)` | Creates a new badge service with custom options (Windows only, options are ignored on macOS) | + +### Badge Operations +| Method | Description | +|----------------------------------------------|------------------------------------------------------------| +| `SetBadge(label string) error` | Sets a badge with the specified label | +| `SetCustomBadge(label string, options Options) 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 Options 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 +} +``` diff --git a/docs/src/content/docs/learn/binding-best-practices.mdx b/docs/src/content/docs/learn/binding-best-practices.mdx new file mode 100644 index 000000000..4536fc3c9 --- /dev/null +++ b/docs/src/content/docs/learn/binding-best-practices.mdx @@ -0,0 +1,115 @@ +--- +title: Binding Best Practices +sidebar: + order: 23 +--- + +import { FileTree } from "@astrojs/starlight/components"; + +This guide provides best practices and patterns for using the Wails binding system effectively in your applications. + +## Service Design Patterns + +### Service Organization + +Organize your services based on functionality rather than technical concerns. For example, instead of having a single large service, split it into smaller, focused services: + +```go +// Instead of this: +type AppService struct {} + +func (s *AppService) GetUser() User { /* ... */ } +func (s *AppService) UpdateUser(user User) error { /* ... */ } +func (s *AppService) GetProducts() []Product { /* ... */ } +func (s *AppService) AddProduct(product Product) error { /* ... */ } + +// Do this: +type UserService struct {} +func (s *UserService) GetUser() User { /* ... */ } +func (s *UserService) UpdateUser(user User) error { /* ... */ } + +type ProductService struct {} +func (s *ProductService) GetProducts() []Product { /* ... */ } +func (s *ProductService) AddProduct(product Product) error { /* ... */ } +``` + +This makes your code more maintainable and easier to understand. + +### Use JSON Tags + +Use JSON tags to control how your models are serialized: + +```go +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Password string `json:"-"` // Exclude from JSON + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} +``` + +### Separate Frontend and Backend Models + +Consider using different models for the frontend and backend: + +```go +// Backend model +type User struct { + ID int + Name string + Email string + Password string // Sensitive data + CreatedAt time.Time + UpdatedAt time.Time +} + +// Frontend model +type UserDTO struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` +} + +func (s *UserService) GetUser() UserDTO { + user := getUserFromDatabase() + return UserDTO{ + ID: user.ID, + Name: user.Name, + Email: user.Email, + CreatedAt: user.CreatedAt, + } +} +``` + +This gives you more control over what data is exposed to the frontend. + +### Use Context for Cancellation + +Use context for cancellation to avoid wasting resources on abandoned requests: + +```go +func (s *ProductService) GetProducts(ctx context.Context, req ProductsRequest) (ProductsResponse, error) { + // Check if the request has been cancelled + select { + case <-ctx.Done(): + return ProductsResponse{}, ctx.Err() + default: + // Continue processing + } + + products, total, err := getProductsFromDatabase(ctx, req.Page, req.PageSize, req.Filter) + if err != nil { + return ProductsResponse{}, err + } + + return ProductsResponse{ + Products: products, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, nil +} +``` diff --git a/docs/src/content/docs/learn/binding-system.mdx b/docs/src/content/docs/learn/binding-system.mdx new file mode 100644 index 000000000..13ce15932 --- /dev/null +++ b/docs/src/content/docs/learn/binding-system.mdx @@ -0,0 +1,155 @@ +--- +title: Binding System Internals +sidebar: + order: 21 +--- + +import { FileTree } from "@astrojs/starlight/components"; + +This guide explains how the Wails binding system works internally, providing insights for developers who want to understand the mechanics behind the automatic code generation. + +## Architecture Overview + +The Wails binding system consists of three main components: + +1. **Collection**: Analyzes Go code to extract information about services, models, and other declarations +2. **Configuration**: Manages settings and options for the binding generation process +3. **Rendering**: Generates JavaScript/TypeScript code based on the collected information + + +- internal/generator/ + - collect/ # Package analysis and information extraction + - config/ # Configuration structures and interfaces + - render/ # Code generation for JS/TS + + +## Collection Process + +The collection process is responsible for analyzing Go packages and extracting information about services, models, and other declarations. This is handled by the `collect` package. + +### Key Components + +- **Collector**: Manages package information and caches collected data +- **Package**: Represents a Go package being analyzed and stores collected services, models, and directives +- **Service**: Collects information about service types and their methods +- **Model**: Collects detailed information about model types, including fields, values, and type parameters +- **Directive**: Parses and interprets `//wails:` directives in Go source code + +### Collection Flow + +1. The collector scans the Go packages specified in the project +2. It identifies service types (structs with methods that will be exposed to the frontend) +3. For each service, it collects information about its methods +4. It identifies model types (structs used as parameters or return values in service methods) +5. For each model, it collects information about its fields and type parameters +6. It processes any `//wails:` directives found in the code + +## Rendering Process + +The rendering process is responsible for generating JavaScript/TypeScript code based on the collected information. This is handled by the `render` package. + +### Key Components + +- **Renderer**: Orchestrates the rendering of service, model, and index files +- **Module**: Represents a single generated JavaScript/TypeScript module +- **Templates**: Text templates used for code generation + +### Rendering Flow + +1. For each service, the renderer generates a JavaScript/TypeScript file with functions that mirror the service methods +2. For each model, the renderer generates a JavaScript/TypeScript class that mirrors the model struct +3. The renderer generates index files that re-export all services and models +4. The renderer applies any custom code injections specified by `//wails:inject` directives + +## Type Mapping + +One of the most important aspects of the binding system is how Go types are mapped to JavaScript/TypeScript types. Here's a summary of the mapping: + +| Go Type | JavaScript Type | TypeScript Type | +|---------|----------------|----------------| +| `bool` | `boolean` | `boolean` | +| `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `float32`, `float64` | `number` | `number` | +| `string` | `string` | `string` | +| `[]byte` | `Uint8Array` | `Uint8Array` | +| `[]T` | `Array` | `T[]` | +| `map[K]V` | `Object` | `Record` | +| `struct` | `Object` | Custom class | +| `interface{}` | `any` | `any` | +| `*T` | `T \| null` | `T \| null` | +| `func` | Not supported | Not supported | +| `chan` | Not supported | Not supported | + +## Directives System + +The binding system supports several directives that can be used to customize the generated code. These directives are added as comments in your Go code. + +### Available Directives + +- `//wails:inject`: Injects custom JavaScript/TypeScript code into the generated bindings +- `//wails:include`: Includes additional files with the generated bindings +- `//wails:internal`: Marks a type or method as internal, preventing it from being exported to the frontend +- `//wails:ignore`: Completely ignores a method during binding generation +- `//wails:id`: Specifies a custom ID for a method, overriding the default hash-based ID + +### Directive Processing + +1. During the collection phase, the collector identifies and parses directives in the Go code +2. The directives are stored with the corresponding declarations (services, methods, models, etc.) +3. During the rendering phase, the renderer applies the directives to customize the generated code + +## Advanced Features + +### Conditional Code Generation + +The binding system supports conditional code generation using a two-character condition prefix for `include` and `inject` directives: + +``` + + + +
+

Application Report

+

Generated: {{.Timestamp}}

+
+
+ + {{range .Items}} +

{{.}}

+ {{end}} +
+ +` + + // Write report to file + file, err := os.Create(reportPath) + if err != nil { + return err + } + defer file.Close() + + t, err := template.New("report").Parse(tmpl) + if err != nil { + return err + } + + err = t.Execute(file, data) + if err != nil { + return err + } + + // Open in browser + return app.Browser.OpenFile(reportPath) +} +``` + +### Development Tools + +Open development resources during development: + +```go +func setupDevelopmentMenu(app *application.App) { + if !app.Environment.Info().Debug { + return // Only show in debug mode + } + + menu := app.Menu.New() + devMenu := menu.AddSubmenu("Development") + + devMenu.Add("Open DevTools").OnClick(func(ctx *application.Context) { + // This would open browser devtools if available + window := app.Window.Current() + if window != nil { + window.OpenDevTools() + } + }) + + devMenu.Add("View Source").OnClick(func(ctx *application.Context) { + // Open source code repository + app.Browser.OpenURL("https://github.com/youruser/yourapp") + }) + + devMenu.Add("API Documentation").OnClick(func(ctx *application.Context) { + // Open local API docs + app.Browser.OpenURL("http://localhost:8080/docs") + }) +} +``` + +## Error Handling + +### Graceful Error Handling + +Always handle potential errors when opening URLs or files: + +```go +func openURLWithFallback(app *application.App, url string, fallbackMessage string) { + err := app.Browser.OpenURL(url) + if err != nil { + app.Logger.Error("Failed to open URL", "url", url, "error", err) + + // Show fallback dialog with URL + dialog := app.Dialog.Info() + dialog.SetTitle("Unable to Open Link") + dialog.SetMessage(fmt.Sprintf("%s\n\nURL: %s", fallbackMessage, url)) + dialog.Show() + } +} + +// Usage +openURLWithFallback(app, + "https://docs.example.com", + "Please open the following URL manually in your browser:") +``` + +### User Feedback + +Provide feedback when operations succeed or fail: + +```go +func openURLWithFeedback(app *application.App, url string) { + err := app.Browser.OpenURL(url) + if err != nil { + // Show error dialog + app.Dialog.Error(). + SetTitle("Browser Error"). + SetMessage(fmt.Sprintf("Could not open URL: %s", err.Error())). + Show() + } else { + // Optionally show success notification + app.Logger.Info("URL opened successfully", "url", url) + } +} +``` + +## Platform Considerations + + + + + On macOS: + + - Uses the `open` command to launch the default browser + - Respects user's default browser setting in System Preferences + - May prompt for permission if the application is sandboxed + - Handles `file://` URLs correctly for local files + + + + + + On Windows: + + - Uses Windows Shell API to open URLs + - Respects default browser setting in Windows Settings + - Handles Windows path formats correctly + - May show security warnings for untrusted URLs + + + + + + On Linux: + + - Attempts to use `xdg-open` first, falls back to other methods + - Behavior varies by desktop environment + - Respects `BROWSER` environment variable if set + - May require additional packages in minimal installations + + + + +## Best Practices + +1. **Always Handle Errors**: Browser operations can fail for various reasons: + ```go + if err := app.Browser.OpenURL(url); err != nil { + app.Logger.Error("Failed to open browser", "error", err) + // Provide fallback or user notification + } + ``` + +2. **Validate URLs**: Ensure URLs are well-formed before opening: + ```go + func isValidHTTPURL(str string) bool { + u, err := url.Parse(str) + return err == nil && (u.Scheme == "http" || u.Scheme == "https") + } + ``` + +3. **User Confirmation**: For external links, consider asking user permission: + ```go + // Show confirmation dialog before opening external links + confirmAndOpen(app, "https://external-site.com") + ``` + +4. **Secure File Paths**: When opening files, ensure paths are safe: + ```go + func openSafeFile(app *application.App, filename string) error { + // Ensure file exists and is readable + if _, err := os.Stat(filename); err != nil { + return err + } + return app.Browser.OpenFile(filename) + } + ``` + +## Complete Example + +Here's a complete example showing various browser integration patterns: + +```go +package main + +import ( + "fmt" + "os" + "path/filepath" + "github.com/wailsapp/wails/v3/pkg/application" +) + +func main() { + app := application.New(application.Options{ + Name: "Browser Integration Demo", + }) + + // Setup menu with browser actions + setupMenu(app) + + // Create main window + window := app.Window.New() + window.SetTitle("Browser Integration") + + err := app.Run() + if err != nil { + panic(err) + } +} + +func setupMenu(app *application.App) { + menu := app.Menu.New() + + // File menu + fileMenu := menu.AddSubmenu("File") + fileMenu.Add("Generate Report").OnClick(func(ctx *application.Context) { + generateHTMLReport(app) + }) + + // Help menu + helpMenu := menu.AddSubmenu("Help") + helpMenu.Add("Documentation").OnClick(func(ctx *application.Context) { + openWithConfirmation(app, "https://docs.example.com") + }) + helpMenu.Add("Support").OnClick(func(ctx *application.Context) { + openWithConfirmation(app, "https://support.example.com") + }) + + app.Menu.Set(menu) +} + +func openWithConfirmation(app *application.App, url string) { + dialog := app.Dialog.Question() + dialog.SetTitle("Open External Link") + dialog.SetMessage(fmt.Sprintf("Open %s in your browser?", url)) + + dialog.AddButton("Open").OnClick(func() { + if err := app.Browser.OpenURL(url); err != nil { + showError(app, "Failed to open URL", err) + } + }) + + dialog.AddButton("Cancel") + dialog.Show() +} + +func generateHTMLReport(app *application.App) { + // Create temporary HTML file + tmpDir := os.TempDir() + reportPath := filepath.Join(tmpDir, "demo_report.html") + + html := ` + + + + Demo Report + + + +
+

Application Report

+

This is a sample report generated by the application.

+
+
+

Report Details

+

This report was generated to demonstrate browser integration.

+
+ +` + + err := os.WriteFile(reportPath, []byte(html), 0644) + if err != nil { + showError(app, "Failed to create report", err) + return + } + + // Open in browser + err = app.Browser.OpenFile(reportPath) + if err != nil { + showError(app, "Failed to open report", err) + } +} + +func showError(app *application.App, message string, err error) { + app.Dialog.Error(). + SetTitle("Error"). + SetMessage(fmt.Sprintf("%s: %v", message, err)). + Show() +} +``` + +:::tip[Pro Tip] +Consider providing fallback mechanisms for when browser operations fail, such as copying URLs to clipboard or showing them in a dialog for manual opening. +::: + +:::danger[Warning] +Always validate URLs and file paths before opening them to prevent security issues. Be cautious about opening user-provided URLs without validation. +::: \ No newline at end of file diff --git a/docs/src/content/docs/learn/build.mdx b/docs/src/content/docs/learn/build.mdx index aeb5804af..e45c6e3b9 100644 --- a/docs/src/content/docs/learn/build.mdx +++ b/docs/src/content/docs/learn/build.mdx @@ -123,6 +123,7 @@ the application on macOS. Key features include: - Building binaries for amd64, arm64 and universal (both) architectures - Generating `.icns` icon file - Creating an `.app` bundle for distributing +- Ad-hoc signing `.app` bundles - Setting macOS-specific build flags and environment variables ## Task Execution and Command Aliases diff --git a/docs/src/content/docs/learn/clipboard.mdx b/docs/src/content/docs/learn/clipboard.mdx index 142d6ec1d..39e14c52f 100644 --- a/docs/src/content/docs/learn/clipboard.mdx +++ b/docs/src/content/docs/learn/clipboard.mdx @@ -11,7 +11,7 @@ The Wails Clipboard API provides a simple interface for interacting with the sys The clipboard can be accessed through the application instance: ```go -clipboard := app.Clipboard() +clipboard := app.Clipboard ``` ## Setting Text @@ -19,7 +19,7 @@ clipboard := app.Clipboard() To set text to the clipboard, utilise the `SetText` method: ```go -success := app.Clipboard().SetText("Hello World") +success := app.Clipboard.SetText("Hello World") if !success { // Handle error } @@ -36,7 +36,7 @@ Setting an empty string (`""`) effectively clears the text content from the clip To retrieve text from the clipboard, utilise the `Text` method: ```go -text, ok := app.Clipboard().Text() +text, ok := app.Clipboard.Text() if !ok { // Handle error } else { @@ -75,7 +75,7 @@ func main() { }) // Create a custom menu - menu := app.NewMenu() + menu := app.Menu.New() if runtime.GOOS == "darwin" { menu.AddRole(application.AppMenu) } @@ -83,33 +83,33 @@ func main() { // Add clipboard operations to menu setClipboardMenu := menu.AddSubmenu("Set Clipboard") setClipboardMenu.Add("Set Text 'Hello'").OnClick(func(ctx *application.Context) { - success := app.Clipboard().SetText("Hello") + success := app.Clipboard.SetText("Hello") if !success { - application.InfoDialog().SetMessage("Failed to set clipboard text").Show() + app.Dialog.Info().SetMessage("Failed to set clipboard text").Show() } }) getClipboardMenu := menu.AddSubmenu("Get Clipboard") getClipboardMenu.Add("Get Text").OnClick(func(ctx *application.Context) { - result, ok := app.Clipboard().Text() + result, ok := app.Clipboard.Text() if !ok { - application.InfoDialog().SetMessage("Failed to get clipboard text").Show() + app.Dialog.Info().SetMessage("Failed to get clipboard text").Show() } else { - application.InfoDialog().SetMessage("Got:\n\n" + result).Show() + app.Dialog.Info().SetMessage("Got:\n\n" + result).Show() } }) clearClipboardMenu := menu.AddSubmenu("Clear Clipboard") clearClipboardMenu.Add("Clear Text").OnClick(func(ctx *application.Context) { - success := app.Clipboard().SetText("") + success := app.Clipboard.SetText("") if success { - application.InfoDialog().SetMessage("Clipboard text cleared").Show() + app.Dialog.Info().SetMessage("Clipboard text cleared").Show() } else { - application.InfoDialog().SetMessage("Clipboard text not cleared").Show() + app.Dialog.Info().SetMessage("Clipboard text not cleared").Show() } }) - app.SetMenu(menu) + app.Menu.Set(menu) app.NewWebviewWindow() err := app.Run() diff --git a/docs/src/content/docs/learn/context-menu.mdx b/docs/src/content/docs/learn/context-menu.mdx index f56e9a763..e5bec1e6e 100644 --- a/docs/src/content/docs/learn/context-menu.mdx +++ b/docs/src/content/docs/learn/context-menu.mdx @@ -10,10 +10,10 @@ Context menus are popup menus that appear when right-clicking elements in your a ## Creating a Context Menu -To create a context menu, utilise the `NewContextMenu` method from your application instance: +To create a context menu, use the `Add` method from the ContextMenu manager: ```go -contextMenu := application.NewContextMenu("menu-id") +contextMenu := app.ContextMenu.Add("menu-id", application.NewContextMenu()) ``` The `menu-id` parameter is a unique identifier for the menu that will be used to associate it with HTML elements. @@ -23,7 +23,7 @@ The `menu-id` parameter is a unique identifier for the menu that will be used to You can add items to your context menu using the same methods as application menus. Here's a simple example: ```go -contextMenu := application.NewContextMenu("editor-menu") +contextMenu := application.NewContextMenu() contextMenu.Add("Cut").OnClick(func(ctx *application.Context) { // Handle cut action }) @@ -33,6 +33,9 @@ contextMenu.Add("Copy").OnClick(func(ctx *application.Context) { contextMenu.Add("Paste").OnClick(func(ctx *application.Context) { // Handle paste action }) + +// Register the context menu with the manager +app.ContextMenu.Add("editor-menu", contextMenu) ``` :::tip[Menu Items] @@ -44,12 +47,15 @@ For detailed information about available menu item types and properties, refer t Context menus can receive data from the HTML element that triggered them. This data can be accessed in the click handlers: ```go -contextMenu := application.NewContextMenu("image-menu") +contextMenu := app.ContextMenu.New() menuItem := contextMenu.Add("Process Image") menuItem.OnClick(func(ctx *application.Context) { imageID := ctx.ContextMenuData() // Process the image using the ID }) + +// Register the context menu with the manager +app.ContextMenu.Add("image-menu", contextMenu) ``` ## Associating with HTML Elements @@ -65,6 +71,10 @@ To associate a context menu with an HTML element, use the `--custom-contextmenu` - `--custom-contextmenu`: Specifies the menu ID (must match the ID used in `NewContextMenu`) - `--custom-contextmenu-data`: Optional data that will be passed to the click handlers +:::note +This feature will only work as expected after the runtime [has been initialised](../runtime#initialisation). +::: + ## Default Context Menu The default context menu is the webview's built-in context menu that provides system-level operations. You can control its visibility using the `--default-contextmenu` CSS property: @@ -99,9 +109,12 @@ The `auto` setting enables "smart" context menu behaviour: Menu items can be updated dynamically using the `SetLabel` method and other property setters. After making changes, call `Update` on the menu to apply them: ```go -contextMenu := application.NewContextMenu("dynamic-menu") +contextMenu := application.NewContextMenu() menuItem := contextMenu.Add("Initial Label") +// Register the context menu with the manager +app.ContextMenu.Add("dynamic-menu", contextMenu) + // Later, update the menu item menuItem.SetLabel("New Label") contextMenu.Update() @@ -182,7 +195,7 @@ func main() { }) // Create a context menu - contextMenu := application.NewContextMenu("test") + contextMenu := app.ContextMenu.New() // Add items that respond to context data clickMe := contextMenu.Add("Click to show context data") @@ -194,6 +207,9 @@ func main() { contextMenu.Update() }) + // Register the context menu with the manager + app.ContextMenu.Add("test", contextMenu) + window := app.NewWebviewWindow() window.SetTitle("Context Menu Demo") diff --git a/docs/src/content/docs/learn/dialogs.mdx b/docs/src/content/docs/learn/dialogs.mdx index a757a11e5..783f7bf8b 100644 --- a/docs/src/content/docs/learn/dialogs.mdx +++ b/docs/src/content/docs/learn/dialogs.mdx @@ -15,7 +15,7 @@ Wails provides a comprehensive dialog system for displaying native system dialog Display simple informational messages to users: ```go -dialog := application.InfoDialog() +dialog := app.Dialog.Info() dialog.SetTitle("Welcome") dialog.SetMessage("Welcome to our application!") dialog.Show() @@ -26,7 +26,7 @@ dialog.Show() Present users with questions and customisable buttons: ```go -dialog := application.QuestionDialog() +dialog := app.Dialog.Question() dialog.SetTitle("Save Changes") dialog.SetMessage("Do you want to save your changes?") dialog.AddButton("Save").OnClick(func() { @@ -42,7 +42,7 @@ dialog.Show() Display error messages: ```go -dialog := application.ErrorDialog() +dialog := app.Dialog.Error() dialog.SetTitle("Error") dialog.SetMessage("Failed to save file") dialog.Show() @@ -55,7 +55,7 @@ dialog.Show() Allow users to select files to open: ```go -dialog := application.OpenFileDialog() +dialog := app.Dialog.OpenFile() dialog.SetTitle("Select Image") dialog.SetFilters([]*application.FileFilter{ { @@ -80,7 +80,7 @@ if paths, err := dialog.PromptForMultipleSelection(); err == nil { Allow users to choose where to save files: ```go -dialog := application.SaveFileDialog() +dialog := app.Dialog.SaveFile() dialog.SetTitle("Save Document") dialog.SetDefaultFilename("document.txt") dialog.SetFilters([]*application.FileFilter{ @@ -102,7 +102,7 @@ if path, err := dialog.PromptForSingleSelection(); err == nil { Dialogs can use custom icons from the built-in icon set: ```go -dialog := application.InfoDialog() +dialog := app.Dialog.Info() dialog.SetIcon(icons.ApplicationDarkMode256) ``` @@ -111,8 +111,8 @@ dialog.SetIcon(icons.ApplicationDarkMode256) Dialogs can be attached to specific windows: ```go -dialog := application.QuestionDialog() -dialog.AttachToWindow(app.CurrentWindow()) +dialog := app.Dialog.Question() +dialog.AttachToWindow(app.Window.Current()) dialog.Show() ``` @@ -121,7 +121,7 @@ dialog.Show() Create buttons with custom labels and actions: ```go -dialog := application.QuestionDialog() +dialog := app.Dialog.Question() dialog.SetMessage("Choose an action") // Add buttons with custom handlers @@ -183,7 +183,9 @@ dialog.SetDefaultButton(cancelButton) // Set default button Allow users to select directories: ```go -dialog := application.DirectoryDialog() +dialog := app.Dialog.OpenFile() +dialog.CanChooseDirectories(true) +dialog.CanChooseFiles(false) dialog.SetTitle("Select Project Directory") if path, err := dialog.PromptForSingleSelection(); err == nil { // Use selected directory path @@ -195,7 +197,7 @@ if path, err := dialog.PromptForSingleSelection(); err == nil { Display application information: ```go -app.ShowAboutDialog() +app.Menu.ShowAbout() ``` ## Best Practices diff --git a/docs/src/content/docs/learn/environment.mdx b/docs/src/content/docs/learn/environment.mdx new file mode 100644 index 000000000..5903d9a59 --- /dev/null +++ b/docs/src/content/docs/learn/environment.mdx @@ -0,0 +1,620 @@ +--- +title: Environment +sidebar: + order: 59 +--- + +import { Tabs, TabItem } from "@astrojs/starlight/components"; + +Wails provides comprehensive environment information through the EnvironmentManager API. This allows your application to detect system properties, theme preferences, and integrate with the operating system's file manager. + +## Accessing the Environment Manager + +The environment manager is accessed through the `Env` property on your application instance: + +```go +app := application.New(application.Options{ + Name: "Environment Demo", +}) + +// Access the environment manager +env := app.Env +``` + +## System Information + +### Get Environment Information + +Retrieve comprehensive information about the runtime environment: + +```go +envInfo := app.Env.Info() + +app.Logger.Info("Environment information", + "os", envInfo.OS, // "windows", "darwin", "linux" + "arch", envInfo.Arch, // "amd64", "arm64", etc. + "debug", envInfo.Debug, // Debug mode flag +) + +// Operating system details +if envInfo.OSInfo != nil { + app.Logger.Info("OS details", + "name", envInfo.OSInfo.Name, + "version", envInfo.OSInfo.Version, + ) +} + +// Platform-specific information +for key, value := range envInfo.PlatformInfo { + app.Logger.Info("Platform info", "key", key, "value", value) +} +``` + +### Environment Structure + +The environment information includes several important fields: + +```go +type EnvironmentInfo struct { + OS string // Operating system: "windows", "darwin", "linux" + Arch string // Architecture: "amd64", "arm64", "386", etc. + Debug bool // Whether running in debug mode + OSInfo *operatingsystem.OS // Detailed OS information + PlatformInfo map[string]any // Platform-specific details +} +``` + +## Theme Detection + +### Dark Mode Detection + +Detect whether the system is using dark mode: + +```go +if app.Env.IsDarkMode() { + app.Logger.Info("System is in dark mode") + // Apply dark theme to your application + applyDarkTheme() +} else { + app.Logger.Info("System is in light mode") + // Apply light theme to your application + applyLightTheme() +} +``` + +### Theme Change Monitoring + +Listen for theme changes to update your application dynamically: + +```go +import "github.com/wailsapp/wails/v3/pkg/events" + +// Listen for theme changes +app.OnApplicationEvent(events.Common.ThemeChanged, func(event *application.ApplicationEvent) { + if app.Env.IsDarkMode() { + app.Logger.Info("Switched to dark mode") + updateApplicationTheme("dark") + } else { + app.Logger.Info("Switched to light mode") + updateApplicationTheme("light") + } +}) + +func updateApplicationTheme(theme string) { + // Update your application's theme + // This could emit an event to the frontend + app.Event.Emit("theme:changed", theme) +} +``` + +## File Manager Integration + +### Open File Manager + +Open the system's file manager at a specific location: + +```go +// Open file manager at a directory +err := app.Env.OpenFileManager("/Users/username/Documents", false) +if err != nil { + app.Logger.Error("Failed to open file manager", "error", err) +} + +// Open file manager and select a specific file +err = app.Env.OpenFileManager("/Users/username/Documents/report.pdf", true) +if err != nil { + app.Logger.Error("Failed to open file manager with selection", "error", err) +} +``` + +### Common Use Cases + +Show files or folders in the file manager from your application: + +```go +func showInFileManager(app *application.App, path string) { + err := app.Env.OpenFileManager(path, true) + if err != nil { + // Fallback: try opening just the directory + dir := filepath.Dir(path) + err = app.Env.OpenFileManager(dir, false) + if err != nil { + app.Logger.Error("Could not open file manager", "path", path, "error", err) + + // Show error to user + app.Dialog.Error(). + SetTitle("File Manager Error"). + SetMessage("Could not open file manager"). + Show() + } + } +} + +// Usage examples +func setupFileMenu(app *application.App) { + menu := app.Menu.New() + fileMenu := menu.AddSubmenu("File") + + fileMenu.Add("Show Downloads Folder").OnClick(func(ctx *application.Context) { + homeDir, _ := os.UserHomeDir() + downloadsDir := filepath.Join(homeDir, "Downloads") + showInFileManager(app, downloadsDir) + }) + + fileMenu.Add("Show Application Data").OnClick(func(ctx *application.Context) { + configDir, _ := os.UserConfigDir() + appDir := filepath.Join(configDir, "MyApp") + showInFileManager(app, appDir) + }) +} +``` + +## Platform-Specific Behavior + +### Adaptive Application Behavior + +Use environment information to adapt your application's behavior: + +```go +func configureForPlatform(app *application.App) { + envInfo := app.Env.Info() + + switch envInfo.OS { + case "darwin": + configureMacOS(app) + case "windows": + configureWindows(app) + case "linux": + configureLinux(app) + } + + // Adapt to architecture + if envInfo.Arch == "arm64" { + app.Logger.Info("Running on ARM architecture") + // Potentially optimize for ARM + } +} + +func configureMacOS(app *application.App) { + app.Logger.Info("Configuring for macOS") + + // macOS-specific configuration + menu := app.Menu.New() + menu.AddRole(application.AppMenu) // Add standard macOS app menu + + // Handle dark mode + if app.Env.IsDarkMode() { + setMacOSDarkTheme() + } +} + +func configureWindows(app *application.App) { + app.Logger.Info("Configuring for Windows") + + // Windows-specific configuration + // Set up Windows-style menus, key bindings, etc. +} + +func configureLinux(app *application.App) { + app.Logger.Info("Configuring for Linux") + + // Linux-specific configuration + // Adapt to different desktop environments +} +``` + +## Debug Mode Handling + +### Development vs Production + +Use debug mode information to enable development features: + +```go +func setupApplicationMode(app *application.App) { + envInfo := app.Env.Info() + + if envInfo.Debug { + app.Logger.Info("Running in debug mode") + setupDevelopmentFeatures(app) + } else { + app.Logger.Info("Running in production mode") + setupProductionFeatures(app) + } +} + +func setupDevelopmentFeatures(app *application.App) { + // Enable development-only features + menu := app.Menu.New() + + // Add development menu + devMenu := menu.AddSubmenu("Development") + devMenu.Add("Reload Application").OnClick(func(ctx *application.Context) { + // Reload the application + window := app.Window.Current() + if window != nil { + window.Reload() + } + }) + + devMenu.Add("Open DevTools").OnClick(func(ctx *application.Context) { + window := app.Window.Current() + if window != nil { + window.OpenDevTools() + } + }) + + devMenu.Add("Show Environment").OnClick(func(ctx *application.Context) { + showEnvironmentDialog(app) + }) +} + +func setupProductionFeatures(app *application.App) { + // Production-only features + // Disable debug logging, enable analytics, etc. +} +``` + +## Environment Information Dialog + +### Display System Information + +Create a dialog showing environment information: + +```go +func showEnvironmentDialog(app *application.App) { + envInfo := app.Env.Info() + + details := fmt.Sprintf(`Environment Information: + +Operating System: %s +Architecture: %s +Debug Mode: %t + +Dark Mode: %t + +Platform Information:`, + envInfo.OS, + envInfo.Arch, + envInfo.Debug, + app.Env.IsDarkMode()) + + // Add platform-specific details + for key, value := range envInfo.PlatformInfo { + details += fmt.Sprintf("\n%s: %v", key, value) + } + + if envInfo.OSInfo != nil { + details += fmt.Sprintf("\n\nOS Details:\nName: %s\nVersion: %s", + envInfo.OSInfo.Name, + envInfo.OSInfo.Version) + } + + dialog := app.Dialog.Info() + dialog.SetTitle("Environment Information") + dialog.SetMessage(details) + dialog.Show() +} +``` + +## Platform Considerations + + + + + On macOS: + + - Dark mode detection uses system appearance settings + - File manager operations use Finder + - Platform info includes macOS version details + - Architecture may be "arm64" on Apple Silicon Macs + + ```go + if envInfo.OS == "darwin" { + // macOS-specific handling + if envInfo.Arch == "arm64" { + app.Logger.Info("Running on Apple Silicon") + } + } + ``` + + + + + + On Windows: + + - Dark mode detection uses Windows theme settings + - File manager operations use Windows Explorer + - Platform info includes Windows version details + - May include additional Windows-specific information + + ```go + if envInfo.OS == "windows" { + // Windows-specific handling + for key, value := range envInfo.PlatformInfo { + if key == "windows_version" { + app.Logger.Info("Windows version", "version", value) + } + } + } + ``` + + + + + + On Linux: + + - Dark mode detection varies by desktop environment + - File manager operations use system default file manager + - Platform info includes distribution details + - Behavior may vary between different Linux distributions + + ```go + if envInfo.OS == "linux" { + // Linux-specific handling + if distro, ok := envInfo.PlatformInfo["distribution"]; ok { + app.Logger.Info("Linux distribution", "distro", distro) + } + } + ``` + + + + +## Best Practices + +1. **Cache Environment Information**: Environment info rarely changes during runtime: + ```go + type App struct { + envInfo *application.EnvironmentInfo + } + + func (a *App) getEnvInfo() application.EnvironmentInfo { + if a.envInfo == nil { + info := a.app.Env.Info() + a.envInfo = &info + } + return *a.envInfo + } + ``` + +2. **Handle Theme Changes**: Listen for system theme changes: + ```go + app.OnApplicationEvent(events.Common.ThemeChanged, func(event *application.ApplicationEvent) { + updateTheme(app.Env.IsDarkMode()) + }) + ``` + +3. **Graceful File Manager Failures**: Always handle file manager errors: + ```go + func openFileManagerSafely(app *application.App, path string) { + err := app.Env.OpenFileManager(path, false) + if err != nil { + // Provide fallback or user notification + app.Logger.Warn("Could not open file manager", "path", path) + } + } + ``` + +4. **Platform-Specific Features**: Use environment info to enable platform features: + ```go + envInfo := app.Env.Info() + if envInfo.OS == "darwin" { + // Enable macOS-specific features + } + ``` + +## Complete Example + +Here's a complete example demonstrating environment management: + +```go +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/events" +) + +func main() { + app := application.New(application.Options{ + Name: "Environment Demo", + }) + + // Setup application based on environment + setupForEnvironment(app) + + // Monitor theme changes + monitorThemeChanges(app) + + // Create menu with environment features + setupEnvironmentMenu(app) + + // Create main window + window := app.Window.New() + window.SetTitle("Environment Demo") + + err := app.Run() + if err != nil { + panic(err) + } +} + +func setupForEnvironment(app *application.App) { + envInfo := app.Env.Info() + + app.Logger.Info("Application environment", + "os", envInfo.OS, + "arch", envInfo.Arch, + "debug", envInfo.Debug, + "darkMode", app.Env.IsDarkMode(), + ) + + // Configure for platform + switch envInfo.OS { + case "darwin": + app.Logger.Info("Configuring for macOS") + // macOS-specific setup + case "windows": + app.Logger.Info("Configuring for Windows") + // Windows-specific setup + case "linux": + app.Logger.Info("Configuring for Linux") + // Linux-specific setup + } + + // Apply initial theme + if app.Env.IsDarkMode() { + applyDarkTheme(app) + } else { + applyLightTheme(app) + } +} + +func monitorThemeChanges(app *application.App) { + app.OnApplicationEvent(events.Common.ThemeChanged, func(event *application.ApplicationEvent) { + if app.Env.IsDarkMode() { + app.Logger.Info("System switched to dark mode") + applyDarkTheme(app) + } else { + app.Logger.Info("System switched to light mode") + applyLightTheme(app) + } + }) +} + +func setupEnvironmentMenu(app *application.App) { + menu := app.Menu.New() + + // Add platform-specific app menu + if runtime.GOOS == "darwin" { + menu.AddRole(application.AppMenu) + } + + // Tools menu + toolsMenu := menu.AddSubmenu("Tools") + + toolsMenu.Add("Show Environment Info").OnClick(func(ctx *application.Context) { + showEnvironmentInfo(app) + }) + + toolsMenu.Add("Open Downloads Folder").OnClick(func(ctx *application.Context) { + openDownloadsFolder(app) + }) + + toolsMenu.Add("Toggle Theme").OnClick(func(ctx *application.Context) { + // This would typically be handled by the system + // but shown here for demonstration + toggleTheme(app) + }) + + app.Menu.Set(menu) +} + +func showEnvironmentInfo(app *application.App) { + envInfo := app.Env.Info() + + message := fmt.Sprintf(`Environment Information: + +Operating System: %s +Architecture: %s +Debug Mode: %t +Dark Mode: %t + +Platform Details:`, + envInfo.OS, + envInfo.Arch, + envInfo.Debug, + app.Env.IsDarkMode()) + + for key, value := range envInfo.PlatformInfo { + message += fmt.Sprintf("\n%s: %v", key, value) + } + + if envInfo.OSInfo != nil { + message += fmt.Sprintf("\n\nOS Information:\nName: %s\nVersion: %s", + envInfo.OSInfo.Name, + envInfo.OSInfo.Version) + } + + dialog := app.Dialog.Info() + dialog.SetTitle("Environment Information") + dialog.SetMessage(message) + dialog.Show() +} + +func openDownloadsFolder(app *application.App) { + homeDir, err := os.UserHomeDir() + if err != nil { + app.Logger.Error("Could not get home directory", "error", err) + return + } + + downloadsDir := filepath.Join(homeDir, "Downloads") + err = app.Env.OpenFileManager(downloadsDir, false) + if err != nil { + app.Logger.Error("Could not open Downloads folder", "error", err) + + app.Dialog.Error(). + SetTitle("File Manager Error"). + SetMessage("Could not open Downloads folder"). + Show() + } +} + +func applyDarkTheme(app *application.App) { + app.Logger.Info("Applying dark theme") + // Emit theme change to frontend + app.Event.Emit("theme:apply", "dark") +} + +func applyLightTheme(app *application.App) { + app.Logger.Info("Applying light theme") + // Emit theme change to frontend + app.Event.Emit("theme:apply", "light") +} + +func toggleTheme(app *application.App) { + // This is just for demonstration + // Real theme changes should come from the system + currentlyDark := app.Env.IsDarkMode() + if currentlyDark { + applyLightTheme(app) + } else { + applyDarkTheme(app) + } +} +``` + +:::tip[Pro Tip] +Use environment information to provide platform-appropriate user experiences. For example, use Command key shortcuts on macOS and Control key shortcuts on Windows/Linux. +::: + +:::danger[Warning] +Environment information is generally stable during application runtime, but theme preferences can change. Always listen for theme change events to keep your UI synchronized. +::: \ No newline at end of file diff --git a/docs/src/content/docs/learn/events.mdx b/docs/src/content/docs/learn/events.mdx index c21d772df..87e00af75 100644 --- a/docs/src/content/docs/learn/events.mdx +++ b/docs/src/content/docs/learn/events.mdx @@ -422,33 +422,85 @@ app.OnApplicationEvent(events.Windows.WebViewNavigationCompleted, func(event *ap ## Custom Events -You can emit and listen for custom events to enable communication between different parts of your application. +You can emit and listen for custom events to enable communication between different parts of your application. Wails v3 provides both a traditional API and a new [Manager API](/learn/manager-api) for better organization. ### Emitting Events You can emit custom events from anywhere in your application: + + ```go -// Emit an event with data from the application +// NEW: Using the Event Manager (recommended) +app.Event.Emit("myevent", "hello") + +// Emit from a specific window +window.EmitEvent("windowevent", "window specific data") +``` + + +```go +// Traditional API (still supported) app.EmitEvent("myevent", "hello") // Emit from a specific window window.EmitEvent("windowevent", "window specific data") ``` + + ### Handling Custom Events -Listen for custom events using the `OnEvent` method: +Listen for custom events using the event management methods: + + ```go -app.OnEvent("myevent", func(e *application.CustomEvent) { +// NEW: Using the Event Manager (recommended) +cancelFunc := app.Event.On("myevent", func(e *application.CustomEvent) { // Access event information name := e.Name // Event name data := e.Data // Event data sender := e.Sender // Name of the window sending the event, or "" if sent from application cancelled := e.IsCancelled() // Event cancellation status }) + +// Remove specific event listener +app.Event.Off("myevent") + +// Remove all event listeners +app.Event.Reset() + +// Listen for events a limited number of times +app.Event.OnMultiple("myevent", func(e *application.CustomEvent) { + // This will only be called 3 times +}, 3) ``` + + +```go +// Traditional API (still supported) +cancelFunc := app.OnEvent("myevent", func(e *application.CustomEvent) { + // Access event information + name := e.Name // Event name + data := e.Data // Event data + sender := e.Sender // Name of the window sending the event, or "" if sent from application + cancelled := e.IsCancelled() // Event cancellation status +}) + +// Remove specific event listener +app.OffEvent("myevent") + +// Remove all event listeners +app.ResetEvents() + +// Listen for events a limited number of times +app.OnMultipleEvent("myevent", func(e *application.CustomEvent) { + // This will only be called 3 times +}, 3) +``` + + ## Event Cancellation @@ -479,7 +531,7 @@ window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent }) // For custom events -app.OnEvent("myevent", func(e *application.CustomEvent) { +app.Event.On("myevent", func(e *application.CustomEvent) { if e.IsCancelled() { app.Logger.Info("Event was cancelled") return diff --git a/docs/src/content/docs/learn/keybindings.mdx b/docs/src/content/docs/learn/keybindings.mdx new file mode 100644 index 000000000..0998f096e --- /dev/null +++ b/docs/src/content/docs/learn/keybindings.mdx @@ -0,0 +1,437 @@ +--- +title: Key Bindings +sidebar: + order: 56 +--- + +import { Tabs, TabItem } from "@astrojs/starlight/components"; + +Wails provides a powerful key binding system that allows you to register global keyboard shortcuts that work across all windows in your application. This enables users to quickly access functionality without navigating through menus. + +## Accessing the Key Binding Manager + +The key binding manager is accessed through the `KeyBindings` property on your application instance: + +```go +app := application.New(application.Options{ + Name: "Keyboard Shortcuts Demo", +}) + +// Access the key binding manager +keyBindings := app.KeyBinding +``` + +## Adding Key Bindings + +### Basic Key Binding + +Register a simple keyboard shortcut: + +```go +app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) { + // Handle save action + app.Logger.Info("Save shortcut triggered") + // Perform save operation... +}) +``` + +### Multiple Key Bindings + +Register multiple shortcuts for common operations: + +```go +// File operations +app.KeyBinding.Add("Ctrl+N", func(window *application.WebviewWindow) { + // New file + window.EmitEvent("file:new", nil) +}) + +app.KeyBinding.Add("Ctrl+O", func(window *application.WebviewWindow) { + // Open file + dialog := app.Dialog.OpenFile() + if file, err := dialog.PromptForSingleSelection(); err == nil { + window.EmitEvent("file:open", file) + } +}) + +app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) { + // Save file + window.EmitEvent("file:save", nil) +}) + +// Edit operations +app.KeyBinding.Add("Ctrl+Z", func(window *application.WebviewWindow) { + // Undo + window.EmitEvent("edit:undo", nil) +}) + +app.KeyBinding.Add("Ctrl+Y", func(window *application.WebviewWindow) { + // Redo (Windows/Linux) + window.EmitEvent("edit:redo", nil) +}) + +app.KeyBinding.Add("Cmd+Shift+Z", func(window *application.WebviewWindow) { + // Redo (macOS) + window.EmitEvent("edit:redo", nil) +}) +``` + +## Key Binding Accelerators + +### Accelerator Format + +Key bindings use a standard accelerator format with modifiers and keys: + +```go +// Modifier keys +"Ctrl+S" // Control + S +"Cmd+S" // Command + S (macOS) +"Alt+F4" // Alt + F4 +"Shift+Ctrl+Z" // Shift + Control + Z + +// Function keys +"F1" // F1 key +"Ctrl+F5" // Control + F5 + +// Special keys +"Escape" // Escape key +"Enter" // Enter key +"Space" // Spacebar +"Tab" // Tab key +"Backspace" // Backspace key +"Delete" // Delete key + +// Arrow keys +"Up" // Up arrow +"Down" // Down arrow +"Left" // Left arrow +"Right" // Right arrow +``` + +### Platform-Specific Accelerators + +Handle platform differences for common shortcuts: + +```go +import "runtime" + +// Cross-platform save shortcut +if runtime.GOOS == "darwin" { + app.KeyBinding.Add("Cmd+S", saveHandler) +} else { + app.KeyBinding.Add("Ctrl+S", saveHandler) +} + +// Or register both +app.KeyBinding.Add("Ctrl+S", saveHandler) +app.KeyBinding.Add("Cmd+S", saveHandler) +``` + +## Managing Key Bindings + +### Removing Key Bindings + +Remove key bindings when they're no longer needed: + +```go +// Remove a specific key binding +app.KeyBinding.Remove("Ctrl+S") + +// Example: Temporary key binding for a modal +app.KeyBinding.Add("Escape", func(window *application.WebviewWindow) { + // Close modal + window.EmitEvent("modal:close", nil) + // Remove this temporary binding + app.KeyBinding.Remove("Escape") +}) +``` + +### Getting All Key Bindings + +Retrieve all registered key bindings: + +```go +allBindings := app.KeyBinding.GetAll() +for _, binding := range allBindings { + app.Logger.Info("Key binding", "accelerator", binding.Accelerator) +} +``` + +## Advanced Usage + +### Context-Aware Key Bindings + +Make key bindings context-aware by checking application state: + +```go +app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) { + // Check current application state + if isEditMode() { + // Save document + saveDocument() + } else if isInSettings() { + // Save settings + saveSettings() + } else { + app.Logger.Info("Save not available in current context") + } +}) +``` + +### Window-Specific Actions + +Key bindings receive the active window, allowing window-specific behavior: + +```go +app.KeyBinding.Add("F11", func(window *application.WebviewWindow) { + // Toggle fullscreen for the active window + if window.Fullscreen() { + window.SetFullscreen(false) + } else { + window.SetFullscreen(true) + } +}) + +app.KeyBinding.Add("Ctrl+W", func(window *application.WebviewWindow) { + // Close the active window + window.Close() +}) +``` + +### Dynamic Key Binding Management + +Dynamically add and remove key bindings based on application state: + +```go +func enableEditMode() { + // Add edit-specific key bindings + app.KeyBinding.Add("Ctrl+B", func(window *application.WebviewWindow) { + window.EmitEvent("format:bold", nil) + }) + + app.KeyBinding.Add("Ctrl+I", func(window *application.WebviewWindow) { + window.EmitEvent("format:italic", nil) + }) + + app.KeyBinding.Add("Ctrl+U", func(window *application.WebviewWindow) { + window.EmitEvent("format:underline", nil) + }) +} + +func disableEditMode() { + // Remove edit-specific key bindings + app.KeyBinding.Remove("Ctrl+B") + app.KeyBinding.Remove("Ctrl+I") + app.KeyBinding.Remove("Ctrl+U") +} +``` + +## Platform Considerations + + + + + On macOS: + + - Use `Cmd` instead of `Ctrl` for standard shortcuts + - `Cmd+Q` is typically reserved for quitting the application + - `Cmd+H` hides the application + - `Cmd+M` minimizes windows + - Consider standard macOS keyboard shortcuts + + Common macOS patterns: + ```go + app.KeyBinding.Add("Cmd+N", newFileHandler) // New + app.KeyBinding.Add("Cmd+O", openFileHandler) // Open + app.KeyBinding.Add("Cmd+S", saveFileHandler) // Save + app.KeyBinding.Add("Cmd+Z", undoHandler) // Undo + app.KeyBinding.Add("Cmd+Shift+Z", redoHandler) // Redo + app.KeyBinding.Add("Cmd+C", copyHandler) // Copy + app.KeyBinding.Add("Cmd+V", pasteHandler) // Paste + ``` + + + + + + On Windows: + + - Use `Ctrl` for standard shortcuts + - `Alt+F4` closes applications + - `F1` typically opens help + - Consider Windows keyboard conventions + + Common Windows patterns: + ```go + app.KeyBinding.Add("Ctrl+N", newFileHandler) // New + app.KeyBinding.Add("Ctrl+O", openFileHandler) // Open + app.KeyBinding.Add("Ctrl+S", saveFileHandler) // Save + app.KeyBinding.Add("Ctrl+Z", undoHandler) // Undo + app.KeyBinding.Add("Ctrl+Y", redoHandler) // Redo + app.KeyBinding.Add("Ctrl+C", copyHandler) // Copy + app.KeyBinding.Add("Ctrl+V", pasteHandler) // Paste + app.KeyBinding.Add("F1", helpHandler) // Help + ``` + + + + + + On Linux: + + - Generally follows Windows conventions with `Ctrl` + - May vary by desktop environment + - Consider GNOME/KDE standard shortcuts + - Some desktop environments reserve certain shortcuts + + Common Linux patterns: + ```go + app.KeyBinding.Add("Ctrl+N", newFileHandler) // New + app.KeyBinding.Add("Ctrl+O", openFileHandler) // Open + app.KeyBinding.Add("Ctrl+S", saveFileHandler) // Save + app.KeyBinding.Add("Ctrl+Z", undoHandler) // Undo + app.KeyBinding.Add("Ctrl+Shift+Z", redoHandler) // Redo + app.KeyBinding.Add("Ctrl+C", copyHandler) // Copy + app.KeyBinding.Add("Ctrl+V", pasteHandler) // Paste + ``` + + + + +## Best Practices + +1. **Use Standard Shortcuts**: Follow platform conventions for common operations: + ```go + // Cross-platform save + if runtime.GOOS == "darwin" { + app.KeyBinding.Add("Cmd+S", saveHandler) + } else { + app.KeyBinding.Add("Ctrl+S", saveHandler) + } + ``` + +2. **Provide Visual Feedback**: Let users know when shortcuts are triggered: + ```go + app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) { + saveDocument() + // Show brief notification + window.EmitEvent("notification:show", "Document saved") + }) + ``` + +3. **Handle Conflicts**: Be careful not to override important system shortcuts: + ```go + // Avoid overriding system shortcuts like: + // Ctrl+Alt+Del (Windows) + // Cmd+Space (macOS Spotlight) + // Alt+Tab (Window switching) + ``` + +4. **Document Shortcuts**: Provide help or documentation for available shortcuts: + ```go + app.KeyBinding.Add("F1", func(window *application.WebviewWindow) { + // Show help dialog with available shortcuts + showKeyboardShortcutsHelp() + }) + ``` + +5. **Clean Up**: Remove temporary key bindings when they're no longer needed: + ```go + func enterEditMode() { + app.KeyBinding.Add("Escape", exitEditModeHandler) + } + + func exitEditModeHandler(window *application.WebviewWindow) { + exitEditMode() + app.KeyBinding.Remove("Escape") // Clean up temporary binding + } + ``` + +## Complete Example + +Here's a complete example of a text editor with keyboard shortcuts: + +```go +package main + +import ( + "runtime" + "github.com/wailsapp/wails/v3/pkg/application" +) + +func main() { + app := application.New(application.Options{ + Name: "Text Editor with Shortcuts", + }) + + // File operations + if runtime.GOOS == "darwin" { + app.KeyBinding.Add("Cmd+N", func(window *application.WebviewWindow) { + window.EmitEvent("file:new", nil) + }) + app.KeyBinding.Add("Cmd+O", func(window *application.WebviewWindow) { + openFile(app, window) + }) + app.KeyBinding.Add("Cmd+S", func(window *application.WebviewWindow) { + window.EmitEvent("file:save", nil) + }) + } else { + app.KeyBinding.Add("Ctrl+N", func(window *application.WebviewWindow) { + window.EmitEvent("file:new", nil) + }) + app.KeyBinding.Add("Ctrl+O", func(window *application.WebviewWindow) { + openFile(app, window) + }) + app.KeyBinding.Add("Ctrl+S", func(window *application.WebviewWindow) { + window.EmitEvent("file:save", nil) + }) + } + + // View operations + app.KeyBinding.Add("F11", func(window *application.WebviewWindow) { + window.SetFullscreen(!window.Fullscreen()) + }) + + app.KeyBinding.Add("F1", func(window *application.WebviewWindow) { + showKeyboardShortcuts(window) + }) + + // Create main window + window := app.Window.New() + window.SetTitle("Text Editor") + + err := app.Run() + if err != nil { + panic(err) + } +} + +func openFile(app *application.App, window *application.WebviewWindow) { + dialog := app.Dialog.OpenFile() + dialog.AddFilter("Text Files", "*.txt;*.md") + + if file, err := dialog.PromptForSingleSelection(); err == nil { + window.EmitEvent("file:open", file) + } +} + +func showKeyboardShortcuts(window *application.WebviewWindow) { + shortcuts := ` +Keyboard Shortcuts: +- Ctrl/Cmd+N: New file +- Ctrl/Cmd+O: Open file +- Ctrl/Cmd+S: Save file +- F11: Toggle fullscreen +- F1: Show this help +` + window.EmitEvent("help:show", shortcuts) +} +``` + +:::tip[Pro Tip] +Test your key bindings on all target platforms to ensure they work correctly and don't conflict with system shortcuts. +::: + +:::danger[Warning] +Be careful not to override critical system shortcuts. Some key combinations are reserved by the operating system and cannot be captured by applications. +::: \ No newline at end of file diff --git a/docs/src/content/docs/learn/manager-api.mdx b/docs/src/content/docs/learn/manager-api.mdx new file mode 100644 index 000000000..d58445742 --- /dev/null +++ b/docs/src/content/docs/learn/manager-api.mdx @@ -0,0 +1,265 @@ +--- +title: Manager API +sidebar: + order: 25 +--- + +import { Tabs, TabItem } from "@astrojs/starlight/components"; + +The Wails v3 Manager API provides an organized and discoverable way to access application functionality through focused manager structs. This new API structure groups related methods together while maintaining full backward compatibility with the traditional App API. + +## Overview + +The Manager API organizes application functionality into eleven focused areas: + +- **`app.Window`** - Window creation, management, and callbacks +- **`app.ContextMenu`** - Context menu registration and management +- **`app.KeyBinding`** - Global key binding management +- **`app.Browser`** - Browser integration (opening URLs and files) +- **`app.Env`** - Environment information and system state +- **`app.Dialog`** - File and message dialog operations +- **`app.Event`** - Custom event handling and application events +- **`app.Menu`** - Application menu management +- **`app.Screen`** - Screen management and coordinate transformations +- **`app.Clipboard`** - Clipboard text operations +- **`app.SystemTray`** - System tray icon creation and management + +## Benefits + +- **Better discoverability** - IDE autocomplete shows organized API surface +- **Improved code organization** - Related methods grouped together +- **Enhanced maintainability** - Separation of concerns across managers +- **Future extensibility** - Easier to add new features to specific areas + +## Usage + +The Manager API provides organized access to all application functionality: + +```go +// Events and custom event handling +app.Event.Emit("custom", data) +app.Event.On("custom", func(e *CustomEvent) { ... }) + +// Window management +window, _ := app.Window.GetByName("main") +app.Window.OnCreate(func(window Window) { ... }) + +// Browser integration +app.Browser.OpenURL("https://wails.io") + +// Menu management +menu := app.Menu.New() +app.Menu.Set(menu) + +// System tray +systray := app.SystemTray.New() +``` + +## Manager Reference + +### Window Manager + +Manages window creation, retrieval, and lifecycle callbacks. + +```go +// Create windows +window := app.Window.New() +window := app.Window.NewWithOptions(options) +current := app.Window.Current() + +// Find windows +window, exists := app.Window.GetByName("main") +windows := app.Window.GetAll() + +// Window callbacks +app.Window.OnCreate(func(window Window) { + // Handle window creation +}) +``` + +### Event Manager + +Handles custom events and application event listening. + +```go +// Custom events +app.Event.Emit("userAction", data) +cancelFunc := app.Event.On("userAction", func(e *CustomEvent) { + // Handle event +}) +app.Event.Off("userAction") +app.Event.Reset() // Remove all listeners + +// Application events +app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *ApplicationEvent) { + // Handle system theme change +}) +``` + +### Browser Manager + +Provides browser integration for opening URLs and files. + +```go +// Open URLs and files in default browser +err := app.Browser.OpenURL("https://wails.io") +err := app.Browser.OpenFile("/path/to/document.pdf") +``` + +### Environment Manager + +Access to system environment information. + +```go +// Get environment info +env := app.Env.Info() +fmt.Printf("OS: %s, Arch: %s\n", env.OS, env.Arch) + +// Check system theme +if app.Env.IsDarkMode() { + // Dark mode is active +} + +// Open file manager +err := app.Env.OpenFileManager("/path/to/folder", false) +``` + +### Dialog Manager + +Organized access to file and message dialogs. + +```go +// File dialogs +result, err := app.Dialog.OpenFile(). + AddFilter("Text Files", "*.txt"). + PromptForSingleSelection() + +result, err = app.Dialog.SaveFile(). + SetDefaultFilename("document.txt"). + PromptForSingleSelection() + +// Message dialogs +app.Dialog.Info(). + SetTitle("Information"). + SetMessage("Operation completed successfully"). + Show() + +app.Dialog.Error(). + SetTitle("Error"). + SetMessage("An error occurred"). + Show() +``` + +### Menu Manager + +Application menu creation and management. + +```go +// Create and set application menu +menu := app.Menu.New() +fileMenu := menu.AddSubmenu("File") +fileMenu.Add("New").OnClick(func(ctx *Context) { + // Handle menu click +}) + +app.Menu.Set(menu) + +// Show about dialog +app.Menu.ShowAbout() +``` + +### Key Binding Manager + +Dynamic management of global key bindings. + +```go +// Add key bindings +app.KeyBinding.Add("ctrl+n", func(window *WebviewWindow) { + // Handle Ctrl+N +}) + +app.KeyBinding.Add("ctrl+q", func(window *WebviewWindow) { + app.Quit() +}) + +// Remove key bindings +app.KeyBinding.Remove("ctrl+n") + +// Get all bindings +bindings := app.KeyBinding.GetAll() +``` + +### Context Menu Manager + +Advanced context menu management (for library authors). + +```go +// Create and register context menu +menu := app.ContextMenu.New() +app.ContextMenu.Add("myMenu", menu) + +// Retrieve context menu +menu, exists := app.ContextMenu.Get("myMenu") + +// Remove context menu +app.ContextMenu.Remove("myMenu") +``` + +### Screen Manager + +Screen management and coordinate transformations for multi-monitor setups. + +```go +// Get screen information +screens := app.Screen.GetAll() +primary := app.Screen.GetPrimary() + +// Coordinate transformations +physicalPoint := app.Screen.DipToPhysicalPoint(logicalPoint) +logicalPoint := app.Screen.PhysicalToDipPoint(physicalPoint) + +// Screen detection +screen := app.Screen.ScreenNearestDipPoint(point) +screen = app.Screen.ScreenNearestDipRect(rect) +``` + +### Clipboard Manager + +Clipboard operations for reading and writing text. + +```go +// Set text to clipboard +success := app.Clipboard.SetText("Hello World") +if !success { + // Handle error +} + +// Get text from clipboard +text, ok := app.Clipboard.Text() +if !ok { + // Handle error +} else { + // Use the text +} +``` + +### SystemTray Manager + +System tray icon creation and management. + +```go +// Create system tray +systray := app.SystemTray.New() +systray.SetLabel("My App") +systray.SetIcon(iconBytes) + +// Add menu to system tray +menu := app.Menu.New() +menu.Add("Open").OnClick(func(ctx *Context) { + // Handle click +}) +systray.SetMenu(menu) + +// Destroy system tray when done +systray.Destroy() +``` \ No newline at end of file diff --git a/docs/src/content/docs/learn/notifications.mdx b/docs/src/content/docs/learn/notifications.mdx new file mode 100644 index 000000000..8cb63646b --- /dev/null +++ b/docs/src/content/docs/learn/notifications.mdx @@ -0,0 +1,304 @@ +--- +title: Notifications +--- + +import { Tabs, TabItem } from "@astrojs/starlight/components"; + +## Introduction + +Wails provides a comprehensive cross-platform notification system for desktop applications. This service allows you to display native system notifications, with support for interactive elements like action buttons and text input fields. + +## Basic Usage + +### Creating the Service + +First, initialize the notifications service: + +```go +import "github.com/wailsapp/wails/v3/pkg/application" +import "github.com/wailsapp/wails/v3/services/notifications" + +// Create a new notification service +notifier := notifications.New() + +//Register the service with the application +app := application.New(application.Options{ + Services: []application.Service{ + application.NewService(notifier), + }, +}) +``` + +## Notification Authorization + +Notifications on macOS require user authorization. Request and check authorization: + +```go +authorized, err := notifier.CheckNotificationAuthorization() +if err != nil { + // Handle authorization error +} +if authorized { + // Send notifications +} else { + // Request authorization + authorized, err = notifier.RequestNotificationAuthorization() +} +``` +On Windows and Linux this always returns `true`. + +## Notification Types + +### Basic Notifications + +Send a basic notification with a unique id, title, optional subtitle (macOS and Linux), and body text to users: + +```go +notifier.SendNotification(notifications.NotificationOptions{ + ID: "unique-id", + Title: "New Calendar Invite", + Subtitle: "From: Jane Doe", // Optional + Body: "Tap to view the event", +}) + +``` + +### Interactive Notifications +Send a notification with action buttons and text inputs. These notifications require a notification category to be resgistered first: + +```go +// Define a unique category id +categoryID := "unique-category-id" + +// Define a category with actions +category := notifications.NotificationCategory{ + ID: categoryID, + Actions: []notifications.NotificationAction{ + { + ID: "OPEN", + Title: "Open", + }, + { + ID: "ARCHIVE", + Title: "Archive", + Destructive: true, /* macOS-specific */ + }, + }, + HasReplyField: true, + ReplyPlaceholder: "message...", + ReplyButtonTitle: "Reply", +} + +// Register the category +notifier.RegisterNotificationCategory(category) + +// Send an interactive notification with the actions registered in the provided category +notifier.SendNotificationWithActions(notifications.NotificationOptions{ + ID: "unique-id", + Title: "New Message", + Subtitle: "From: Jane Doe", + Body: "Are you able to make it?", + CategoryID: categoryID, +}) +``` + +## Notification Responses + +Process user interactions with notifications: + +```go +notifier.OnNotificationResponse(func(result notifications.NotificationResult) { + response := result.Response + fmt.Printf("Notification %s was actioned with: %s\n", response.ID, response.ActionIdentifier) + + if response.ActionIdentifier == "TEXT_REPLY" { + fmt.Printf("User replied: %s\n", response.UserText) + } + + if data, ok := response.UserInfo["sender"].(string); ok { + fmt.Printf("Original sender: %s\n", data) + } + + // Emit an event to the frontend + app.Event.Emit("notification", result.Response) +}) +``` + +## Notification Customisation + +### Custom Metadata + +Basic and interactive notifications can include custom data: + +```go +notifier.SendNotification(notifications.NotificationOptions{ + ID: "unique-id", + Title: "New Calendar Invite", + Subtitle: "From: Jane Doe", // Optional + Body: "Tap to view the event", + Data: map[string]interface{}{ + "sender": "jane.doe@example.com", + "timestamp": "2025-03-10T15:30:00Z", + } +}) + +``` + +## Platform Considerations + + + + + On macOS, notifications: + + - Require user authorization + - Require the app to be notorized for distribution + - Use system-standard notification appearances + - Support `subtitle` + - Support user text input + - Support the `Destructive` action option + - Automatically handle dark/light mode + + + + + + On Windows, notifications: + + - Use Windows system toast styles + - Adapt to Windows theme settings + - Support user text input + - Support high DPI displays + - Do not support `subtitle` + + + + + + On Linux, dialog behaviour depends on the desktop environment: + + - Use native notifications when available + - Follow desktop environment theme + - Position according to desktop environment rules + - Support `subtitle` + - Do not support user text input + + + + +## Best Practices + +1. Check and request for authorization: + - macOS requires user authorization + +2. Provide clear and concise notifications: + - Use descriptive titles, subtitles, text, and action titles + +3. Handle dialog responses appropriately: + - Check for errors in notification responses + - Provide feedback for user actions + +4. Consider platform conventions: + - Follow platform-specific notification patterns + - Respect system settings + +## Examples + +Explore this example: + +- [Notifications](/examples/notifications) + +## API Reference + +### Service Management +| Method | Description | +|--------------------------------------------|-------------------------------------------------------| +| `New()` | Creates a new notifications service | + +### Notification Authorization +| Method | Description | +|----------------------------------------------|------------------------------------------------------------| +| `RequestNotificationAuthorization()` | Requests permission to display notifications (macOS) | +| `CheckNotificationAuthorization()` | Checks current notification authorization status (macOS) | + +### Sending Notifications +| Method | Description | +|------------------------------------------------------------|---------------------------------------------------| +| `SendNotification(options NotificationOptions)` | Sends a basic notification | +| `SendNotificationWithActions(options NotificationOptions)` | Sends an interactive notification with actions | + +### Notification Categories +| Method | Description | +|---------------------------------------------------------------|---------------------------------------------------| +| `RegisterNotificationCategory(category NotificationCategory)` | Registers a reusable notification category | +| `RemoveNotificationCategory(categoryID string)` | Removes a previously registered category | + +### Managing Notifications +| Method | Description | +|-------------------------------------------------|---------------------------------------------------------------------| +| `RemoveAllPendingNotifications()` | Removes all pending notifications (macOS and Linux only) | +| `RemovePendingNotification(identifier string)` | Removes a specific pending notification (macOS and Linux only) | +| `RemoveAllDeliveredNotifications()` | Removes all delivered notifications (macOS and Linux only) | +| `RemoveDeliveredNotification(identifier string)`| Removes a specific delivered notification (macOS and Linux only) | +| `RemoveNotification(identifier string)` | Removes a notification (Linux-specific) | + +### Event Handling +| Method | Description | +|--------------------------------------------------------------------|-------------------------------------------------| +| `OnNotificationResponse(callback func(result NotificationResult))` | Registers a callback for notification responses | + +### Structs and Types + +#### NotificationOptions +```go +type NotificationOptions struct { + ID string // Unique identifier for the notification + Title string // Main notification title + Subtitle string // Subtitle text (macOS and Linux only) + Body string // Main notification content + CategoryID string // Category identifier for interactive notifications + Data map[string]interface{} // Custom data to associate with the notification +} +``` + +#### NotificationCategory +```go +type NotificationCategory struct { + ID string // Unique identifier for the category + Actions []NotificationAction // Button actions for the notification + HasReplyField bool // Whether to include a text input field + ReplyPlaceholder string // Placeholder text for the input field + ReplyButtonTitle string // Text for the reply button +} +``` + +#### NotificationAction +```go +type NotificationAction struct { + ID string // Unique identifier for the action + Title string // Button text + Destructive bool // Whether the action is destructive (macOS-specific) +} +``` + +#### NotificationResponse +```go +type NotificationResponse struct { + ID string // Notification identifier + ActionIdentifier string // Action that was triggered + CategoryID string // Category of the notification + Title string // Title of the notification + Subtitle string // Subtitle of the notification + Body string // Body text of the notification + UserText string // Text entered by the user + UserInfo map[string]interface{} // Custom data from the notification +} +``` + +#### NotificationResult +```go +type NotificationResult struct { + Response NotificationResponse // Response data + Error error // Any error that occurred +} +``` \ No newline at end of file diff --git a/docs/src/content/docs/learn/runtime.mdx b/docs/src/content/docs/learn/runtime.mdx index 3c2fbfc33..7b66a06b3 100644 --- a/docs/src/content/docs/learn/runtime.mdx +++ b/docs/src/content/docs/learn/runtime.mdx @@ -22,12 +22,12 @@ The runtime is required for integration between Go and the frontend. There are 2 ways to integrate the runtime: - Using the `@wailsio/runtime` package -- Using a pre-built version of the runtime +- Using a pre-built bundle ## Using the npm package The `@wailsio/runtime` package is a JavaScript package that provides access to -the Wails runtime from the frontend. It is used in by all the standard templates +the Wails runtime from the frontend. It is used by all standard templates and is the recommended way to integrate the runtime into your application. By using the `@wailsio/runtime` package, you will only include the parts of the runtime that you use. @@ -37,25 +37,55 @@ The package is available on npm and can be installed using: npm install --save @wailsio/runtime ``` -## Using a pre-built local version of the runtime +## Using a pre-built bundle Some projects will not use a Javascript bundler and may prefer to use a -pre-built version of the runtime. This is the default for the examples in -`v3/examples`. The pre-built version of the runtime can be generated using the -following command: +pre-built bundled version of the runtime. This version can be generated locally +using the following command: ```shell wails3 generate runtime ``` -This will generate a `runtime.js` (and `runtime.debug.js`) file in the current -directory. This file can be used by your application by adding it to your frontend project: +The command will output a `runtime.js` (and `runtime.debug.js`) file in the current +directory. This file is an ES module that can be imported by your application scripts +just like the npm package, but the API is also exported to the global window object, +so for simpler applications you can use it as follows: ```html - + + - + ``` + +:::caution +It is important to include the `type="module"` attribute on the ` + + +
+ +

Wails + Typescript

+
Set a badge label below πŸ‘‡
+
+
+ + + + + + +
+
+ +
+ + diff --git a/v3/examples/badge-custom/frontend/dist/style.css b/v3/examples/badge-custom/frontend/dist/style.css new file mode 100644 index 000000000..6ce81cad2 --- /dev/null +++ b/v3/examples/badge-custom/frontend/dist/style.css @@ -0,0 +1,155 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +h3 { + font-size: 3em; + line-height: 1.1; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +button { + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.result { + height: 20px; + line-height: 20px; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + text-align: center; +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + color: black; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v3/examples/badge-custom/frontend/dist/typescript.svg b/v3/examples/badge-custom/frontend/dist/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/v3/examples/badge-custom/frontend/dist/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/badge-custom/frontend/dist/wails.png b/v3/examples/badge-custom/frontend/dist/wails.png new file mode 100644 index 000000000..8bdf42483 Binary files /dev/null and b/v3/examples/badge-custom/frontend/dist/wails.png differ diff --git a/v3/examples/badge-custom/frontend/index.html b/v3/examples/badge-custom/frontend/index.html new file mode 100644 index 000000000..e4a9ec4a2 --- /dev/null +++ b/v3/examples/badge-custom/frontend/index.html @@ -0,0 +1,39 @@ + + + + + + + + Wails App + + +
+ +

Wails + Typescript

+
Set a badge label below πŸ‘‡
+
+
+ + + + + + +
+
+ +
+ + + diff --git a/v3/examples/badge-custom/frontend/package.json b/v3/examples/badge-custom/frontend/package.json new file mode 100644 index 000000000..b39da7ece --- /dev/null +++ b/v3/examples/badge-custom/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build:dev": "tsc && vite build --minify false --mode development", + "build": "tsc && vite build --mode production", + "preview": "vite preview" + }, + "dependencies": { + "@wailsio/runtime": "latest" + }, + "devDependencies": { + "typescript": "^4.9.3", + "vite": "^5.0.0" + } +} diff --git a/v3/examples/badge-custom/frontend/public/Inter-Medium.ttf b/v3/examples/badge-custom/frontend/public/Inter-Medium.ttf new file mode 100644 index 000000000..a01f3777a Binary files /dev/null and b/v3/examples/badge-custom/frontend/public/Inter-Medium.ttf differ diff --git a/v3/examples/badge-custom/frontend/public/style.css b/v3/examples/badge-custom/frontend/public/style.css new file mode 100644 index 000000000..6ce81cad2 --- /dev/null +++ b/v3/examples/badge-custom/frontend/public/style.css @@ -0,0 +1,155 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +h3 { + font-size: 3em; + line-height: 1.1; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +button { + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.result { + height: 20px; + line-height: 20px; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + text-align: center; +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + color: black; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v3/examples/badge-custom/frontend/public/typescript.svg b/v3/examples/badge-custom/frontend/public/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/v3/examples/badge-custom/frontend/public/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/badge-custom/frontend/public/wails.png b/v3/examples/badge-custom/frontend/public/wails.png new file mode 100644 index 000000000..8bdf42483 Binary files /dev/null and b/v3/examples/badge-custom/frontend/public/wails.png differ diff --git a/v3/examples/badge-custom/frontend/src/main.ts b/v3/examples/badge-custom/frontend/src/main.ts new file mode 100644 index 000000000..405f4fe28 --- /dev/null +++ b/v3/examples/badge-custom/frontend/src/main.ts @@ -0,0 +1,59 @@ +import {Events} from "@wailsio/runtime"; +import {SetBadge, RemoveBadge, SetCustomBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/badgeservice"; +import { RGBA } from "../bindings/image/color/models"; + +const setCustomButton = document.getElementById('set-custom')! as HTMLButtonElement; +const setButton = document.getElementById('set')! as HTMLButtonElement; +const removeButton = document.getElementById('remove')! as HTMLButtonElement; +const setButtonUsingGo = document.getElementById('set-go')! as HTMLButtonElement; +const removeButtonUsingGo = document.getElementById('remove-go')! as HTMLButtonElement; +const labelElement : HTMLInputElement = document.getElementById('label')! as HTMLInputElement; +const timeElement = document.getElementById('time')! as HTMLDivElement; + +setCustomButton.addEventListener('click', () => { + console.log("click!") + let label = (labelElement as HTMLInputElement).value + SetCustomBadge(label, { + BackgroundColour: RGBA.createFrom({ + R: 0, + G: 255, + B: 255, + A: 255, + }), + FontName: "arialb.ttf", // System font + FontSize: 16, + SmallFontSize: 10, + TextColour: RGBA.createFrom({ + R: 0, + G: 0, + B: 0, + A: 255, + }), + }); +}) + +setButton.addEventListener('click', () => { + let label = (labelElement as HTMLInputElement).value + SetBadge(label); +}); + +removeButton.addEventListener('click', () => { + RemoveBadge(); +}); + +setButtonUsingGo.addEventListener('click', () => { + let label = (labelElement as HTMLInputElement).value + void Events.Emit({ + name: "set:badge", + data: label, + }) +}) + +removeButtonUsingGo.addEventListener('click', () => { + void Events.Emit({name:"remove:badge", data: null}) +}) + +Events.On('time', (time: {data: any}) => { + timeElement.innerText = time.data; +}); + diff --git a/v3/examples/badge-custom/frontend/src/vite-env.d.ts b/v3/examples/badge-custom/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/v3/examples/badge-custom/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/v3/examples/badge-custom/frontend/tsconfig.json b/v3/examples/badge-custom/frontend/tsconfig.json new file mode 100644 index 000000000..c267ecf24 --- /dev/null +++ b/v3/examples/badge-custom/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/v3/examples/badge-custom/main.go b/v3/examples/badge-custom/main.go new file mode 100644 index 000000000..e27103443 --- /dev/null +++ b/v3/examples/badge-custom/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "embed" + _ "embed" + "image/color" + "log" + "time" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/services/badge" +) + +// Wails uses Go's `embed` package to embed the frontend files into the binary. +// Any files in the frontend/dist folder will be embedded into the binary and +// made available to the frontend. +// See https://pkg.go.dev/embed for more information. + +//go:embed all:frontend/dist +var assets embed.FS + +// main function serves as the application's entry point. It initializes the application, creates a window, +// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and +// logs any error that might occur. +func main() { + // Create a new Wails application by providing the necessary options. + // Variables 'Name' and 'Description' are for application metadata. + // 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files. + // 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances. + // 'Mac' options tailor the application when running an macOS. + + badgeService := badge.NewWithOptions(badge.Options{ + TextColour: color.RGBA{255, 255, 204, 255}, + BackgroundColour: color.RGBA{16, 124, 16, 255}, + FontName: "consolab.ttf", + FontSize: 20, + SmallFontSize: 14, + }) + + app := application.New(application.Options{ + Name: "badge", + Description: "A demo of using raw HTML & CSS", + Services: []application.Service{ + application.NewService(badgeService), + }, + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + // Create a new window with the necessary options. + // 'Title' is the title of the window. + // 'Mac' options tailor the window when running on macOS. + // 'BackgroundColour' is the background colour of the window. + // 'URL' is the URL that will be loaded into the webview. + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Window 1", + Mac: application.MacWindow{ + InvisibleTitleBarHeight: 50, + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInset, + }, + BackgroundColour: application.NewRGB(27, 38, 54), + URL: "/", + }) + + app.Event.On("remove:badge", func(event *application.CustomEvent) { + err := badgeService.RemoveBadge() + if err != nil { + log.Fatal(err) + } + }) + + app.Event.On("set:badge", func(event *application.CustomEvent) { + text := event.Data.(string) + err := badgeService.SetBadge(text) + if err != nil { + log.Fatal(err) + } + }) + + // Create a goroutine that emits an event containing the current time every second. + // The frontend can listen to this event and update the UI accordingly. + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + now := time.Now().Format(time.RFC1123) + app.Event.Emit("time", now) + case <-app.Context().Done(): + return + } + } + }() + + // Run the application. This blocks until the application has been exited. + err := app.Run() + + // If an error occurred while running the application, log it and exit. + if err != nil { + log.Fatal(err) + } +} diff --git a/v3/examples/badge/README.md b/v3/examples/badge/README.md new file mode 100644 index 000000000..abb7b9653 --- /dev/null +++ b/v3/examples/badge/README.md @@ -0,0 +1,71 @@ +# Welcome to Your New Wails3 Project! +Now that you have your project set up, it's time to explore the basic badge features that Wails3 offers on **macOS** and **Windows**. + +## Exploring Badge Features + +### Creating the Service + +First, initialize the badge service: + +```go +import "github.com/wailsapp/wails/v3/pkg/application" +import "github.com/wailsapp/wails/v3/pkg/services/badge" + +// Create a new badge service +badgeService := badge.New() + +// Register the service with the application +app := application.New(application.Options{ + Services: []application.Service{ + application.NewService(badgeService), + }, +}) +``` + +## Badge Operations + +### Setting a Badge + +Set a badge on the application tile/dock icon: + +#### Go +```go +// Set a default badge +badgeService.SetBadge("") + +// Set a numeric badge +badgeService.SetBadge("3") + +// Set a text badge +badgeService.SetBadge("New") +``` + +#### JS +```js +import {SetBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; + +// Set a default badge +SetBadge("") + +// Set a numeric badge +SetBadge("3") + +// Set a text badge +SetBadge("New") +``` + +### Removing a Badge + +Remove the badge from the application icon: + +#### Go +```go +badgeService.RemoveBadge() +``` + +#### JS +```js +import {RemoveBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; + +RemoveBadge() +``` \ No newline at end of file diff --git a/v3/examples/badge/Taskfile.yml b/v3/examples/badge/Taskfile.yml new file mode 100644 index 000000000..d1bcfaacf --- /dev/null +++ b/v3/examples/badge/Taskfile.yml @@ -0,0 +1,34 @@ +version: '3' + +includes: + common: ./build/Taskfile.yml + windows: ./build/windows/Taskfile.yml + darwin: ./build/darwin/Taskfile.yml + linux: ./build/linux/Taskfile.yml + +vars: + APP_NAME: "badge" + BIN_DIR: "bin" + VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' + +tasks: + build: + summary: Builds the application + cmds: + - task: "{{OS}}:build" + + package: + summary: Packages a production build of the application + cmds: + - task: "{{OS}}:package" + + run: + summary: Runs the application + cmds: + - task: "{{OS}}:run" + + dev: + summary: Runs the application in development mode + cmds: + - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} + diff --git a/v3/examples/badge/build/Taskfile.yml b/v3/examples/badge/build/Taskfile.yml new file mode 100644 index 000000000..5f3517efc --- /dev/null +++ b/v3/examples/badge/build/Taskfile.yml @@ -0,0 +1,86 @@ +version: '3' + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + dir: frontend + sources: + - package.json + - package-lock.json + generates: + - node_modules/* + preconditions: + - sh: npm version + msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/" + cmds: + - npm install + + build:frontend: + label: build:frontend (PRODUCTION={{.PRODUCTION}}) + summary: Build the frontend project + dir: frontend + sources: + - "**/*" + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - npm run {{.BUILD_COMMAND}} -q + env: + PRODUCTION: '{{.PRODUCTION | default "false"}}' + vars: + BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build:dev{{end}}' + + + generate:bindings: + label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) + summary: Generates bindings for the frontend + deps: + - task: go:mod:tidy + sources: + - "**/*.[jt]s" + - exclude: frontend/**/* + - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true -ts + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` files from an image + dir: build + sources: + - "appicon.png" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico + + dev:frontend: + summary: Runs the frontend in development mode + dir: frontend + deps: + - task: install:frontend:deps + cmds: + - npm run dev -- --port {{.VITE_PORT}} --strictPort + + update:build-assets: + summary: Updates the build assets + dir: build + cmds: + - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . diff --git a/v3/examples/badge/build/appicon.png b/v3/examples/badge/build/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/v3/examples/badge/build/appicon.png differ diff --git a/v3/examples/badge/build/config.yml b/v3/examples/badge/build/config.yml new file mode 100644 index 000000000..aaa3240fb --- /dev/null +++ b/v3/examples/badge/build/config.yml @@ -0,0 +1,63 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: '3' + +# This information is used to generate the build assets. +info: + companyName: "My Company" # The name of the company + productName: "My Product" # The name of the application + productIdentifier: "com.mycompany.myproduct" # The unique product identifier + description: "A program that does X" # The application description + copyright: "(c) 2025, My Company" # Copyright text + comments: "Some Product Comments" # Comments + version: "0.0.1" # The application version + +# Dev mode configuration +dev_mode: + root_path: . + log_level: warn + debounce: 1000 + ignore: + dir: + - .git + - node_modules + - frontend + - bin + file: + - .DS_Store + - .gitignore + - .gitkeep + watched_extension: + - "*.go" + git_ignore: true + executes: + - cmd: wails3 task common:install:frontend:deps + type: once + - cmd: wails3 task common:dev:frontend + type: background + - cmd: go mod tidy + type: blocking + - cmd: wails3 task build + type: blocking + - cmd: wails3 task run + type: primary + +# File Associations +# More information at: https://v3.wails.io/noit/done/yet +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor +# mimeType: image/jpeg # (optional) + +# Other data +other: + - name: My Other Data \ No newline at end of file diff --git a/v3/examples/badge/build/darwin/Info.dev.plist b/v3/examples/badge/build/darwin/Info.dev.plist new file mode 100644 index 000000000..4caddf720 --- /dev/null +++ b/v3/examples/badge/build/darwin/Info.dev.plist @@ -0,0 +1,32 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + badge + CFBundleIdentifier + com.wails.badge + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + Β© now, My Company + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + \ No newline at end of file diff --git a/v3/examples/badge/build/darwin/Info.plist b/v3/examples/badge/build/darwin/Info.plist new file mode 100644 index 000000000..0dc90b2e7 --- /dev/null +++ b/v3/examples/badge/build/darwin/Info.plist @@ -0,0 +1,27 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + badge + CFBundleIdentifier + com.wails.badge + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + Β© now, My Company + + \ No newline at end of file diff --git a/v3/examples/badge/build/darwin/Taskfile.yml b/v3/examples/badge/build/darwin/Taskfile.yml new file mode 100644 index 000000000..f0791fea9 --- /dev/null +++ b/v3/examples/badge/build/darwin/Taskfile.yml @@ -0,0 +1,81 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Creates a production build of the application + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: darwin + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + CGO_CFLAGS: "-mmacosx-version-min=10.15" + CGO_LDFLAGS: "-mmacosx-version-min=10.15" + MACOSX_DEPLOYMENT_TARGET: "10.15" + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + build:universal: + summary: Builds darwin universal binary (arm64 + amd64) + deps: + - task: build + vars: + ARCH: amd64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" + - task: build + vars: + ARCH: arm64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + cmds: + - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + package: + summary: Packages a production build of the application into a `.app` bundle + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:app:bundle + + package:universal: + summary: Packages darwin universal binary (arm64 + amd64) + deps: + - task: build:universal + cmds: + - task: create:app:bundle + + + create:app:bundle: + summary: Creates an `.app` bundle + cmds: + - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} + - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources + - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS + - cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents + - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app + + run: + cmds: + - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources} + - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources + - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS + - cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist + - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app + - '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}' diff --git a/v3/examples/badge/build/darwin/icons.icns b/v3/examples/badge/build/darwin/icons.icns new file mode 100644 index 000000000..1b5bd4c86 Binary files /dev/null and b/v3/examples/badge/build/darwin/icons.icns differ diff --git a/v3/examples/badge/build/linux/Taskfile.yml b/v3/examples/badge/build/linux/Taskfile.yml new file mode 100644 index 000000000..560cc9c92 --- /dev/null +++ b/v3/examples/badge/build/linux/Taskfile.yml @@ -0,0 +1,119 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds the application for Linux + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + env: + GOOS: linux + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application for Linux + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:appimage + - task: create:deb + - task: create:rpm + - task: create:aur + + create:appimage: + summary: Creates an AppImage + dir: build/linux/appimage + deps: + - task: build + vars: + PRODUCTION: "true" + - task: generate:dotdesktop + cmds: + - cp {{.APP_BINARY}} {{.APP_NAME}} + - cp ../../appicon.png appicon.png + - wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build + vars: + APP_NAME: '{{.APP_NAME}}' + APP_BINARY: '../../../bin/{{.APP_NAME}}' + ICON: '../../appicon.png' + DESKTOP_FILE: '../{{.APP_NAME}}.desktop' + OUTPUT_DIR: '../../../bin' + + create:deb: + summary: Creates a deb package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:deb + + create:rpm: + summary: Creates a rpm package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:rpm + + create:aur: + summary: Creates a arch linux packager package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:aur + + generate:deb: + summary: Creates a deb package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:rpm: + summary: Creates a rpm package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:aur: + summary: Creates a arch linux packager package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:dotdesktop: + summary: Generates a `.desktop` file + dir: build + cmds: + - mkdir -p {{.ROOT_DIR}}/build/linux/appimage + - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}" + vars: + APP_NAME: '{{.APP_NAME}}' + EXEC: '{{.APP_NAME}}' + ICON: 'appicon' + CATEGORIES: 'Development;' + OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}' diff --git a/v3/examples/badge/build/linux/appimage/build.sh b/v3/examples/badge/build/linux/appimage/build.sh new file mode 100644 index 000000000..85901c34e --- /dev/null +++ b/v3/examples/badge/build/linux/appimage/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) 2018-Present Lea Anthony +# SPDX-License-Identifier: MIT + +# Fail script on any error +set -euxo pipefail + +# Define variables +APP_DIR="${APP_NAME}.AppDir" + +# Create AppDir structure +mkdir -p "${APP_DIR}/usr/bin" +cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" +cp "${ICON_PATH}" "${APP_DIR}/" +cp "${DESKTOP_FILE}" "${APP_DIR}/" + +if [[ $(uname -m) == *x86_64* ]]; then + # Download linuxdeploy and make it executable + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + + # Run linuxdeploy to bundle the application + ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage +else + # Download linuxdeploy and make it executable (arm64) + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage + chmod +x linuxdeploy-aarch64.AppImage + + # Run linuxdeploy to bundle the application (arm64) + ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage +fi + +# Rename the generated AppImage +mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage" + diff --git a/v3/examples/badge/build/linux/nfpm/nfpm.yaml b/v3/examples/badge/build/linux/nfpm/nfpm.yaml new file mode 100644 index 000000000..5b7ea9d02 --- /dev/null +++ b/v3/examples/badge/build/linux/nfpm/nfpm.yaml @@ -0,0 +1,50 @@ +# Feel free to remove those if you don't want/need to use them. +# Make sure to check the documentation at https://nfpm.goreleaser.com +# +# The lines below are called `modelines`. See `:help modeline` + +name: "badge" +arch: ${GOARCH} +platform: "linux" +version: "0.1.0" +section: "default" +priority: "extra" +maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> +description: "My Product Description" +vendor: "My Company" +homepage: "https://wails.io" +license: "MIT" +release: "1" + +contents: + - src: "./bin/badge" + dst: "/usr/local/bin/badge" + - src: "./build/appicon.png" + dst: "/usr/share/icons/hicolor/128x128/apps/badge.png" + - src: "./build/linux/badge.desktop" + dst: "/usr/share/applications/badge.desktop" + +depends: + - gtk3 + - libwebkit2gtk + +# replaces: +# - foobar +# provides: +# - bar +# depends: +# - gtk3 +# - libwebkit2gtk +# recommends: +# - whatever +# suggests: +# - something-else +# conflicts: +# - not-foo +# - not-bar +# changelog: "changelog.yaml" +# scripts: +# preinstall: ./build/linux/nfpm/scripts/preinstall.sh +# postinstall: ./build/linux/nfpm/scripts/postinstall.sh +# preremove: ./build/linux/nfpm/scripts/preremove.sh +# postremove: ./build/linux/nfpm/scripts/postremove.sh diff --git a/v3/examples/badge/build/linux/nfpm/scripts/postinstall.sh b/v3/examples/badge/build/linux/nfpm/scripts/postinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge/build/linux/nfpm/scripts/postinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge/build/linux/nfpm/scripts/postremove.sh b/v3/examples/badge/build/linux/nfpm/scripts/postremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge/build/linux/nfpm/scripts/postremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge/build/linux/nfpm/scripts/preinstall.sh b/v3/examples/badge/build/linux/nfpm/scripts/preinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge/build/linux/nfpm/scripts/preinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge/build/linux/nfpm/scripts/preremove.sh b/v3/examples/badge/build/linux/nfpm/scripts/preremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge/build/linux/nfpm/scripts/preremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge/build/windows/Taskfile.yml b/v3/examples/badge/build/windows/Taskfile.yml new file mode 100644 index 000000000..534f4fb31 --- /dev/null +++ b/v3/examples/badge/build/windows/Taskfile.yml @@ -0,0 +1,63 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds the application for Windows + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + env: + GOOS: windows + CGO_ENABLED: 0 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application into a `.exe` bundle + cmds: + - task: create:nsis:installer + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: '{{.ARCH | default ARCH}}' + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + vars: + ARCH: '{{.ARCH | default ARCH}}' + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}.exe' diff --git a/v3/examples/badge/build/windows/icon.ico b/v3/examples/badge/build/windows/icon.ico new file mode 100644 index 000000000..bfa0690b7 Binary files /dev/null and b/v3/examples/badge/build/windows/icon.ico differ diff --git a/v3/examples/badge/build/windows/info.json b/v3/examples/badge/build/windows/info.json new file mode 100644 index 000000000..850b2b5b0 --- /dev/null +++ b/v3/examples/badge/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "0.1.0" + }, + "info": { + "0000": { + "ProductVersion": "0.1.0", + "CompanyName": "My Company", + "FileDescription": "My Product Description", + "LegalCopyright": "Β© now, My Company", + "ProductName": "My Product", + "Comments": "This is a comment" + } + } +} \ No newline at end of file diff --git a/v3/examples/badge/build/windows/nsis/project.nsi b/v3/examples/badge/build/windows/nsis/project.nsi new file mode 100644 index 000000000..985b8e207 --- /dev/null +++ b/v3/examples/badge/build/windows/nsis/project.nsi @@ -0,0 +1,112 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the wails_tools.nsh file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "my-project" # Default "badge" +## !define INFO_COMPANYNAME "My Company" # Default "My Company" +## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "Β© now, My Company" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/v3/examples/badge/build/windows/nsis/wails_tools.nsh b/v3/examples/badge/build/windows/nsis/wails_tools.nsh new file mode 100644 index 000000000..6fc10ab79 --- /dev/null +++ b/v3/examples/badge/build/windows/nsis/wails_tools.nsh @@ -0,0 +1,212 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "badge" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "My Company" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "My Product" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "0.1.0" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "Β© now, My Company" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + +!macroend \ No newline at end of file diff --git a/v3/examples/badge/build/windows/wails.exe.manifest b/v3/examples/badge/build/windows/wails.exe.manifest new file mode 100644 index 000000000..fcfd2fc46 --- /dev/null +++ b/v3/examples/badge/build/windows/wails.exe.manifest @@ -0,0 +1,22 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + + + + + + + \ No newline at end of file diff --git a/v3/examples/badge/frontend/Inter Font License.txt b/v3/examples/badge/frontend/Inter Font License.txt new file mode 100644 index 000000000..b525cbf3a --- /dev/null +++ b/v3/examples/badge/frontend/Inter Font License.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/badgeservice.ts b/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/badgeservice.ts new file mode 100644 index 000000000..f959d50dc --- /dev/null +++ b/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/badgeservice.ts @@ -0,0 +1,33 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * Service represents the notifications service + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +/** + * RemoveBadge removes the badge label from the application icon. + */ +export function RemoveBadge(): $CancellablePromise { + return $Call.ByID(2374916939); +} + +/** + * SetBadge sets the badge label on the application icon. + */ +export function SetBadge(label: string): $CancellablePromise { + return $Call.ByID(784276339, label); +} + +export function SetCustomBadge(label: string, options: $models.Options): $CancellablePromise { + return $Call.ByID(3058653106, label, options); +} diff --git a/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts b/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts new file mode 100644 index 000000000..df3ea9723 --- /dev/null +++ b/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts @@ -0,0 +1,11 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as BadgeService from "./badgeservice.js"; +export { + BadgeService +}; + +export { + Options +} from "./models.js"; diff --git a/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/models.ts b/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/models.ts new file mode 100644 index 000000000..67ed264c0 --- /dev/null +++ b/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/models.ts @@ -0,0 +1,58 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as color$0 from "../../../../../../../image/color/models.js"; + +export class Options { + "TextColour": color$0.RGBA; + "BackgroundColour": color$0.RGBA; + "FontName": string; + "FontSize": number; + "SmallFontSize": number; + + /** Creates a new Options instance. */ + constructor($$source: Partial = {}) { + if (!("TextColour" in $$source)) { + this["TextColour"] = (new color$0.RGBA()); + } + if (!("BackgroundColour" in $$source)) { + this["BackgroundColour"] = (new color$0.RGBA()); + } + if (!("FontName" in $$source)) { + this["FontName"] = ""; + } + if (!("FontSize" in $$source)) { + this["FontSize"] = 0; + } + if (!("SmallFontSize" in $$source)) { + this["SmallFontSize"] = 0; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new Options instance from a string or object. + */ + static createFrom($$source: any = {}): Options { + const $$createField0_0 = $$createType0; + const $$createField1_0 = $$createType0; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("TextColour" in $$parsedSource) { + $$parsedSource["TextColour"] = $$createField0_0($$parsedSource["TextColour"]); + } + if ("BackgroundColour" in $$parsedSource) { + $$parsedSource["BackgroundColour"] = $$createField1_0($$parsedSource["BackgroundColour"]); + } + return new Options($$parsedSource as Partial); + } +} + +// Private type creation functions +const $$createType0 = color$0.RGBA.createFrom; diff --git a/v3/examples/badge/frontend/bindings/image/color/index.ts b/v3/examples/badge/frontend/bindings/image/color/index.ts new file mode 100644 index 000000000..97b507b08 --- /dev/null +++ b/v3/examples/badge/frontend/bindings/image/color/index.ts @@ -0,0 +1,6 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +export { + RGBA +} from "./models.js"; diff --git a/v3/examples/badge/frontend/bindings/image/color/models.ts b/v3/examples/badge/frontend/bindings/image/color/models.ts new file mode 100644 index 000000000..0d4eab56d --- /dev/null +++ b/v3/examples/badge/frontend/bindings/image/color/models.ts @@ -0,0 +1,46 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +/** + * RGBA represents a traditional 32-bit alpha-premultiplied color, having 8 + * bits for each of red, green, blue and alpha. + * + * An alpha-premultiplied color component C has been scaled by alpha (A), so + * has valid values 0 <= C <= A. + */ +export class RGBA { + "R": number; + "G": number; + "B": number; + "A": number; + + /** Creates a new RGBA instance. */ + constructor($$source: Partial = {}) { + if (!("R" in $$source)) { + this["R"] = 0; + } + if (!("G" in $$source)) { + this["G"] = 0; + } + if (!("B" in $$source)) { + this["B"] = 0; + } + if (!("A" in $$source)) { + this["A"] = 0; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new RGBA instance from a string or object. + */ + static createFrom($$source: any = {}): RGBA { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new RGBA($$parsedSource as Partial); + } +} diff --git a/v3/examples/badge/frontend/dist/Inter-Medium.ttf b/v3/examples/badge/frontend/dist/Inter-Medium.ttf new file mode 100644 index 000000000..a01f3777a Binary files /dev/null and b/v3/examples/badge/frontend/dist/Inter-Medium.ttf differ diff --git a/v3/examples/badge/frontend/dist/assets/index-djydTGr9.js b/v3/examples/badge/frontend/dist/assets/index-djydTGr9.js new file mode 100644 index 000000000..130eddeb5 --- /dev/null +++ b/v3/examples/badge/frontend/dist/assets/index-djydTGr9.js @@ -0,0 +1,6 @@ +(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(o){if(o.ep)return;o.ep=!0;const i=n(o);fetch(o.href,i)}})();const ce="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";function $(t=21){let e="",n=t|0;for(;n--;)e+=ce[Math.random()*64|0];return e}const ae=window.location.origin+"/wails/runtime",C=Object.freeze({Call:0,Clipboard:1,Application:2,Events:3,ContextMenu:4,Dialog:5,Window:6,Screens:7,System:8,Browser:9,CancelCall:10});let ue=$();function z(t,e=""){return function(n,r=null){return de(t,n,e,r)}}async function de(t,e,n,r){var o,i;let s=new URL(ae);s.searchParams.append("object",t.toString()),s.searchParams.append("method",e.toString()),r&&s.searchParams.append("args",JSON.stringify(r));let c={"x-wails-client-id":ue};n&&(c["x-wails-window-name"]=n);let l=await fetch(s,{headers:c});if(!l.ok)throw new Error(await l.text());return((i=(o=l.headers.get("Content-Type"))===null||o===void 0?void 0:o.indexOf("application/json"))!==null&&i!==void 0?i:-1)!==-1?l.json():l.text()}z(C.System);const P=function(){var t,e,n,r,o;try{if(!((e=(t=window.chrome)===null||t===void 0?void 0:t.webview)===null||e===void 0)&&e.postMessage)return window.chrome.webview.postMessage.bind(window.chrome.webview);if(!((o=(r=(n=window.webkit)===null||n===void 0?void 0:n.messageHandlers)===null||r===void 0?void 0:r.external)===null||o===void 0)&&o.postMessage)return window.webkit.messageHandlers.external.postMessage.bind(window.webkit.messageHandlers.external)}catch{}return console.warn(` +%c⚠️ Browser Environment Detected %c + +%cOnly UI previews are available in the browser. For full functionality, please run the application in desktop mode. +More information at: https://v3.wails.io/learn/build/#using-a-browser-for-development +`,"background: #ffffff; color: #000000; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 2px solid #000000;","background: transparent;","color: #ffffff; font-style: italic; font-weight: bold;"),null}();function R(t){P==null||P(t)}function Q(){return window._wails.environment.OS==="windows"}function fe(){return!!window._wails.environment.Debug}function we(){return new MouseEvent("mousedown").buttons===0}function Z(t){var e;return t.target instanceof HTMLElement?t.target:!(t.target instanceof HTMLElement)&&t.target instanceof Node&&(e=t.target.parentElement)!==null&&e!==void 0?e:document.body}document.addEventListener("DOMContentLoaded",()=>{});window.addEventListener("contextmenu",ge);const pe=z(C.ContextMenu),me=0;function he(t,e,n,r){pe(me,{id:t,x:e,y:n,data:r})}function ge(t){const e=Z(t),n=window.getComputedStyle(e).getPropertyValue("--custom-contextmenu").trim();if(n){t.preventDefault();const r=window.getComputedStyle(e).getPropertyValue("--custom-contextmenu-data");he(n,t.clientX,t.clientY,r)}else ye(t,e)}function ye(t,e){if(fe())return;switch(window.getComputedStyle(e).getPropertyValue("--default-contextmenu").trim()){case"show":return;case"hide":t.preventDefault();return}if(e.isContentEditable)return;const n=window.getSelection(),r=n&&n.toString().length>0;if(r)for(let o=0;o{F=t,F||(v=E=!1,u())};window.addEventListener("mousedown",N,{capture:!0});window.addEventListener("mousemove",N,{capture:!0});window.addEventListener("mouseup",N,{capture:!0});for(const t of["click","contextmenu","dblclick"])window.addEventListener(t,be,{capture:!0});function be(t){(S||E)&&(t.stopImmediatePropagation(),t.stopPropagation(),t.preventDefault())}const B=0,ve=1,D=2;function N(t){let e,n=t.buttons;switch(t.type){case"mousedown":e=B,H||(n=h|1<"u"||typeof e=="object"))try{var n=L.call(e);return(n===Le||n===Re||n===Te||n===ze)&&e("")==null}catch{}return!1})}function He(t){if(Y(t))return!0;if(!t||typeof t!="function"&&typeof t!="object")return!1;try{y(t,null,_)}catch(e){if(e!==O)return!1}return!X(t)&&U(t)}function Be(t){if(Y(t))return!0;if(!t||typeof t!="function"&&typeof t!="object")return!1;if(ke)return U(t);if(X(t))return!1;var e=L.call(t);return e!==Me&&e!==Oe&&!/^\[object HTML/.test(e)?!1:U(t)}const m=y?He:Be;var I;class W extends Error{constructor(e,n){super(e,n),this.name="CancelError"}}class x extends Error{constructor(e,n,r){super((r??"Unhandled rejection in cancelled promise.")+" Reason: "+De(n),{cause:n}),this.promise=e,this.name="CancelledRejectionError"}}const f=Symbol("barrier"),J=Symbol("cancelImpl"),V=(I=Symbol.species)!==null&&I!==void 0?I:Symbol("speciesPolyfill");class a extends Promise{constructor(e,n){let r,o;if(super((l,d)=>{r=l,o=d}),this.constructor[V]!==Promise)throw new TypeError("CancellablePromise does not support transparent subclassing. Please refrain from overriding the [Symbol.species] static property.");let i={promise:this,resolve:r,reject:o,get oncancelled(){return n??null},set oncancelled(l){n=l??void 0}};const s={get root(){return s},resolving:!1,settled:!1};Object.defineProperties(this,{[f]:{configurable:!1,enumerable:!1,writable:!0,value:null},[J]:{configurable:!1,enumerable:!1,writable:!1,value:te(i,s)}});const c=re(i,s);try{e(ne(i,s),c)}catch(l){s.resolving?console.log("Unhandled exception in CancellablePromise executor.",l):c(l)}}cancel(e){return new a(n=>{Promise.all([this[J](new W("Promise cancelled.",{cause:e})),Ie(this)]).then(()=>n(),()=>n())})}cancelOn(e){return e.aborted?this.cancel(e.reason):e.addEventListener("abort",()=>void this.cancel(e.reason),{capture:!0}),this}then(e,n,r){if(!(this instanceof a))throw new TypeError("CancellablePromise.prototype.then called on an invalid object.");if(m(e)||(e=q),m(n)||(n=K),e===q&&n==K)return new a(i=>i(this));const o={};return this[f]=o,new a((i,s)=>{super.then(c=>{var l;this[f]===o&&(this[f]=null),(l=o.resolve)===null||l===void 0||l.call(o);try{i(e(c))}catch(d){s(d)}},c=>{var l;this[f]===o&&(this[f]=null),(l=o.resolve)===null||l===void 0||l.call(o);try{i(n(c))}catch(d){s(d)}})},async i=>{try{return r==null?void 0:r(i)}finally{await this.cancel(i)}})}catch(e,n){return this.then(void 0,e,n)}finally(e,n){if(!(this instanceof a))throw new TypeError("CancellablePromise.prototype.finally called on an invalid object.");return m(e)?this.then(r=>a.resolve(e()).then(()=>r),r=>a.resolve(e()).then(()=>{throw r}),n):this.then(e,e,n)}static get[V](){return Promise}static all(e){let n=Array.from(e);const r=n.length===0?a.resolve(n):new a((o,i)=>{Promise.all(n).then(o,i)},o=>M(r,n,o));return r}static allSettled(e){let n=Array.from(e);const r=n.length===0?a.resolve(n):new a((o,i)=>{Promise.allSettled(n).then(o,i)},o=>M(r,n,o));return r}static any(e){let n=Array.from(e);const r=n.length===0?a.resolve(n):new a((o,i)=>{Promise.any(n).then(o,i)},o=>M(r,n,o));return r}static race(e){let n=Array.from(e);const r=new a((o,i)=>{Promise.race(n).then(o,i)},o=>M(r,n,o));return r}static cancel(e){const n=new a(()=>{});return n.cancel(e),n}static timeout(e,n){const r=new a(()=>{});return AbortSignal&&typeof AbortSignal=="function"&&AbortSignal.timeout&&typeof AbortSignal.timeout=="function"?AbortSignal.timeout(e).addEventListener("abort",()=>void r.cancel(n)):setTimeout(()=>void r.cancel(n),e),r}static sleep(e,n){return new a(r=>{setTimeout(()=>r(n),e)})}static reject(e){return new a((n,r)=>r(e))}static resolve(e){return e instanceof a?e:new a(n=>n(e))}static withResolvers(){let e={oncancelled:null};return e.promise=new a((n,r)=>{e.resolve=n,e.reject=r},n=>{var r;(r=e.oncancelled)===null||r===void 0||r.call(e,n)}),e}}function te(t,e){let n;return r=>{if(e.settled||(e.settled=!0,e.reason=r,t.reject(r),Promise.prototype.then.call(t.promise,void 0,o=>{if(o!==r)throw o})),!(!e.reason||!t.oncancelled))return n=new Promise(o=>{try{o(t.oncancelled(e.reason.cause))}catch(i){Promise.reject(new x(t.promise,i,"Unhandled exception in oncancelled callback."))}}).catch(o=>{Promise.reject(new x(t.promise,o,"Unhandled rejection in oncancelled callback."))}),t.oncancelled=null,n}}function ne(t,e){return n=>{if(!e.resolving){if(e.resolving=!0,n===t.promise){if(e.settled)return;e.settled=!0,t.reject(new TypeError("A promise cannot be resolved with itself."));return}if(n!=null&&(typeof n=="object"||typeof n=="function")){let r;try{r=n.then}catch(o){e.settled=!0,t.reject(o);return}if(m(r)){try{let s=n.cancel;if(m(s)){const c=l=>{Reflect.apply(s,n,[l])};e.reason?te(Object.assign(Object.assign({},t),{oncancelled:c}),e)(e.reason):t.oncancelled=c}}catch{}const o={root:e.root,resolving:!1,get settled(){return this.root.settled},set settled(s){this.root.settled=s},get reason(){return this.root.reason}},i=re(t,o);try{Reflect.apply(r,n,[ne(t,o),i])}catch(s){i(s)}return}}e.settled||(e.settled=!0,t.resolve(n))}}}function re(t,e){return n=>{if(!e.resolving)if(e.resolving=!0,e.settled){try{if(n instanceof W&&e.reason instanceof W&&Object.is(n.cause,e.reason.cause))return}catch{}Promise.reject(new x(t.promise,n))}else e.settled=!0,t.reject(n)}}function M(t,e,n){const r=[];for(const o of e){let i;try{if(!m(o.then)||(i=o.cancel,!m(i)))continue}catch{continue}let s;try{s=Reflect.apply(i,o,[n])}catch(c){Promise.reject(new x(t,c,"Unhandled exception in cancel method."));continue}s&&r.push((s instanceof Promise?s:Promise.resolve(s)).catch(c=>{Promise.reject(new x(t,c,"Unhandled rejection in cancel method."))}))}return Promise.all(r)}function q(t){return t}function K(t){throw t}function De(t){try{if(t instanceof Error||typeof t!="object"||t.toString!==Object.prototype.toString)return""+t}catch{}try{return JSON.stringify(t)}catch{}try{return Object.prototype.toString.call(t)}catch{}return""}function Ie(t){var e;let n=(e=t[f])!==null&&e!==void 0?e:{};return"promise"in n||Object.assign(n,g()),t[f]==null&&(n.resolve(),t[f]=n),n.promise}let g=Promise.withResolvers;g&&typeof g=="function"?g=g.bind(Promise):g=function(){let t,e;return{promise:new Promise((r,o)=>{t=r,e=o}),resolve:t,reject:e}};window._wails=window._wails||{};window._wails.callResultHandler=Xe;window._wails.callErrorHandler=Ye;const Fe=z(C.Call),_e=z(C.CancelCall),b=new Map,Ue=0,We=0;class Ne extends Error{constructor(e,n){super(e,n),this.name="RuntimeError"}}function Xe(t,e,n){const r=oe(t);if(r)if(!e)r.resolve(void 0);else if(!n)r.resolve(e);else try{r.resolve(JSON.parse(e))}catch(o){r.reject(new TypeError("could not parse result: "+o.message,{cause:o}))}}function Ye(t,e,n){const r=oe(t);if(r)if(!n)r.reject(new Error(e));else{let o;try{o=JSON.parse(e)}catch(c){r.reject(new TypeError("could not parse error: "+c.message,{cause:c}));return}let i={};o.cause&&(i.cause=o.cause);let s;switch(o.kind){case"ReferenceError":s=new ReferenceError(o.message,i);break;case"TypeError":s=new TypeError(o.message,i);break;case"RuntimeError":s=new Ne(o.message,i);break;default:s=new Error(o.message,i);break}r.reject(s)}}function oe(t){const e=b.get(t);return b.delete(t),e}function Ge(){let t;do t=$();while(b.has(t));return t}function Je(t){const e=Ge(),n=a.withResolvers();b.set(e,{resolve:n.resolve,reject:n.reject});const r=Fe(Ue,Object.assign({"call-id":e},t));let o=!1;r.then(()=>{o=!0},s=>{b.delete(e),n.reject(s)});const i=()=>(b.delete(e),_e(We,{"call-id":e}).catch(s=>{console.error("Error while requesting binding call cancellation:",s)}));return n.oncancelled=()=>o?i():r.then(i),n.promise}function ie(t,...e){return Je({methodID:t,args:e})}const w=new Map;class Ve{constructor(e,n,r){this.eventName=e,this.callback=n,this.maxCallbacks=r||-1}dispatch(e){try{this.callback(e)}catch(n){console.error(n)}return this.maxCallbacks===-1?!1:(this.maxCallbacks-=1,this.maxCallbacks===0)}}function qe(t){let e=w.get(t.eventName);e&&(e=e.filter(n=>n!==t),e.length===0?w.delete(t.eventName):w.set(t.eventName,e))}window._wails=window._wails||{};window._wails.dispatchWailsEvent=Ze;const Ke=z(C.Events),$e=0;class Qe{constructor(e,n=null){this.name=e,this.data=n}}function Ze(t){let e=w.get(t.name);if(!e)return;let n=new Qe(t.name,t.data);"sender"in t&&(n.sender=t.sender),e=e.filter(r=>!r.dispatch(n)),e.length===0?w.delete(t.name):w.set(t.name,e)}function et(t,e,n){let r=w.get(t)||[];const o=new Ve(t,e,n);return r.push(o),w.set(t,r),()=>qe(o)}function tt(t,e){return et(t,e,-1)}function se(t){return Ke($e,t)}window._wails=window._wails||{};window._wails.invoke=R;R("wails:runtime:ready");function nt(){return ie(2374916939)}function rt(t){return ie(784276339,t)}const ot=document.getElementById("set"),it=document.getElementById("remove"),st=document.getElementById("set-go"),lt=document.getElementById("remove-go"),le=document.getElementById("label"),ct=document.getElementById("time");ot.addEventListener("click",()=>{let t=le.value;rt(t)});it.addEventListener("click",()=>{nt()});st.addEventListener("click",()=>{let t=le.value;se({name:"set:badge",data:t})});lt.addEventListener("click",()=>{se({name:"remove:badge",data:null})});tt("time",t=>{ct.innerText=t.data}); diff --git a/v3/examples/badge/frontend/dist/index.html b/v3/examples/badge/frontend/dist/index.html new file mode 100644 index 000000000..d5b44d109 --- /dev/null +++ b/v3/examples/badge/frontend/dist/index.html @@ -0,0 +1,38 @@ + + + + + + + + Wails App + + + +
+ +

Wails + Typescript

+
Set a badge label below πŸ‘‡
+
+
+ + + + + +
+
+ +
+ + diff --git a/v3/examples/badge/frontend/dist/style.css b/v3/examples/badge/frontend/dist/style.css new file mode 100644 index 000000000..6ce81cad2 --- /dev/null +++ b/v3/examples/badge/frontend/dist/style.css @@ -0,0 +1,155 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +h3 { + font-size: 3em; + line-height: 1.1; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +button { + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.result { + height: 20px; + line-height: 20px; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + text-align: center; +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + color: black; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v3/examples/badge/frontend/dist/typescript.svg b/v3/examples/badge/frontend/dist/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/v3/examples/badge/frontend/dist/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/badge/frontend/dist/wails.png b/v3/examples/badge/frontend/dist/wails.png new file mode 100644 index 000000000..8bdf42483 Binary files /dev/null and b/v3/examples/badge/frontend/dist/wails.png differ diff --git a/v3/examples/badge/frontend/index.html b/v3/examples/badge/frontend/index.html new file mode 100644 index 000000000..616cb4c0f --- /dev/null +++ b/v3/examples/badge/frontend/index.html @@ -0,0 +1,38 @@ + + + + + + + + Wails App + + +
+ +

Wails + Typescript

+
Set a badge label below πŸ‘‡
+
+
+ + + + + +
+
+ +
+ + + diff --git a/v3/examples/badge/frontend/package.json b/v3/examples/badge/frontend/package.json new file mode 100644 index 000000000..b39da7ece --- /dev/null +++ b/v3/examples/badge/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build:dev": "tsc && vite build --minify false --mode development", + "build": "tsc && vite build --mode production", + "preview": "vite preview" + }, + "dependencies": { + "@wailsio/runtime": "latest" + }, + "devDependencies": { + "typescript": "^4.9.3", + "vite": "^5.0.0" + } +} diff --git a/v3/examples/badge/frontend/public/Inter-Medium.ttf b/v3/examples/badge/frontend/public/Inter-Medium.ttf new file mode 100644 index 000000000..a01f3777a Binary files /dev/null and b/v3/examples/badge/frontend/public/Inter-Medium.ttf differ diff --git a/v3/examples/badge/frontend/public/style.css b/v3/examples/badge/frontend/public/style.css new file mode 100644 index 000000000..6ce81cad2 --- /dev/null +++ b/v3/examples/badge/frontend/public/style.css @@ -0,0 +1,155 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +h3 { + font-size: 3em; + line-height: 1.1; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +button { + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.result { + height: 20px; + line-height: 20px; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + text-align: center; +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + color: black; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v3/examples/badge/frontend/public/typescript.svg b/v3/examples/badge/frontend/public/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/v3/examples/badge/frontend/public/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/badge/frontend/public/wails.png b/v3/examples/badge/frontend/public/wails.png new file mode 100644 index 000000000..8bdf42483 Binary files /dev/null and b/v3/examples/badge/frontend/public/wails.png differ diff --git a/v3/examples/badge/frontend/src/main.ts b/v3/examples/badge/frontend/src/main.ts new file mode 100644 index 000000000..934741aba --- /dev/null +++ b/v3/examples/badge/frontend/src/main.ts @@ -0,0 +1,35 @@ +import {Events} from "@wailsio/runtime"; +import {SetBadge, RemoveBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/badgeservice"; + +const setButton = document.getElementById('set')! as HTMLButtonElement; +const removeButton = document.getElementById('remove')! as HTMLButtonElement; +const setButtonUsingGo = document.getElementById('set-go')! as HTMLButtonElement; +const removeButtonUsingGo = document.getElementById('remove-go')! as HTMLButtonElement; +const labelElement : HTMLInputElement = document.getElementById('label')! as HTMLInputElement; +const timeElement = document.getElementById('time')! as HTMLDivElement; + +setButton.addEventListener('click', () => { + let label = (labelElement as HTMLInputElement).value + SetBadge(label); +}); + +removeButton.addEventListener('click', () => { + RemoveBadge(); +}); + +setButtonUsingGo.addEventListener('click', () => { + let label = (labelElement as HTMLInputElement).value + void Events.Emit({ + name: "set:badge", + data: label, + }) +}) + +removeButtonUsingGo.addEventListener('click', () => { + void Events.Emit({name:"remove:badge", data: null}) +}) + +Events.On('time', (time: {data: any}) => { + timeElement.innerText = time.data; +}); + diff --git a/v3/examples/badge/frontend/src/vite-env.d.ts b/v3/examples/badge/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/v3/examples/badge/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/v3/examples/badge/frontend/tsconfig.json b/v3/examples/badge/frontend/tsconfig.json new file mode 100644 index 000000000..c267ecf24 --- /dev/null +++ b/v3/examples/badge/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/v3/examples/badge/main.go b/v3/examples/badge/main.go new file mode 100644 index 000000000..9de2944c0 --- /dev/null +++ b/v3/examples/badge/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "embed" + _ "embed" + "log" + "time" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/services/badge" +) + +// Wails uses Go's `embed` package to embed the frontend files into the binary. +// Any files in the frontend/dist folder will be embedded into the binary and +// made available to the frontend. +// See https://pkg.go.dev/embed for more information. + +//go:embed all:frontend/dist +var assets embed.FS + +// main function serves as the application's entry point. It initializes the application, creates a window, +// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and +// logs any error that might occur. +func main() { + // Create a new Wails application by providing the necessary options. + // Variables 'Name' and 'Description' are for application metadata. + // 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files. + // 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances. + // 'Mac' options tailor the application when running an macOS. + + badgeService := badge.New() + + app := application.New(application.Options{ + Name: "badge", + Description: "A demo of using raw HTML & CSS", + Services: []application.Service{ + application.NewService(badgeService), + }, + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + // Create a new window with the necessary options. + // 'Title' is the title of the window. + // 'Mac' options tailor the window when running on macOS. + // 'BackgroundColour' is the background colour of the window. + // 'URL' is the URL that will be loaded into the webview. + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Window 1", + Mac: application.MacWindow{ + InvisibleTitleBarHeight: 50, + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInset, + }, + BackgroundColour: application.NewRGB(27, 38, 54), + URL: "/", + }) + + // Store cleanup functions for proper resource management + removeBadgeHandler := app.Event.On("remove:badge", func(event *application.CustomEvent) { + err := badgeService.RemoveBadge() + if err != nil { + log.Fatal(err) + } + }) + + setBadgeHandler := app.Event.On("set:badge", func(event *application.CustomEvent) { + text := event.Data.(string) + err := badgeService.SetBadge(text) + if err != nil { + log.Fatal(err) + } + }) + + // Note: In a production application, you would call these cleanup functions + // when the handlers are no longer needed, e.g., during shutdown: + // defer removeBadgeHandler() + // defer setBadgeHandler() + _ = removeBadgeHandler // Acknowledge we're storing the cleanup functions + _ = setBadgeHandler + + // Create a goroutine that emits an event containing the current time every second. + // The frontend can listen to this event and update the UI accordingly. + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + now := time.Now().Format(time.RFC1123) + app.Event.Emit("time", now) + case <-app.Context().Done(): + return + } + } + }() + + // Run the application. This blocks until the application has been exited. + err := app.Run() + + // If an error occurred while running the application, log it and exit. + if err != nil { + log.Fatal(err) + } +} diff --git a/v3/examples/binding/README.md b/v3/examples/binding/README.md index 9b5d68a7a..37d0f2cc8 100644 --- a/v3/examples/binding/README.md +++ b/v3/examples/binding/README.md @@ -2,10 +2,10 @@ This example demonstrates how to generate bindings for your application. -To generate bindings, run `wails3 generate bindings -b -d assets/bindings` in this directory. +To generate bindings, run `wails3 generate bindings -clean -b -d assets/bindings` in this directory. See more options by running `wails3 generate bindings --help`. ## Notes - The bindings generator is still a work in progress and is subject to change. - - The generated code uses `wails.CallByID` by default. This is the most secure and quickest way to call a function. In a future release, we will look at generating `wails.CallByName` too. + - The generated code uses `wails.CallByID` by default. This is the most robust way to call a function. diff --git a/v3/examples/binding/assets/bindings/github.com/wailsapp/wails/v3/examples/binding/greetservice.js b/v3/examples/binding/assets/bindings/github.com/wailsapp/wails/v3/examples/binding/greetservice.js index 0521b91e8..0e5e40d84 100644 --- a/v3/examples/binding/assets/bindings/github.com/wailsapp/wails/v3/examples/binding/greetservice.js +++ b/v3/examples/binding/assets/bindings/github.com/wailsapp/wails/v3/examples/binding/greetservice.js @@ -9,7 +9,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "/wails/runtime.js"; +import {Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create} from "/wails/runtime.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports @@ -18,36 +18,31 @@ import * as data$0 from "./data/models.js"; /** * GetPerson returns a person with the given name. * @param {string} name - * @returns {Promise & { cancel(): void }} + * @returns {$CancellablePromise} */ export function GetPerson(name) { - let $resultPromise = /** @type {any} */($Call.ByID(2952413357, name)); - let $typingPromise = /** @type {any} */($resultPromise.then(($result) => { + return $Call.ByID(2952413357, name).then(/** @type {($result: any) => any} */(($result) => { return $$createType0($result); })); - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; } /** * Greet greets a person * @param {string} name * @param {number[]} counts - * @returns {Promise & { cancel(): void }} + * @returns {$CancellablePromise} */ export function Greet(name, ...counts) { - let $resultPromise = /** @type {any} */($Call.ByID(1411160069, name, counts)); - return $resultPromise; + return $Call.ByID(1411160069, name, counts); } /** * GreetPerson greets a person * @param {data$0.Person} person - * @returns {Promise & { cancel(): void }} + * @returns {$CancellablePromise} */ export function GreetPerson(person) { - let $resultPromise = /** @type {any} */($Call.ByID(4021313248, person)); - return $resultPromise; + return $Call.ByID(4021313248, person); } // Private type creation functions diff --git a/v3/examples/binding/main.go b/v3/examples/binding/main.go index 29666a703..6e956bac1 100644 --- a/v3/examples/binding/main.go +++ b/v3/examples/binding/main.go @@ -23,7 +23,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ URL: "/", DevToolsEnabled: true, }) diff --git a/v3/examples/build/main.go b/v3/examples/build/main.go index b8a0db307..d610b69a7 100755 --- a/v3/examples/build/main.go +++ b/v3/examples/build/main.go @@ -25,13 +25,13 @@ func main() { }, }) - app.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(*application.ApplicationEvent) { + app.Event.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(*application.ApplicationEvent) { log.Println("ApplicationDidFinishLaunching") }) currentWindow := func(fn func(window *application.WebviewWindow)) { - if app.CurrentWindow() != nil { - fn(app.CurrentWindow()) + if app.Window.Current() != nil { + fn(app.Window.Current()) } else { println("Current WebviewWindow is nil") } @@ -51,7 +51,7 @@ func main() { myMenu.Add("New WebviewWindow"). SetAccelerator("CmdOrCtrl+N"). OnClick(func(ctx *application.Context) { - app.NewWebviewWindow(). + app.Window.New(). SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)). SetRelativePosition(rand.Intn(1000), rand.Intn(800)). SetURL("https://wails.io"). @@ -61,7 +61,7 @@ func main() { myMenu.Add("New Frameless WebviewWindow"). SetAccelerator("CmdOrCtrl+F"). OnClick(func(ctx *application.Context) { - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ X: rand.Intn(1000), Y: rand.Intn(800), Frameless: true, @@ -74,7 +74,7 @@ func main() { if runtime.GOOS == "darwin" { myMenu.Add("New WebviewWindow (MacTitleBarHiddenInset)"). OnClick(func(ctx *application.Context) { - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Mac: application.MacWindow{ TitleBar: application.MacTitleBarHiddenInset, InvisibleTitleBarHeight: 25, @@ -88,7 +88,7 @@ func main() { }) myMenu.Add("New WebviewWindow (MacTitleBarHiddenInsetUnified)"). OnClick(func(ctx *application.Context) { - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Mac: application.MacWindow{ TitleBar: application.MacTitleBarHiddenInsetUnified, InvisibleTitleBarHeight: 50, @@ -102,7 +102,7 @@ func main() { }) myMenu.Add("New WebviewWindow (MacTitleBarHidden)"). OnClick(func(ctx *application.Context) { - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Mac: application.MacWindow{ TitleBar: application.MacTitleBarHidden, InvisibleTitleBarHeight: 25, @@ -238,20 +238,12 @@ func main() { }) }) stateMenu.Add("Get Primary Screen").OnClick(func(ctx *application.Context) { - screen, err := app.GetPrimaryScreen() - if err != nil { - application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show() - return - } + screen := app.Screen.GetPrimary() msg := fmt.Sprintf("Screen: %+v", screen) application.InfoDialog().SetTitle("Primary Screen").SetMessage(msg).Show() }) stateMenu.Add("Get Screens").OnClick(func(ctx *application.Context) { - screens, err := app.GetScreens() - if err != nil { - application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show() - return - } + screens := app.Screen.GetAll() for _, screen := range screens { msg := fmt.Sprintf("Screen: %+v", screen) application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show() @@ -268,9 +260,9 @@ func main() { application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show() }) }) - app.NewWebviewWindow() + app.Window.New() - app.SetMenu(menu) + app.Menu.Set(menu) err := app.Run() if err != nil { diff --git a/v3/examples/cancel-async/README.md b/v3/examples/cancel-async/README.md new file mode 100644 index 000000000..feec0e5d6 --- /dev/null +++ b/v3/examples/cancel-async/README.md @@ -0,0 +1,5 @@ +# Binding Call Cancelling Example + +This example demonstrates how to cancel binding calls in async/await style. + +To regenerate bindings, run `wails3 generate bindings -clean -b -d assets/bindings` in this directory. diff --git a/v3/examples/cancel-async/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-async/index.js b/v3/examples/cancel-async/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-async/index.js new file mode 100644 index 000000000..d2bf43c94 --- /dev/null +++ b/v3/examples/cancel-async/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-async/index.js @@ -0,0 +1,8 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as Service from "./service.js"; +export { + Service +}; diff --git a/v3/examples/cancel-async/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-async/service.js b/v3/examples/cancel-async/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-async/service.js new file mode 100644 index 000000000..6aae91e1d --- /dev/null +++ b/v3/examples/cancel-async/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-async/service.js @@ -0,0 +1,16 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create} from "/wails/runtime.js"; + +/** + * A long running operation of specified duration. + * @param {number} milliseconds + * @returns {$CancellablePromise} + */ +export function LongRunning(milliseconds) { + return $Call.ByID(298191698, milliseconds); +} diff --git a/v3/examples/cancel-async/assets/index.html b/v3/examples/cancel-async/assets/index.html new file mode 100644 index 000000000..ff0609928 --- /dev/null +++ b/v3/examples/cancel-async/assets/index.html @@ -0,0 +1,134 @@ + + + + + Wails Alpha + + + + +
Alpha
+
+

Documentation

+

Feedback

+
+
Please enter a duration in milliseconds below πŸ‘‡
+
+ + + + +
+ + + diff --git a/v3/examples/cancel-async/main.go b/v3/examples/cancel-async/main.go new file mode 100644 index 000000000..9fb52481b --- /dev/null +++ b/v3/examples/cancel-async/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "embed" + "log" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +//go:embed assets/* +var assets embed.FS + +func main() { + app := application.New(application.Options{ + Services: []application.Service{ + application.NewService(&Service{}), + }, + Assets: application.AssetOptions{ + Handler: application.BundledAssetFileServer(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + app.Window.NewWithOptions(application.WebviewWindowOptions{ + URL: "/", + DevToolsEnabled: true, + }) + + err := app.Run() + + if err != nil { + log.Fatal(err) + } + +} diff --git a/v3/examples/cancel-async/service.go b/v3/examples/cancel-async/service.go new file mode 100644 index 000000000..b2cc72e9b --- /dev/null +++ b/v3/examples/cancel-async/service.go @@ -0,0 +1,19 @@ +package main + +import ( + "context" + "time" +) + +type Service struct { +} + +// A long running operation of specified duration. +func (*Service) LongRunning(ctx context.Context, milliseconds int) error { + select { + case <-time.After(time.Duration(milliseconds) * time.Millisecond): + return nil + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/v3/examples/cancel-chaining/README.md b/v3/examples/cancel-chaining/README.md new file mode 100644 index 000000000..10c5855ea --- /dev/null +++ b/v3/examples/cancel-chaining/README.md @@ -0,0 +1,5 @@ +# Binding Call Cancelling Example + +This example demonstrates how to cancel binding calls in promise chaining style. + +To regenerate bindings, run `wails3 generate bindings -clean -b -d assets/bindings` in this directory. diff --git a/v3/examples/cancel-chaining/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-chaining/index.js b/v3/examples/cancel-chaining/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-chaining/index.js new file mode 100644 index 000000000..d2bf43c94 --- /dev/null +++ b/v3/examples/cancel-chaining/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-chaining/index.js @@ -0,0 +1,8 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as Service from "./service.js"; +export { + Service +}; diff --git a/v3/examples/cancel-chaining/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-chaining/service.js b/v3/examples/cancel-chaining/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-chaining/service.js new file mode 100644 index 000000000..6aae91e1d --- /dev/null +++ b/v3/examples/cancel-chaining/assets/bindings/github.com/wailsapp/wails/v3/examples/cancel-chaining/service.js @@ -0,0 +1,16 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create} from "/wails/runtime.js"; + +/** + * A long running operation of specified duration. + * @param {number} milliseconds + * @returns {$CancellablePromise} + */ +export function LongRunning(milliseconds) { + return $Call.ByID(298191698, milliseconds); +} diff --git a/v3/examples/cancel-chaining/assets/index.html b/v3/examples/cancel-chaining/assets/index.html new file mode 100644 index 000000000..71221c28d --- /dev/null +++ b/v3/examples/cancel-chaining/assets/index.html @@ -0,0 +1,136 @@ + + + + + Wails Alpha + + + + +
Alpha
+
+

Documentation

+

Feedback

+
+
Please enter a duration in milliseconds below πŸ‘‡
+
+ + + + +
+ + + diff --git a/v3/examples/cancel-chaining/main.go b/v3/examples/cancel-chaining/main.go new file mode 100644 index 000000000..9fb52481b --- /dev/null +++ b/v3/examples/cancel-chaining/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "embed" + "log" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +//go:embed assets/* +var assets embed.FS + +func main() { + app := application.New(application.Options{ + Services: []application.Service{ + application.NewService(&Service{}), + }, + Assets: application.AssetOptions{ + Handler: application.BundledAssetFileServer(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + app.Window.NewWithOptions(application.WebviewWindowOptions{ + URL: "/", + DevToolsEnabled: true, + }) + + err := app.Run() + + if err != nil { + log.Fatal(err) + } + +} diff --git a/v3/examples/cancel-chaining/service.go b/v3/examples/cancel-chaining/service.go new file mode 100644 index 000000000..b2cc72e9b --- /dev/null +++ b/v3/examples/cancel-chaining/service.go @@ -0,0 +1,19 @@ +package main + +import ( + "context" + "time" +) + +type Service struct { +} + +// A long running operation of specified duration. +func (*Service) LongRunning(ctx context.Context, milliseconds int) error { + select { + case <-time.After(time.Duration(milliseconds) * time.Millisecond): + return nil + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/v3/examples/clipboard/main.go b/v3/examples/clipboard/main.go index 746f19f75..efeed80de 100644 --- a/v3/examples/clipboard/main.go +++ b/v3/examples/clipboard/main.go @@ -28,26 +28,26 @@ func main() { setClipboardMenu := menu.AddSubmenu("Set Clipboard") setClipboardMenu.Add("Set Text 'Hello'").OnClick(func(ctx *application.Context) { - success := app.Clipboard().SetText("Hello") + success := app.Clipboard.SetText("Hello") if !success { application.InfoDialog().SetMessage("Failed to set clipboard text").Show() } }) setClipboardMenu.Add("Set Text 'World'").OnClick(func(ctx *application.Context) { - success := app.Clipboard().SetText("World") + success := app.Clipboard.SetText("World") if !success { application.InfoDialog().SetMessage("Failed to set clipboard text").Show() } }) setClipboardMenu.Add("Set Text (current time)").OnClick(func(ctx *application.Context) { - success := app.Clipboard().SetText(time.Now().String()) + success := app.Clipboard.SetText(time.Now().String()) if !success { application.InfoDialog().SetMessage("Failed to set clipboard text").Show() } }) getClipboardMenu := menu.AddSubmenu("Get Clipboard") getClipboardMenu.Add("Get Text").OnClick(func(ctx *application.Context) { - result, ok := app.Clipboard().Text() + result, ok := app.Clipboard.Text() if !ok { application.InfoDialog().SetMessage("Failed to get clipboard text").Show() } else { @@ -57,7 +57,7 @@ func main() { clearClipboardMenu := menu.AddSubmenu("Clear Clipboard") clearClipboardMenu.Add("Clear Text").OnClick(func(ctx *application.Context) { - success := app.Clipboard().SetText("") + success := app.Clipboard.SetText("") if success { application.InfoDialog().SetMessage("Clipboard text cleared").Show() } else { @@ -65,9 +65,9 @@ func main() { } }) - app.SetMenu(menu) + app.Menu.Set(menu) - app.NewWebviewWindow() + app.Window.New() err := app.Run() diff --git a/v3/examples/contextmenus/main.go b/v3/examples/contextmenus/main.go index 4d40db5be..70cdf5c7e 100644 --- a/v3/examples/contextmenus/main.go +++ b/v3/examples/contextmenus/main.go @@ -24,7 +24,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Context Menu Demo", Width: 1024, Height: 1024, @@ -35,7 +35,7 @@ func main() { }, }) - contextMenu := application.NewContextMenu("test") + contextMenu := app.ContextMenu.New() clickMe := contextMenu.Add("Click to set Menuitem label to Context Data") contextDataMenuItem := contextMenu.Add("Current context data: No Context Data") clickMe.OnClick(func(data *application.Context) { @@ -44,6 +44,9 @@ func main() { contextMenu.Update() }) + // Register the context menu + app.ContextMenu.Add("test", contextMenu) + err := app.Run() if err != nil { diff --git a/v3/examples/custom-protocol-example/.gitignore b/v3/examples/custom-protocol-example/.gitignore new file mode 100644 index 000000000..ba8194ab6 --- /dev/null +++ b/v3/examples/custom-protocol-example/.gitignore @@ -0,0 +1,6 @@ +.task +bin +frontend/dist +frontend/node_modules +build/linux/appimage/build +build/windows/nsis/MicrosoftEdgeWebview2Setup.exe \ No newline at end of file diff --git a/v3/examples/custom-protocol-example/README.md b/v3/examples/custom-protocol-example/README.md new file mode 100644 index 000000000..ad12c3f40 --- /dev/null +++ b/v3/examples/custom-protocol-example/README.md @@ -0,0 +1,59 @@ +# Welcome to Your New Wails3 Project! + +Congratulations on generating your Wails3 application! This README will guide you through the next steps to get your project up and running. + +## Getting Started + +1. Navigate to your project directory in the terminal. + +2. To run your application in development mode, use the following command: + + ``` + wails3 dev + ``` + + This will start your application and enable hot-reloading for both frontend and backend changes. + +3. To build your application for production, use: + + ``` + wails3 build + ``` + + This will create a production-ready executable in the `build` directory. + +## Exploring Wails3 Features + +Now that you have your project set up, it's time to explore the features that Wails3 offers: + +1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications. + +2. **Run an example**: To run any of the examples, navigate to the example's directory and use: + + ``` + go run . + ``` + + Note: Some examples may be under development during the alpha phase. + +3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3.wails.io/) for in-depth guides and API references. + +4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions). + +## Project Structure + +Take a moment to familiarize yourself with your project structure: + +- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript) +- `main.go`: The entry point of your Go backend +- `app.go`: Define your application structure and methods here +- `wails.json`: Configuration file for your Wails project + +## Next Steps + +1. Modify the frontend in the `frontend/` directory to create your desired UI. +2. Add backend functionality in `main.go`. +3. Use `wails3 dev` to see your changes in real-time. +4. When ready, build your application with `wails3 build`. + +Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community. diff --git a/v3/examples/custom-protocol-example/Taskfile.yml b/v3/examples/custom-protocol-example/Taskfile.yml new file mode 100644 index 000000000..572db59ac --- /dev/null +++ b/v3/examples/custom-protocol-example/Taskfile.yml @@ -0,0 +1,33 @@ +version: '3' + +includes: + common: ./build/Taskfile.yml + windows: ./build/windows/Taskfile.yml + darwin: ./build/darwin/Taskfile.yml + linux: ./build/linux/Taskfile.yml + +vars: + APP_NAME: "custom-protocol-example" + BIN_DIR: "bin" + +tasks: + build: + summary: Builds the application + cmds: + - task: "{{OS}}:build" + + package: + summary: Packages a production build of the application + cmds: + - task: "{{OS}}:package" + + run: + summary: Runs the application + cmds: + - task: "{{OS}}:run" + + dev: + summary: Runs the application in development mode + cmds: + - wails3 dev -config ./build/config.yml + diff --git a/v3/examples/custom-protocol-example/build/Taskfile.yml b/v3/examples/custom-protocol-example/build/Taskfile.yml new file mode 100644 index 000000000..ba497b5b6 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/Taskfile.yml @@ -0,0 +1,85 @@ +version: '3' + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + dir: frontend + sources: + - package.json + - package-lock.json + generates: + - node_modules/* + preconditions: + - sh: npm version + cmds: + - echo "Skipping frontend dependencies installation" + + build:frontend: + label: build:frontend (PRODUCTION={{.PRODUCTION}}) + summary: Build the frontend project + dir: frontend + sources: + - "**/*" + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - npm run {{.BUILD_COMMAND}} -q + env: + PRODUCTION: '{{.PRODUCTION | default "false"}}' + vars: + BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build{{end}}' + + + generate:bindings: + label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) + summary: Generates bindings for the frontend + deps: + - task: go:mod:tidy + sources: + - "**/*.[jt]s" + - exclude: frontend/**/* + - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` files from an image + dir: build + sources: + - "appicon.png" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico + + dev:frontend: + summary: Runs the frontend in development mode + dir: frontend + deps: + - task: install:frontend:deps + cmds: + - echo "Skipping frontend development mode" + + update:build-assets: + summary: Updates the build assets + dir: build + cmds: + - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . diff --git a/v3/examples/custom-protocol-example/build/appicon.png b/v3/examples/custom-protocol-example/build/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/v3/examples/custom-protocol-example/build/appicon.png differ diff --git a/v3/examples/custom-protocol-example/build/config.yml b/v3/examples/custom-protocol-example/build/config.yml new file mode 100644 index 000000000..8f2cb0348 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/config.yml @@ -0,0 +1,67 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: '3' + +# This information is used to generate the build assets. +info: + companyName: "My Company" # The name of the company + productName: "My Product" # The name of the application + productIdentifier: "com.mycompany.myproduct" # The unique product identifier + description: "A program that does X" # The application description + copyright: "(c) 2025, My Company" # Copyright text + comments: "Some Product Comments" # Comments + version: "0.0.1" # The application version + +# Dev mode configuration +dev_mode: + root_path: . + log_level: warn + debounce: 1000 + ignore: + dir: + - .git + - node_modules + - frontend + - bin + file: + - .DS_Store + - .gitignore + - .gitkeep + watched_extension: + - "*.go" + git_ignore: true + executes: + - cmd: wails3 task common:install:frontend:deps + type: once + - cmd: wails3 task common:dev:frontend + type: background + - cmd: go mod tidy + type: blocking + - cmd: wails3 task build + type: blocking + - cmd: wails3 task run + type: primary + +# File Associations +# More information at: https://v3.wails.io/noit/done/yet +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor +# mimeType: image/jpeg # (optional) + +protocols: + - scheme: wailsexample + description: Wails Example Application Custom Protocol + +# Other data +other: + - name: My Other Data \ No newline at end of file diff --git a/v3/examples/custom-protocol-example/build/darwin/Info.dev.plist b/v3/examples/custom-protocol-example/build/darwin/Info.dev.plist new file mode 100644 index 000000000..a46aa82d9 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/darwin/Info.dev.plist @@ -0,0 +1,43 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + custom-protocol-example + CFBundleIdentifier + com.mycompany.myproduct + CFBundleVersion + 0.0.1 + CFBundleGetInfoString + Some Product Comments + CFBundleShortVersionString + 0.0.1 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + (c) 2025, My Company + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + CFBundleURLTypes + + + CFBundleURLName + wails.com.wailsexample + CFBundleURLSchemes + + wailsexample + + + + + \ No newline at end of file diff --git a/v3/examples/custom-protocol-example/build/darwin/Info.plist b/v3/examples/custom-protocol-example/build/darwin/Info.plist new file mode 100644 index 000000000..d88fd1030 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/darwin/Info.plist @@ -0,0 +1,38 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + custom-protocol-example + CFBundleIdentifier + com.mycompany.myproduct + CFBundleVersion + 0.0.1 + CFBundleGetInfoString + Some Product Comments + CFBundleShortVersionString + 0.0.1 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + (c) 2025, My Company + CFBundleURLTypes + + + CFBundleURLName + wails.com.wailsexample + CFBundleURLSchemes + + wailsexample + + + + + \ No newline at end of file diff --git a/v3/examples/custom-protocol-example/build/darwin/Taskfile.yml b/v3/examples/custom-protocol-example/build/darwin/Taskfile.yml new file mode 100644 index 000000000..e456dbad5 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/darwin/Taskfile.yml @@ -0,0 +1,76 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Creates a production build of the application + deps: + - task: common:go:mod:tidy + - task: common:generate:icons + - task: common:build:frontend + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: darwin + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + CGO_CFLAGS: "-mmacosx-version-min=10.15" + CGO_LDFLAGS: "-mmacosx-version-min=10.15" + MACOSX_DEPLOYMENT_TARGET: "10.15" + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + build:universal: + summary: Builds darwin universal binary (arm64 + amd64) + deps: + - task: build + vars: + ARCH: amd64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" + - task: build + vars: + ARCH: arm64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + cmds: + - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + package: + summary: Packages a production build of the application into a `.app` bundle + deps: + - task: build + vars: + PRODUCTION: "false" + cmds: + - task: create:app:bundle + + package:universal: + summary: Packages darwin universal binary (arm64 + amd64) + deps: + - task: build:universal + cmds: + - task: create:app:bundle + + + create:app:bundle: + summary: Creates an `.app` bundle + cmds: + - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} + - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources + - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS + - cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents + - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app + + run: + cmds: + - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources} + - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources + - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS + - cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist + - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app + - '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}' diff --git a/v3/examples/custom-protocol-example/build/darwin/icons.icns b/v3/examples/custom-protocol-example/build/darwin/icons.icns new file mode 100644 index 000000000..1b5bd4c86 Binary files /dev/null and b/v3/examples/custom-protocol-example/build/darwin/icons.icns differ diff --git a/v3/examples/custom-protocol-example/build/linux/Taskfile.yml b/v3/examples/custom-protocol-example/build/linux/Taskfile.yml new file mode 100644 index 000000000..a6115574a --- /dev/null +++ b/v3/examples/custom-protocol-example/build/linux/Taskfile.yml @@ -0,0 +1,113 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds the application for Linux + deps: + - task: common:go:mod:tidy + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + env: + GOOS: linux + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application for Linux + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:appimage + - task: create:deb + - task: create:rpm + - task: create:aur + + create:appimage: + summary: Creates an AppImage + dir: build/linux/appimage + deps: + - task: build + vars: + PRODUCTION: "true" + - task: generate:dotdesktop + cmds: + - cp {{.APP_BINARY}} {{.APP_NAME}} + - cp ../../appicon.png appicon.png + - wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build + vars: + APP_NAME: '{{.APP_NAME}}' + APP_BINARY: '../../../bin/{{.APP_NAME}}' + ICON: '../../appicon.png' + DESKTOP_FILE: '../{{.APP_NAME}}.desktop' + OUTPUT_DIR: '../../../bin' + + create:deb: + summary: Creates a deb package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:deb + + create:rpm: + summary: Creates a rpm package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:rpm + + create:aur: + summary: Creates a arch linux packager package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:aur + + generate:deb: + summary: Creates a deb package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:rpm: + summary: Creates a rpm package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:aur: + summary: Creates a arch linux packager package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:dotdesktop: + summary: Generates a `.desktop` file + dir: build + cmds: + - mkdir -p {{.ROOT_DIR}}/build/linux/appimage + - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}" + vars: + APP_NAME: '{{.APP_NAME}}' + EXEC: '{{.APP_NAME}}' + ICON: 'appicon' + CATEGORIES: 'Development;' + OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}' diff --git a/v3/examples/custom-protocol-example/build/linux/appimage/build.sh b/v3/examples/custom-protocol-example/build/linux/appimage/build.sh new file mode 100644 index 000000000..85901c34e --- /dev/null +++ b/v3/examples/custom-protocol-example/build/linux/appimage/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) 2018-Present Lea Anthony +# SPDX-License-Identifier: MIT + +# Fail script on any error +set -euxo pipefail + +# Define variables +APP_DIR="${APP_NAME}.AppDir" + +# Create AppDir structure +mkdir -p "${APP_DIR}/usr/bin" +cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" +cp "${ICON_PATH}" "${APP_DIR}/" +cp "${DESKTOP_FILE}" "${APP_DIR}/" + +if [[ $(uname -m) == *x86_64* ]]; then + # Download linuxdeploy and make it executable + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + + # Run linuxdeploy to bundle the application + ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage +else + # Download linuxdeploy and make it executable (arm64) + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage + chmod +x linuxdeploy-aarch64.AppImage + + # Run linuxdeploy to bundle the application (arm64) + ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage +fi + +# Rename the generated AppImage +mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage" + diff --git a/v3/examples/custom-protocol-example/build/linux/nfpm/nfpm.yaml b/v3/examples/custom-protocol-example/build/linux/nfpm/nfpm.yaml new file mode 100644 index 000000000..7356f2992 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/linux/nfpm/nfpm.yaml @@ -0,0 +1,50 @@ +# Feel free to remove those if you don't want/need to use them. +# Make sure to check the documentation at https://nfpm.goreleaser.com +# +# The lines below are called `modelines`. See `:help modeline` + +name: "custom-protocol-example" +arch: ${GOARCH} +platform: "linux" +version: "0.0.1" +section: "default" +priority: "extra" +maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> +description: "A program that does X" +vendor: "My Company" +homepage: "https://wails.io" +license: "MIT" +release: "1" + +contents: + - src: "./bin/custom-protocol-example" + dst: "/usr/local/bin/custom-protocol-example" + - src: "./build/appicon.png" + dst: "/usr/share/icons/hicolor/128x128/apps/custom-protocol-example.png" + - src: "./build/linux/custom-protocol-example.desktop" + dst: "/usr/share/applications/custom-protocol-example.desktop" + +depends: + - gtk3 + - libwebkit2gtk + +# replaces: +# - foobar +# provides: +# - bar +# depends: +# - gtk3 +# - libwebkit2gtk +# recommends: +# - whatever +# suggests: +# - something-else +# conflicts: +# - not-foo +# - not-bar +# changelog: "changelog.yaml" +# scripts: +# preinstall: ./build/linux/nfpm/scripts/preinstall.sh +# postinstall: ./build/linux/nfpm/scripts/postinstall.sh +# preremove: ./build/linux/nfpm/scripts/preremove.sh +# postremove: ./build/linux/nfpm/scripts/postremove.sh diff --git a/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/postinstall.sh b/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/postinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/postinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/postremove.sh b/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/postremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/postremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/preinstall.sh b/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/preinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/preinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/preremove.sh b/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/preremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/linux/nfpm/scripts/preremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/custom-protocol-example/build/windows/Taskfile.yml b/v3/examples/custom-protocol-example/build/windows/Taskfile.yml new file mode 100644 index 000000000..805c1aa7f --- /dev/null +++ b/v3/examples/custom-protocol-example/build/windows/Taskfile.yml @@ -0,0 +1,57 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds the application for Windows + deps: + - task: common:go:mod:tidy + - task: common:generate:icons + cmds: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + env: + GOOS: windows + CGO_ENABLED: 0 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application into a `.exe` bundle + cmds: + - task: create:nsis:installer + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: '{{.ARCH | default ARCH}}' + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + vars: + ARCH: '{{.ARCH | default ARCH}}' + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}.exe' diff --git a/v3/examples/custom-protocol-example/build/windows/icon.ico b/v3/examples/custom-protocol-example/build/windows/icon.ico new file mode 100644 index 000000000..bfa0690b7 Binary files /dev/null and b/v3/examples/custom-protocol-example/build/windows/icon.ico differ diff --git a/v3/examples/custom-protocol-example/build/windows/info.json b/v3/examples/custom-protocol-example/build/windows/info.json new file mode 100644 index 000000000..71dfd6d99 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "0.0.1" + }, + "info": { + "0000": { + "ProductVersion": "0.0.1", + "CompanyName": "My Company", + "FileDescription": "A program that does X", + "LegalCopyright": "(c) 2025, My Company", + "ProductName": "My Product", + "Comments": "Some Product Comments" + } + } +} \ No newline at end of file diff --git a/v3/examples/custom-protocol-example/build/windows/nsis/project.nsi b/v3/examples/custom-protocol-example/build/windows/nsis/project.nsi new file mode 100644 index 000000000..5ff2eb085 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/windows/nsis/project.nsi @@ -0,0 +1,112 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the wails_tools.nsh file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "my-project" # Default "custom-protocol-example" +## !define INFO_COMPANYNAME "My Company" # Default "My Company" +## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "Β© now, My Company" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/v3/examples/custom-protocol-example/build/windows/nsis/wails_tools.nsh b/v3/examples/custom-protocol-example/build/windows/nsis/wails_tools.nsh new file mode 100644 index 000000000..607fc4e85 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/windows/nsis/wails_tools.nsh @@ -0,0 +1,212 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "custom-protocol-example" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "My Company" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "My Product" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "0.0.1" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "(c) 2025, My Company" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + +!macroend \ No newline at end of file diff --git a/v3/examples/custom-protocol-example/build/windows/wails.exe.manifest b/v3/examples/custom-protocol-example/build/windows/wails.exe.manifest new file mode 100644 index 000000000..21af92674 --- /dev/null +++ b/v3/examples/custom-protocol-example/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/v3/examples/custom-protocol-example/frontend/.gitignore b/v3/examples/custom-protocol-example/frontend/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/v3/examples/custom-protocol-example/frontend/bindings/github.com/wailsapp/wails/v3/examples/custom-protocol-example/greetservice.js b/v3/examples/custom-protocol-example/frontend/bindings/github.com/wailsapp/wails/v3/examples/custom-protocol-example/greetservice.js new file mode 100644 index 000000000..0b93e6d75 --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/bindings/github.com/wailsapp/wails/v3/examples/custom-protocol-example/greetservice.js @@ -0,0 +1,15 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +/** + * @param {string} name + * @returns {$CancellablePromise} + */ +export function Greet(name) { + return $Call.ByID(1411160069, name); +} diff --git a/v3/examples/custom-protocol-example/frontend/bindings/github.com/wailsapp/wails/v3/examples/custom-protocol-example/index.js b/v3/examples/custom-protocol-example/frontend/bindings/github.com/wailsapp/wails/v3/examples/custom-protocol-example/index.js new file mode 100644 index 000000000..fdf1ff435 --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/bindings/github.com/wailsapp/wails/v3/examples/custom-protocol-example/index.js @@ -0,0 +1,8 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as GreetService from "./greetservice.js"; +export { + GreetService +}; diff --git a/v3/examples/custom-protocol-example/frontend/dist/assets/index-BS9x21Y4.js b/v3/examples/custom-protocol-example/frontend/dist/assets/index-BS9x21Y4.js new file mode 100644 index 000000000..4f1023c59 --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/dist/assets/index-BS9x21Y4.js @@ -0,0 +1,24 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))o(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const s of r.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&o(s)}).observe(document,{childList:!0,subtree:!0});function n(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function o(i){if(i.ep)return;i.ep=!0;const r=n(i);fetch(i.href,r)}})();const S="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";function R(e=21){let t="",n=e|0;for(;n--;)t+=S[Math.random()*64|0];return t}const U=window.location.origin+"/wails/runtime",_=Object.freeze({Call:0,Clipboard:1,Application:2,Events:3,ContextMenu:4,Dialog:5,Window:6,Screens:7,System:8,Browser:9,CancelCall:10});let T=R();function k(e,t=""){return function(n,o=null){return I(e,n,t,o)}}async function I(e,t,n,o){var i,r;let s=new URL(U);s.searchParams.append("object",e.toString()),s.searchParams.append("method",t.toString()),o&&s.searchParams.append("args",JSON.stringify(o));let c={"x-wails-client-id":T};n&&(c["x-wails-window-name"]=n);let a=await fetch(s,{headers:c});if(!a.ok)throw new Error(await a.text());return((r=(i=a.headers.get("Content-Type"))===null||i===void 0?void 0:i.indexOf("application/json"))!==null&&r!==void 0?r:-1)!==-1?a.json():a.text()}k(_.System);const z=function(){var e,t,n,o,i;try{if(!((t=(e=window.chrome)===null||e===void 0?void 0:e.webview)===null||t===void 0)&&t.postMessage)return window.chrome.webview.postMessage.bind(window.chrome.webview);if(!((i=(o=(n=window.webkit)===null||n===void 0?void 0:n.messageHandlers)===null||o===void 0?void 0:o.external)===null||i===void 0)&&i.postMessage)return window.webkit.messageHandlers.external.postMessage.bind(window.webkit.messageHandlers.external)}catch{}return console.warn(` +%c⚠️ Browser Environment Detected %c + +%cOnly UI previews are available in the browser. For full functionality, please run the application in desktop mode. +More information at: https://v3.wails.io/learn/build/#using-a-browser-for-development +`,"background: #ffffff; color: #000000; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 2px solid #000000;","background: transparent;","color: #ffffff; font-style: italic; font-weight: bold;"),null}();function h(e){z==null||z(e)}function H(){return window._wails.environment.OS==="windows"}function W(){return!!window._wails.environment.Debug}function B(){return new MouseEvent("mousedown").buttons===0}function P(e){var t;return e.target instanceof HTMLElement?e.target:!(e.target instanceof HTMLElement)&&e.target instanceof Node&&(t=e.target.parentElement)!==null&&t!==void 0?t:document.body}document.addEventListener("DOMContentLoaded",()=>{});window.addEventListener("contextmenu",X);const N=k(_.ContextMenu),j=0;function F(e,t,n,o){N(j,{id:e,x:t,y:n,data:o})}function X(e){const t=P(e),n=window.getComputedStyle(t).getPropertyValue("--custom-contextmenu").trim();if(n){e.preventDefault();const o=window.getComputedStyle(t).getPropertyValue("--custom-contextmenu-data");F(n,e.clientX,e.clientY,o)}else Y(e,t)}function Y(e,t){if(W())return;switch(window.getComputedStyle(t).getPropertyValue("--default-contextmenu").trim()){case"show":return;case"hide":e.preventDefault();return}if(t.isContentEditable)return;const n=window.getSelection(),o=n&&n.toString().length>0;if(o)for(let i=0;i{M=e,M||(w=p=!1,l())};window.addEventListener("mousedown",D,{capture:!0});window.addEventListener("mousemove",D,{capture:!0});window.addEventListener("mouseup",D,{capture:!0});for(const e of["click","contextmenu","dblclick"])window.addEventListener(e,A,{capture:!0});function A(e){(g||p)&&(e.stopImmediatePropagation(),e.stopPropagation(),e.preventDefault())}const C=0,V=1,L=2;function D(e){let t,n=e.buttons;switch(e.type){case"mousedown":t=C,E||(n=f|1<n!==e),t.length===0?d.delete(e.eventName):d.set(e.eventName,t))}window._wails=window._wails||{};window._wails.dispatchWailsEvent=ee;k(_.Events);class ${constructor(t,n=null){this.name=t,this.data=n}}function ee(e){let t=d.get(e.name);if(!t)return;let n=new $(e.name,e.data);"sender"in e&&(n.sender=e.sender),t=t.filter(o=>!o.dispatch(n)),t.length===0?d.delete(e.name):d.set(e.name,t)}function te(e,t,n){let o=d.get(e)||[];const i=new Q(e,t,n);return o.push(i),d.set(e,o),()=>Z(i)}function ne(e,t){return te(e,t,-1)}window._wails=window._wails||{};window._wails.invoke=h;h("wails:runtime:ready");document.addEventListener("DOMContentLoaded",()=>{const e=document.getElementById("app");e?e.innerHTML=` +
+

Custom Protocol / Deep Link Test

+

+ This page demonstrates handling custom URL schemes (deep links). +

+

+ Example Link: + Try opening this URL (e.g., by pasting it into your browser's address bar or using open your-app-scheme://... in terminal): +
+ wailsexample://test/path?value=123&message=hello +

+ +
+ Received URL: +
Waiting for application to be opened via a custom URL...
+
+
+ `:console.error('Element with ID "app" not found.')});ne("frontend:ShowURL",e=>{console.log("frontend:ShowURL event received, data:",e),displayUrl(e.data)});window.displayUrl=function(e){const t=document.getElementById("received-url");t?t.textContent=e||"No URL received or an error occurred.":console.error("Element with ID 'received-url' not found in displayUrl.")}; diff --git a/v3/examples/custom-protocol-example/frontend/dist/assets/index-uDrhEpT0.css b/v3/examples/custom-protocol-example/frontend/dist/assets/index-uDrhEpT0.css new file mode 100644 index 000000000..37a5c205c --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/dist/assets/index-uDrhEpT0.css @@ -0,0 +1 @@ +:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;margin:0;background-color:#f7f7f7;color:#333;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:20px;box-sizing:border-box}h1{color:#0056b3;font-size:1.8em;margin-bottom:.8em}#app{width:100%;max-width:600px;margin:auto}.container{background-color:#fff;padding:25px 30px;border-radius:8px;box-shadow:0 2px 10px #0000001a;text-align:left}p{line-height:1.6;font-size:1em;margin-bottom:1em}a{color:#007bff;text-decoration:none}a:hover{text-decoration:underline}.url-display{margin-top:25px;padding:15px;background-color:#e9ecef;border:1px solid #ced4da;border-radius:4px;font-family:Courier New,Courier,monospace;font-size:.95em;word-break:break-all}.label{font-weight:700;display:block;margin-bottom:8px;color:#495057}.logo{height:6em;padding:1.5em;will-change:filter;transition:filter .3s}.logo:hover{filter:drop-shadow(0 0 2em #646cffaa)}.logo.vanilla:hover{filter:drop-shadow(0 0 2em #f7df1eaa)}.card{padding:2em}.read-the-docs{color:#888}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}} diff --git a/v3/examples/custom-protocol-example/frontend/dist/index.html b/v3/examples/custom-protocol-example/frontend/dist/index.html new file mode 100644 index 000000000..126962c69 --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/dist/index.html @@ -0,0 +1,14 @@ + + + + + + + Vite App + + + + +
+ + diff --git a/v3/examples/custom-protocol-example/frontend/dist/vite.svg b/v3/examples/custom-protocol-example/frontend/dist/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/dist/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/custom-protocol-example/frontend/index.html b/v3/examples/custom-protocol-example/frontend/index.html new file mode 100644 index 000000000..72ba3a8b3 --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/v3/examples/custom-protocol-example/frontend/package-lock.json b/v3/examples/custom-protocol-example/frontend/package-lock.json new file mode 100644 index 000000000..2b2b28cc5 --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/package-lock.json @@ -0,0 +1,1017 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@wailsio/runtime": "^3.0.0-alpha.66" + }, + "devDependencies": { + "vite": "^6.3.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", + "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", + "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", + "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", + "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", + "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", + "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", + "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", + "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", + "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", + "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", + "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", + "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", + "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", + "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", + "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", + "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", + "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", + "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", + "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", + "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wailsio/runtime": { + "version": "3.0.0-alpha.66", + "resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.66.tgz", + "integrity": "sha512-ENLu8rn1griL1gFHJqkq1i+BVxrrA0JPJHYneUJYuf/s54kjuQViW0RKDEe/WTDo56ABpfykrd/T8OYpPUyXUw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", + "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.41.0", + "@rollup/rollup-android-arm64": "4.41.0", + "@rollup/rollup-darwin-arm64": "4.41.0", + "@rollup/rollup-darwin-x64": "4.41.0", + "@rollup/rollup-freebsd-arm64": "4.41.0", + "@rollup/rollup-freebsd-x64": "4.41.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", + "@rollup/rollup-linux-arm-musleabihf": "4.41.0", + "@rollup/rollup-linux-arm64-gnu": "4.41.0", + "@rollup/rollup-linux-arm64-musl": "4.41.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-musl": "4.41.0", + "@rollup/rollup-linux-s390x-gnu": "4.41.0", + "@rollup/rollup-linux-x64-gnu": "4.41.0", + "@rollup/rollup-linux-x64-musl": "4.41.0", + "@rollup/rollup-win32-arm64-msvc": "4.41.0", + "@rollup/rollup-win32-ia32-msvc": "4.41.0", + "@rollup/rollup-win32-x64-msvc": "4.41.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/v3/examples/custom-protocol-example/frontend/package.json b/v3/examples/custom-protocol-example/frontend/package.json new file mode 100644 index 000000000..64d7435a8 --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/package.json @@ -0,0 +1,17 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^6.3.5" + }, + "dependencies": { + "@wailsio/runtime": "^3.0.0-alpha.66" + } +} diff --git a/v3/examples/custom-protocol-example/frontend/public/vite.svg b/v3/examples/custom-protocol-example/frontend/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/custom-protocol-example/frontend/src/counter.js b/v3/examples/custom-protocol-example/frontend/src/counter.js new file mode 100644 index 000000000..881e2d7ad --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/src/counter.js @@ -0,0 +1,9 @@ +export function setupCounter(element) { + let counter = 0 + const setCounter = (count) => { + counter = count + element.innerHTML = `count is ${counter}` + } + element.addEventListener('click', () => setCounter(counter + 1)) + setCounter(0) +} diff --git a/v3/examples/custom-protocol-example/frontend/src/javascript.svg b/v3/examples/custom-protocol-example/frontend/src/javascript.svg new file mode 100644 index 000000000..f9abb2b72 --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/src/javascript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/custom-protocol-example/frontend/src/main.js b/v3/examples/custom-protocol-example/frontend/src/main.js new file mode 100644 index 000000000..fd11d82fa --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/src/main.js @@ -0,0 +1,45 @@ +import './style.css' +import { Events } from '@wailsio/runtime' + +document.addEventListener('DOMContentLoaded', () => { + const appDiv = document.getElementById('app'); + if (appDiv) { + appDiv.innerHTML = ` +
+

Custom Protocol / Deep Link Test

+

+ This page demonstrates handling custom URL schemes (deep links). +

+

+ Example Link: + Try opening this URL (e.g., by pasting it into your browser's address bar or using open your-app-scheme://... in terminal): +
+ wailsexample://test/path?value=123&message=hello +

+ +
+ Received URL: +
Waiting for application to be opened via a custom URL...
+
+
+ `; + } else { + console.error('Element with ID "app" not found.'); + } +}); + +// Listen for the event from Go +Events.On('frontend:ShowURL', (e) => { + console.log('frontend:ShowURL event received, data:', e); + displayUrl(e.data); +}); + +// Make displayUrl available globally just in case, though direct call from event is better +window.displayUrl = function(url) { + const urlElement = document.getElementById('received-url'); + if (urlElement) { + urlElement.textContent = url || "No URL received or an error occurred."; + } else { + console.error("Element with ID 'received-url' not found in displayUrl."); + } +} diff --git a/v3/examples/custom-protocol-example/frontend/src/style.css b/v3/examples/custom-protocol-example/frontend/src/style.css new file mode 100644 index 000000000..0fb047c33 --- /dev/null +++ b/v3/examples/custom-protocol-example/frontend/src/style.css @@ -0,0 +1,142 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + margin: 0; /* Reset default margin */ + background-color: #f7f7f7; + color: #333; + display: flex; /* Use flexbox to center content */ + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ + min-height: 100vh; /* Full viewport height */ + padding: 20px; /* Add some padding around the content */ + box-sizing: border-box; /* Ensure padding doesn't expand body beyond viewport */ +} + +h1 { + color: #0056b3; + font-size: 1.8em; + margin-bottom: 0.8em; +} + +#app { + width: 100%; + max-width: 600px; + margin: auto; /* This also helps in centering if body flex isn't enough or overridden */ +} + +.container { + background-color: #fff; + padding: 25px 30px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + text-align: left; +} + +p { + line-height: 1.6; + font-size: 1em; + margin-bottom: 1em; +} + +a { + color: #007bff; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.url-display { + margin-top: 25px; + padding: 15px; + background-color: #e9ecef; + border: 1px solid #ced4da; + border-radius: 4px; + font-family: 'Courier New', Courier, monospace; + font-size: 0.95em; + word-break: break-all; +} + +.label { + font-weight: bold; + display: block; + margin-bottom: 8px; + color: #495057; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/v3/examples/custom-protocol-example/greetservice.go b/v3/examples/custom-protocol-example/greetservice.go new file mode 100644 index 000000000..8972c39cd --- /dev/null +++ b/v3/examples/custom-protocol-example/greetservice.go @@ -0,0 +1,7 @@ +package main + +type GreetService struct{} + +func (g *GreetService) Greet(name string) string { + return "Hello " + name + "!" +} diff --git a/v3/examples/custom-protocol-example/main.go b/v3/examples/custom-protocol-example/main.go new file mode 100644 index 000000000..f5c5db38b --- /dev/null +++ b/v3/examples/custom-protocol-example/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "embed" + _ "embed" + "log" + "time" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/events" +) + +// Wails uses Go's `embed` package to embed the frontend files into the binary. +// Any files in the frontend/dist folder will be embedded into the binary and +// made available to the frontend. +// See https://pkg.go.dev/embed for more information. + +//go:embed all:frontend/dist +var assets embed.FS + +// main function serves as the application's entry point. It initializes the application, creates a window, +// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and +// logs any error that might occur. +func main() { + // Create a new Wails application by providing the necessary options. + // Variables 'Name' and 'Description' are for application metadata. + // 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files. + // 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances. + // 'Mac' options tailor the application when running an macOS. + app := application.New(application.Options{ + Name: "custom-protocol-example", + Description: "A demo of using raw HTML & CSS", + Services: []application.Service{ + application.NewService(&GreetService{}), + }, + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + // Listen for the system event indicating the app was launched with a URL + app.Event.OnApplicationEvent(events.Common.ApplicationLaunchedWithUrl, func(e *application.ApplicationEvent) { + app.Event.Emit("frontend:ShowURL", e.Context().URL()) + }) + + // Create a new window with the necessary options. + // 'Title' is the title of the window. + // 'Mac' options tailor the window when running on macOS. + // 'BackgroundColour' is the background colour of the window. + // 'URL' is the URL that will be loaded into the webview. + _ = app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Window 1", + Mac: application.MacWindow{ + InvisibleTitleBarHeight: 50, + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInset, + }, + BackgroundColour: application.NewRGB(27, 38, 54), + URL: "/", + }) + + // Create a goroutine that emits an event containing the current time every second. + // The frontend can listen to this event and update the UI accordingly. + go func() { + for { + now := time.Now().Format(time.RFC1123) + app.Event.Emit("time", now) + time.Sleep(time.Second) + } + }() + + // Run the application. This blocks until the application has been exited. + err := app.Run() + + // If an error occurred while running the application, log it and exit. + if err != nil { + log.Fatal(err) + } +} diff --git a/v3/examples/dev/go.mod b/v3/examples/dev/go.mod index 0dafe2e84..f91cd7357 100644 --- a/v3/examples/dev/go.mod +++ b/v3/examples/dev/go.mod @@ -1,47 +1,49 @@ module changeme -go 1.23.4 +go 1.24.0 require github.com/wailsapp/wails/v3 v3.0.0-alpha.0 require ( + dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/xdg v0.5.3 // indirect github.com/bep/debounce v1.2.1 // indirect - github.com/ebitengine/purego v0.4.0-alpha.4 // indirect + github.com/cloudflare/circl v1.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/ebitengine/purego v0.8.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-git/go-git/v5 v5.13.1 // indirect + github.com/go-git/go-git/v5 v5.13.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/imdario/mergo v0.3.12 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect - github.com/leaanthony/u v1.1.0 // indirect - github.com/lmittmann/tint v1.0.4 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/lmittmann/tint v1.0.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/samber/lo v1.38.1 // indirect + github.com/samber/lo v1.49.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/stretchr/testify v1.10.0 // indirect - github.com/wailsapp/go-webview2 v1.0.19 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/wailsapp/go-webview2 v1.0.21 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/v3/examples/dev/go.sum b/v3/examples/dev/go.sum index c0693ced2..cbadfe003 100644 --- a/v3/examples/dev/go.sum +++ b/v3/examples/dev/go.sum @@ -1,71 +1,58 @@ -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes= -github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= -github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= +github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= -github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.1.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.2.0 h1:GcoouCP9J+5slw2uXAocL70z8ml4A8B/H8nEPt6CLPk= -github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= -github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M= -github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= -github.com/go-git/go-git/v5 v5.3.0 h1:8WKMtJR2j8RntEXR/uvTKagfEt4GYlwQ7mntE4+0GWc= -github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw= -github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= -github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -74,131 +61,86 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= -github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI= -github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= -github.com/lmittmann/tint v1.0.0 h1:fzEj70K1L58uyoePQxKe+ezDZJ5pybiWGdA0JeFvvyw= -github.com/lmittmann/tint v1.0.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/lmittmann/tint v1.0.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= +github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= -github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= -github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/wailsapp/go-webview2 v1.0.9 h1:lrU+q0cf1wgLdR69rN+ZnRtMJNaJRrcQ4ELxoO7/xjs= -github.com/wailsapp/go-webview2 v1.0.9/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= -github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= +github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= -github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v3/examples/dev/main.go b/v3/examples/dev/main.go index f3948061f..111273721 100644 --- a/v3/examples/dev/main.go +++ b/v3/examples/dev/main.go @@ -23,7 +23,7 @@ func main() { }, }) // Create window - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Plain Bundle", CSS: `body { background-color: rgba(255, 255, 255, 0); } .main { color: white; margin: 20%; }`, Mac: application.MacWindow{ diff --git a/v3/examples/dialogs-basic/main.go b/v3/examples/dialogs-basic/main.go index 2618e5960..8df5a1c37 100644 --- a/v3/examples/dialogs-basic/main.go +++ b/v3/examples/dialogs-basic/main.go @@ -23,7 +23,7 @@ func main() { }) // Create main window - mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + mainWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Dialog Tests", Width: 800, Height: 600, @@ -34,7 +34,7 @@ func main() { // Create main menu menu := app.NewMenu() - app.SetMenu(menu) + app.Menu.Set(menu) menu.AddRole(application.AppMenu) menu.AddRole(application.EditMenu) menu.AddRole(application.WindowMenu) diff --git a/v3/examples/dialogs/main.go b/v3/examples/dialogs/main.go index 6b7b4f3b9..07521b30d 100644 --- a/v3/examples/dialogs/main.go +++ b/v3/examples/dialogs/main.go @@ -60,7 +60,7 @@ func main() { dialog.Show() }) infoMenu.Add("About").OnClick(func(ctx *application.Context) { - app.ShowAboutDialog() + app.Menu.ShowAbout() }) questionMenu := menu.AddSubmenu("Question") @@ -73,7 +73,7 @@ func main() { }) questionMenu.Add("Question (Attached to Window)").OnClick(func(ctx *application.Context) { dialog := application.QuestionDialog() - dialog.AttachToWindow(app.CurrentWindow()) + dialog.AttachToWindow(app.Window.Current()) dialog.SetMessage("No default button") dialog.AddButton("Yes") dialog.AddButton("No") @@ -196,7 +196,7 @@ func main() { CanChooseFiles(true). CanCreateDirectories(true). ShowHiddenFiles(true). - AttachToWindow(app.CurrentWindow()). + AttachToWindow(app.Window.Current()). PromptForSingleSelection() if result != "" { application.InfoDialog().SetMessage(result).Show() @@ -310,7 +310,7 @@ func main() { }) saveMenu.Add("Select File (Attach To WebviewWindow)").OnClick(func(ctx *application.Context) { result, _ := application.SaveFileDialog(). - AttachToWindow(app.CurrentWindow()). + AttachToWindow(app.Window.Current()). PromptForSingleSelection() if result != "" { application.InfoDialog().SetMessage(result).Show() @@ -350,9 +350,9 @@ func main() { } }) - app.SetMenu(menu) + app.Menu.Set(menu) - app.NewWebviewWindow() + app.Window.New() err := app.Run() diff --git a/v3/examples/drag-n-drop/main.go b/v3/examples/drag-n-drop/main.go index 6596f7514..0ab4283dd 100644 --- a/v3/examples/drag-n-drop/main.go +++ b/v3/examples/drag-n-drop/main.go @@ -25,7 +25,7 @@ func main() { }, }) - window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Drag-n-drop Demo", Mac: application.MacWindow{ Backdrop: application.MacBackdropTranslucent, @@ -37,7 +37,7 @@ func main() { window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) { files := event.Context().DroppedFiles() - app.EmitEvent("files", files) + app.Event.Emit("files", files) app.Logger.Info("Files Dropped!", "files", files) }) diff --git a/v3/examples/environment/main.go b/v3/examples/environment/main.go index 3a326f4eb..76822aaa9 100644 --- a/v3/examples/environment/main.go +++ b/v3/examples/environment/main.go @@ -24,7 +24,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Environment Demo", Width: 800, Height: 600, diff --git a/v3/examples/events-bug/main.go b/v3/examples/events-bug/main.go index 02af23856..c56c11a56 100644 --- a/v3/examples/events-bug/main.go +++ b/v3/examples/events-bug/main.go @@ -28,7 +28,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Name: "Window 1", Title: "Window 1", URL: "https://wails.io", @@ -39,7 +39,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Name: "Window 2", Title: "Window 2", URL: "https://google.com", diff --git a/v3/examples/events/assets/index.html b/v3/examples/events/assets/index.html index d379ac0c9..069baabdb 100644 --- a/v3/examples/events/assets/index.html +++ b/v3/examples/events/assets/index.html @@ -9,7 +9,7 @@

Events Demo


The main program emits an event every 10s which will be displayed in the section below. -To send an event from this window, click here: +To send an event from this window, click here:
diff --git a/v3/examples/events/main.go b/v3/examples/events/main.go index 3b04f06d9..6f3d71be5 100644 --- a/v3/examples/events/main.go +++ b/v3/examples/events/main.go @@ -27,21 +27,29 @@ func main() { }) // Custom event handling - app.OnEvent("myevent", func(e *application.CustomEvent) { + app.Event.On("myevent", func(e *application.CustomEvent) { app.Logger.Info("[Go] CustomEvent received", "name", e.Name, "data", e.Data, "sender", e.Sender, "cancelled", e.IsCancelled()) }) // OS specific application events - app.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) { - for { - // This emits a custom event every 10 seconds - // As it's sent from the application, the sender will be blank - app.EmitEvent("myevent", "hello") - time.Sleep(10 * time.Second) - } + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) { + go func() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + // This emits a custom event every 10 seconds + // As it's sent from the application, the sender will be blank + app.Event.Emit("myevent", "hello") + case <-app.Context().Done(): + return + } + } + }() }) - app.OnApplicationEvent(events.Common.ThemeChanged, func(event *application.ApplicationEvent) { + app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(event *application.ApplicationEvent) { app.Logger.Info("System theme changed!") if event.Context().IsDarkMode() { app.Logger.Info("System is now using dark mode!") @@ -51,11 +59,11 @@ func main() { }) // Platform agnostic events - app.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) { + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) { app.Logger.Info("events.Common.ApplicationStarted fired!") }) - win1 := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + win1 := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Window 1", Name: "Window 1", Mac: application.MacWindow{ @@ -77,7 +85,7 @@ func main() { e.Cancel() }) - win2 := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + win2 := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Window 2", Mac: application.MacWindow{ Backdrop: application.MacBackdropTranslucent, @@ -87,9 +95,15 @@ func main() { }) go func() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() for { - win2.EmitEvent("windowevent", "ooooh!") - time.Sleep(10 * time.Second) + select { + case <-ticker.C: + win2.EmitEvent("windowevent", "ooooh!") + case <-app.Context().Done(): + return + } } }() diff --git a/v3/examples/file-association/build/Taskfile.common.yml b/v3/examples/file-association/build/Taskfile.common.yml index 2b8a4d26d..650c8ea83 100644 --- a/v3/examples/file-association/build/Taskfile.common.yml +++ b/v3/examples/file-association/build/Taskfile.common.yml @@ -56,7 +56,7 @@ tasks: - "appicon.png" generates: - "icons.icns" - - "icons.ico" + - "icon.ico" cmds: - wails3 generate icons -input appicon.png diff --git a/v3/examples/file-association/go.mod b/v3/examples/file-association/go.mod index 24020d5a9..4d7fef32e 100644 --- a/v3/examples/file-association/go.mod +++ b/v3/examples/file-association/go.mod @@ -1,21 +1,22 @@ module changeme -go 1.23.4 +go 1.24.0 require github.com/wailsapp/wails/v3 v3.0.0-alpha.7 require ( dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.1.4 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/xdg v0.5.3 // indirect github.com/bep/debounce v1.2.1 // indirect - github.com/cloudflare/circl v1.5.0 // indirect - github.com/cyphar/filepath-securejoin v0.4.0 // indirect - github.com/ebitengine/purego v0.4.0-alpha.4 // indirect + github.com/cloudflare/circl v1.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/ebitengine/purego v0.8.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-git/go-git/v5 v5.13.1 // indirect + github.com/go-git/go-git/v5 v5.13.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect @@ -24,26 +25,25 @@ require ( github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect - github.com/leaanthony/u v1.1.0 // indirect - github.com/lmittmann/tint v1.0.4 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/lmittmann/tint v1.0.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pjbgf/sha1cd v0.3.1 // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/samber/lo v1.38.1 // indirect + github.com/samber/lo v1.49.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/skeema/knownhosts v1.3.0 // indirect - github.com/wailsapp/go-webview2 v1.0.19 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/wailsapp/go-webview2 v1.0.21 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/tools v0.29.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/v3/examples/file-association/go.sum b/v3/examples/file-association/go.sum index 17f4ab183..cbadfe003 100644 --- a/v3/examples/file-association/go.sum +++ b/v3/examples/file-association/go.sum @@ -1,60 +1,50 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/ProtonMail/go-crypto v1.1.4/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/cyphar/filepath-securejoin v0.4.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes= -github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= +github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= -github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= -github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= @@ -71,24 +61,23 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= -github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI= -github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= -github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= -github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= +github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= -github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -96,116 +85,62 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= -github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/wailsapp/go-webview2 v1.0.16 h1:wffnvnkkLvhRex/aOrA3R7FP7rkvOqL/bir1br7BekU= -github.com/wailsapp/go-webview2 v1.0.16/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= -github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= +github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v3/examples/file-association/main.go b/v3/examples/file-association/main.go index b6f51e2c8..9fb6fa058 100644 --- a/v3/examples/file-association/main.go +++ b/v3/examples/file-association/main.go @@ -15,7 +15,7 @@ import ( // made available to the frontend. // See https://pkg.go.dev/embed for more information. -//go:embed frontend/dist +//go:embed frontend var assets embed.FS // main function serves as the application's entry point. It initializes the application, creates a window, @@ -48,7 +48,7 @@ func main() { // 'Mac' options tailor the window when running on macOS. // 'BackgroundColour' is the background colour of the window. // 'URL' is the URL that will be loaded into the webview. - window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Window 1", Mac: application.MacWindow{ InvisibleTitleBarHeight: 50, @@ -60,7 +60,7 @@ func main() { }) var filename string - app.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(event *application.ApplicationEvent) { + app.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(event *application.ApplicationEvent) { filename = event.Context().Filename() }) @@ -74,10 +74,16 @@ func main() { // Create a goroutine that emits an event containing the current time every second. // The frontend can listen to this event and update the UI accordingly. go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() for { - now := time.Now().Format(time.RFC1123) - app.EmitEvent("time", now) - time.Sleep(time.Second) + select { + case <-ticker.C: + now := time.Now().Format(time.RFC1123) + app.Event.Emit("time", now) + case <-app.Context().Done(): + return + } } }() diff --git a/v3/examples/frameless/assets/index.html b/v3/examples/frameless/assets/index.html index 8cc945d55..2776cfaa9 100644 --- a/v3/examples/frameless/assets/index.html +++ b/v3/examples/frameless/assets/index.html @@ -34,6 +34,10 @@ closeButton.addEventListener('click', function(event) { window.wails.Window.Close(); }); + let toggleFramelessButton = document.querySelector('#toggle-frameless'); + toggleFramelessButton.addEventListener('click', function(event) { + window.wails.Window.ToggleFrameless(); + }); }); @@ -42,6 +46,7 @@
Draggable
Not Draggable
Not Draggable
+
Not Draggable
Draggable
diff --git a/v3/examples/frameless/main.go b/v3/examples/frameless/main.go index 037ef5de7..93be412fd 100644 --- a/v3/examples/frameless/main.go +++ b/v3/examples/frameless/main.go @@ -24,7 +24,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true, }) diff --git a/v3/examples/gin-example/README.md b/v3/examples/gin-example/README.md new file mode 100644 index 000000000..b4d85cb9b --- /dev/null +++ b/v3/examples/gin-example/README.md @@ -0,0 +1,104 @@ +# Gin Example + +This example demonstrates how to use the [Gin web framework](https://github.com/gin-gonic/gin) with Wails. + +## Overview + +This example shows how to: + +- Set up Gin as the asset handler for a Wails application +- Create a middleware that routes requests between Wails and Gin +- Define API endpoints with Gin +- Communicate between the Gin-served frontend and Wails backend +- Implement custom Gin middleware + +## Running the Example + +```bash +cd v3/examples/gin-example +go mod tidy +go run . +``` + +## How It Works + +The example uses Gin's HTTP router to serve the frontend content whilst still allowing Wails to handle its internal routes. This is achieved through: + +1. Creating a Gin router with routes for the frontend +2. Implementing a middleware function that decides whether to pass requests to Gin or let Wails handle them +3. Configuring the Wails application to use both the Gin router as the asset handler and the custom middleware + +### Wails-Gin Integration + +The key part of the integration is the middleware function: + +```go +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 its internal routes + if r.URL.Path == "/wails/runtime.js" || r.URL.Path == "/wails/ipc" { + next.ServeHTTP(w, r) + return + } + + // Let Gin handle everything else + ginEngine.ServeHTTP(w, r) + }) + } +} +``` + +This allows you to leverage Gin's powerful routing and middleware capabilities whilst still maintaining full access to Wails features. + +### Custom Gin Middleware + +The example also demonstrates how to create custom Gin middleware: + +```go +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, + ) + } +} +``` + +This middleware is applied to all Gin routes and logs details about each request. + +### Application Configuration + +The Wails application is configured to use Gin as follows: + +```go +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), + }, +}) +``` + +This configuration tells Wails to: +1. Use the Gin engine as the primary handler for HTTP requests +2. Use our custom middleware to route requests between Wails and Gin diff --git a/v3/examples/gin-example/go.mod b/v3/examples/gin-example/go.mod new file mode 100644 index 000000000..ef253a93c --- /dev/null +++ b/v3/examples/gin-example/go.mod @@ -0,0 +1,72 @@ +module gin-example + +go 1.24.0 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/wailsapp/wails/v3 v3.0.0 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/cloudflare/circl v1.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.13.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lmittmann/tint v1.0.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/wailsapp/go-webview2 v1.0.21 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/wailsapp/wails/v3 => ../.. diff --git a/v3/examples/gin-example/go.sum b/v3/examples/gin-example/go.sum new file mode 100644 index 000000000..59438b668 --- /dev/null +++ b/v3/examples/gin-example/go.sum @@ -0,0 +1,203 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= +github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= +github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= +github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/v3/examples/gin-example/main.go b/v3/examples/gin-example/main.go new file mode 100644 index 000000000..8138d5403 --- /dev/null +++ b/v3/examples/gin-example/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "embed" + "log" + "net/http" + "strings" + "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 strings.HasPrefix(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 and store cleanup function + removeGinHandler := app.Event.On("gin-button-clicked", func(event *application.CustomEvent) { + log.Printf("Received event from frontend: %v", event.Data) + }) + // Note: In production, call removeGinHandler() during cleanup + _ = removeGinHandler + + // 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) + } +} diff --git a/v3/examples/gin-example/static/index.html b/v3/examples/gin-example/static/index.html new file mode 100644 index 000000000..df02b840f --- /dev/null +++ b/v3/examples/gin-example/static/index.html @@ -0,0 +1,96 @@ + + + Wails + Gin Example + + + +
+

Wails + Gin Integration

+
+

Hello World!

+

This page is being served by Gin.

+

Click the button below to trigger a Wails event:

+ + +
+
+

API Example

+

Try the Gin API endpoint:

+ +
+
+
+ + + diff --git a/v3/examples/gin-routing/README.md b/v3/examples/gin-routing/README.md new file mode 100644 index 000000000..5d8babc31 --- /dev/null +++ b/v3/examples/gin-routing/README.md @@ -0,0 +1,26 @@ +# Gin Routing Example + +This example demonstrates how to use the [Gin web framework](https://github.com/gin-gonic/gin) as a router with Wails. + +## Overview + +This example shows how to: + +- Set up Gin as the asset handler for a Wails application +- Create a middleware that routes requests between Wails and Gin +- Define API endpoints with Gin +- Communicate between the Gin-served frontend and Wails backend +- Implement custom Gin middleware + +## Running the Example + +```bash +cd v3/examples/gin-routing +go mod tidy +go run . +``` + +## Documentation + +Please consult the [Using Gin for Routing](https://v3.wails.io/guides/gin-routing/) guide for a detailed explanation of the example. + diff --git a/v3/examples/gin-routing/go.mod b/v3/examples/gin-routing/go.mod new file mode 100644 index 000000000..ef253a93c --- /dev/null +++ b/v3/examples/gin-routing/go.mod @@ -0,0 +1,72 @@ +module gin-example + +go 1.24.0 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/wailsapp/wails/v3 v3.0.0 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/cloudflare/circl v1.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.13.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lmittmann/tint v1.0.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/wailsapp/go-webview2 v1.0.21 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/wailsapp/wails/v3 => ../.. diff --git a/v3/examples/gin-routing/go.sum b/v3/examples/gin-routing/go.sum new file mode 100644 index 000000000..59438b668 --- /dev/null +++ b/v3/examples/gin-routing/go.sum @@ -0,0 +1,203 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= +github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= +github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= +github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/v3/examples/gin-routing/main.go b/v3/examples/gin-routing/main.go new file mode 100644 index 000000000..a78494808 --- /dev/null +++ b/v3/examples/gin-routing/main.go @@ -0,0 +1,114 @@ +package main + +import ( + "embed" + "log" + "net/http" + "strings" + "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 strings.HasPrefix(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) + } +} diff --git a/v3/examples/gin-routing/static/index.html b/v3/examples/gin-routing/static/index.html new file mode 100644 index 000000000..aefbd7a28 --- /dev/null +++ b/v3/examples/gin-routing/static/index.html @@ -0,0 +1,94 @@ + + + Wails + Gin Example + + + +
+

Wails + Gin Integration

+
+

Hello World!

+

This page is being served by Gin.

+

Click the button below to trigger a Wails event:

+ + +
+
+

API Example

+

Try the Gin API endpoint:

+ +
+
+
+ + + diff --git a/v3/examples/gin-service/README.md b/v3/examples/gin-service/README.md new file mode 100644 index 000000000..89e5019d7 --- /dev/null +++ b/v3/examples/gin-service/README.md @@ -0,0 +1,21 @@ +# Gin Service Example + +This example demonstrates how to use the [Gin web framework](https://github.com/gin-gonic/gin) in a Wails Service. + +## Overview + +This example shows how to: + +- Set up Gin as the asset handler for a Wails application +- Create a middleware that routes requests between Wails and Gin +- Define API endpoints with Gin +- Communicate between the Gin-served frontend and Wails backend +- Implement custom Gin middleware + +## Running the Example + + +## Documentation + +Please consult the [Using Gin for Routing](https://v3.wails.io/guides/gin-routing/) guide for a detailed explanation of the example. + diff --git a/v3/examples/gin-service/assets/index.html b/v3/examples/gin-service/assets/index.html new file mode 100644 index 000000000..34324b89f --- /dev/null +++ b/v3/examples/gin-service/assets/index.html @@ -0,0 +1,249 @@ + + + + + + Gin Service Example + + + +

Gin Service Example

+ +
+

API Endpoints

+

Try the Gin API endpoints mounted at /api:

+ + + + + + + +
+
Results will appear here...
+
+
+ +
+

Event Communication

+

Trigger an event to communicate with the Gin service:

+ + + +
+
Event responses will appear here...
+
+
+ + + + + + diff --git a/v3/examples/gin-service/go.mod b/v3/examples/gin-service/go.mod new file mode 100644 index 000000000..607c1701f --- /dev/null +++ b/v3/examples/gin-service/go.mod @@ -0,0 +1,74 @@ +module gin-service + +go 1.24.0 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/wailsapp/wails/v3 v3.0.0-alpha.9 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudflare/circl v1.6.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.13.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lmittmann/tint v1.0.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/wailsapp/go-webview2 v1.0.21 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/wailsapp/wails/v3 v3.0.0-alpha.9 => ../.. diff --git a/v3/examples/gin-service/go.sum b/v3/examples/gin-service/go.sum new file mode 100644 index 000000000..86b344c16 --- /dev/null +++ b/v3/examples/gin-service/go.sum @@ -0,0 +1,208 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= +github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= +github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= +github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/v3/examples/gin-service/main.go b/v3/examples/gin-service/main.go new file mode 100644 index 000000000..f27fef415 --- /dev/null +++ b/v3/examples/gin-service/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "embed" + "log/slog" + "os" + + "gin-service/services" + "github.com/wailsapp/wails/v3/pkg/application" +) + +//go:embed assets/* +var assets embed.FS + +func main() { + app := application.New(application.Options{ + Name: "Gin Service Demo", + Description: "A demo of using Gin in Wails services", + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + LogLevel: slog.LevelDebug, + Services: []application.Service{ + application.NewServiceWithOptions(services.NewGinService(), application.ServiceOptions{ + Route: "/api", + }), + }, + Assets: application.AssetOptions{ + Handler: application.BundledAssetFileServer(assets), + }, + }) + + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Gin Service Demo", + Width: 1024, + Height: 768, + }) + + err := app.Run() + + if err != nil { + println(err.Error()) + os.Exit(1) + } +} diff --git a/v3/examples/gin-service/services/gin_service.go b/v3/examples/gin-service/services/gin_service.go new file mode 100644 index 000000000..e0fa290ec --- /dev/null +++ b/v3/examples/gin-service/services/gin_service.go @@ -0,0 +1,250 @@ +package services + +import ( + "context" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/wailsapp/wails/v3/pkg/application" +) + +// User represents a user in the system +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"createdAt"` +} + +// GinService implements a Wails service that uses Gin for HTTP handling +type GinService struct { + ginEngine *gin.Engine + users []User + nextID int + mu sync.RWMutex + app *application.App + maxUsers int // Maximum number of users to prevent unbounded growth + removeEventHandler func() // Store cleanup function for event handler +} + +type EventData struct { + Message string `json:"message"` + Timestamp string `json:"timestamp"` +} + +// NewGinService creates a new GinService instance +func NewGinService() *GinService { + // Create a new Gin router + ginEngine := gin.New() + + // Add middlewares + ginEngine.Use(gin.Recovery()) + ginEngine.Use(LoggingMiddleware()) + + service := &GinService{ + ginEngine: ginEngine, + users: []User{ + {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now().Add(-72 * time.Hour)}, + {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now().Add(-48 * time.Hour)}, + {ID: 3, Name: "Charlie", Email: "charlie@example.com", CreatedAt: time.Now().Add(-24 * time.Hour)}, + }, + nextID: 4, + maxUsers: 1000, // Limit to prevent unbounded slice growth + } + + // Define routes + service.setupRoutes() + + return service +} + +// ServiceName returns the name of the service +func (s *GinService) ServiceName() string { + return "Gin API Service" +} + +// ServiceStartup is called when the service starts +func (s *GinService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + // You can access the application instance via ctx + s.app = application.Get() + + // Register an event handler that can be triggered from the frontend + // Store the cleanup function for proper resource management + s.removeEventHandler = s.app.Event.On("gin-api-event", func(event *application.CustomEvent) { + // Log the event data + // Parse the event data + s.app.Logger.Info("Received event from frontend", "data", event.Data) + + // You could also emit an event back to the frontend + s.app.Event.Emit("gin-api-response", map[string]interface{}{ + "message": "Response from Gin API Service", + "time": time.Now().Format(time.RFC3339), + }) + }) + + return nil +} + +// ServiceShutdown is called when the service shuts down +func (s *GinService) ServiceShutdown(ctx context.Context) error { + // Clean up event handler to prevent memory leaks + if s.removeEventHandler != nil { + s.removeEventHandler() + } + return nil +} + +// ServeHTTP implements the http.Handler interface +func (s *GinService) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // All other requests go to the Gin router + s.ginEngine.ServeHTTP(w, r) +} + +// setupRoutes configures the API routes +func (s *GinService) setupRoutes() { + // Basic info endpoint + s.ginEngine.GET("/info", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "service": "Gin API Service", + "version": "1.0.0", + "time": time.Now().Format(time.RFC3339), + }) + }) + + // Users group + users := s.ginEngine.Group("/users") + { + // Get all users + users.GET("", func(c *gin.Context) { + s.mu.RLock() + defer s.mu.RUnlock() + c.JSON(http.StatusOK, s.users) + }) + + // Get user by ID + users.GET("/:id", func(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + + s.mu.RLock() + defer s.mu.RUnlock() + + for _, user := range s.users { + if user.ID == id { + c.JSON(http.StatusOK, user) + return + } + } + + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + }) + + // Create a new user + users.POST("", func(c *gin.Context) { + var newUser User + if err := c.ShouldBindJSON(&newUser); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Validate required fields + if newUser.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Name is required"}) + return + } + if newUser.Email == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Email is required"}) + return + } + // Basic email validation (consider using a proper validator library in production) + if !strings.Contains(newUser.Email, "@") { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email format"}) + return + } + + s.mu.Lock() + defer s.mu.Unlock() + + // Check if we've reached the maximum number of users + if len(s.users) >= s.maxUsers { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Maximum number of users reached"}) + return + } + + // Set the ID and creation time + newUser.ID = s.nextID + newUser.CreatedAt = time.Now() + s.nextID++ + + // Add to the users slice + s.users = append(s.users, newUser) + + c.JSON(http.StatusCreated, newUser) + + // Emit an event to notify about the new user + s.app.Event.Emit("user-created", newUser) + }) + + // Delete a user + users.DELETE("/:id", func(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + + s.mu.Lock() + defer s.mu.Unlock() + + for i, user := range s.users { + if user.ID == id { + // Remove the user from the slice + s.users = append(s.users[:i], s.users[i+1:]...) + c.JSON(http.StatusOK, gin.H{"message": "User deleted"}) + return + } + } + + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + }) + } +} + +// LoggingMiddleware is a Gin middleware that logs request details +func LoggingMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Start timer + start := time.Now() + + // Process request + c.Next() + + // Calculate latency + latency := time.Since(start) + + // Log request details + statusCode := c.Writer.Status() + clientIP := c.ClientIP() + method := c.Request.Method + path := c.Request.URL.Path + + // Get the application instance + app := application.Get() + if app != nil { + app.Logger.Info("HTTP Request", + "status", statusCode, + "method", method, + "path", path, + "ip", clientIP, + "latency", latency, + ) + } + } +} diff --git a/v3/examples/hide-window/main.go b/v3/examples/hide-window/main.go index df89c7d4a..1a65c7498 100644 --- a/v3/examples/hide-window/main.go +++ b/v3/examples/hide-window/main.go @@ -19,9 +19,9 @@ func main() { }, }) - systemTray := app.NewSystemTray() + systemTray := app.SystemTray.New() - window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Width: 500, Height: 800, Frameless: false, @@ -43,7 +43,7 @@ func main() { } // Click Dock icon tigger application show - app.OnApplicationEvent(events.Mac.ApplicationShouldHandleReopen, func(event *application.ApplicationEvent) { + app.Event.OnApplicationEvent(events.Mac.ApplicationShouldHandleReopen, func(event *application.ApplicationEvent) { println("reopen") window.Show() }) diff --git a/v3/examples/html-dnd-api/main.go b/v3/examples/html-dnd-api/main.go index 9d3d37ffa..549523ef1 100644 --- a/v3/examples/html-dnd-api/main.go +++ b/v3/examples/html-dnd-api/main.go @@ -23,7 +23,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Drag-n-drop Demo", Mac: application.MacWindow{ Backdrop: application.MacBackdropTranslucent, diff --git a/v3/examples/ignore-mouse/main.go b/v3/examples/ignore-mouse/main.go index 869fdd9c5..1fc0304f2 100644 --- a/v3/examples/ignore-mouse/main.go +++ b/v3/examples/ignore-mouse/main.go @@ -16,7 +16,7 @@ func main() { }, }) - window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Width: 800, Height: 600, Title: "Ignore Mouse Example", diff --git a/v3/examples/keybindings/main.go b/v3/examples/keybindings/main.go index ee9dee825..7fb48e23e 100644 --- a/v3/examples/keybindings/main.go +++ b/v3/examples/keybindings/main.go @@ -20,7 +20,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Name: "Window 1", Title: "Window 1", URL: "https://wails.io", @@ -31,7 +31,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Name: "Window 2", Title: "Window 2", URL: "https://google.com", diff --git a/v3/examples/menu/main.go b/v3/examples/menu/main.go index 62bcb454b..0349eceb6 100644 --- a/v3/examples/menu/main.go +++ b/v3/examples/menu/main.go @@ -27,16 +27,7 @@ func main() { if runtime.GOOS == "darwin" { menu.AddRole(application.AppMenu) } - fileMenu := menu.AddRole(application.FileMenu) - _ = fileMenu - //fileMenu.FindByRole(application.Open).OnClick(func(context *application.Context) { - // selection, err := application.OpenFileDialog().PromptForSingleSelection() - // if err != nil { - // println("Error: " + err.Error()) - // return - // } - // println("You selected: " + selection) - //}) + menu.AddRole(application.FileMenu) menu.AddRole(application.EditMenu) menu.AddRole(application.WindowMenu) menu.AddRole(application.HelpMenu) @@ -44,6 +35,12 @@ func main() { // Let's make a "Demo" menu myMenu := menu.AddSubmenu("Demo") + // Hidden menu item that can be unhidden + hidden := myMenu.Add("I was hidden").SetHidden(true) + myMenu.Add("Toggle the hidden menu").OnClick(func(ctx *application.Context) { + hidden.SetHidden(!hidden.Hidden()) + }) + // Disabled menu item myMenu.Add("Not Enabled").SetEnabled(false) @@ -59,11 +56,11 @@ func main() { // You can control the current window from the menu myMenu.Add("Lock WebviewWindow Resize").OnClick(func(ctx *application.Context) { - if app.CurrentWindow().Resizable() { - app.CurrentWindow().SetResizable(false) + if app.Window.Current().Resizable() { + app.Window.Current().SetResizable(false) ctx.ClickedMenuItem().SetLabel("Unlock WebviewWindow Resize") } else { - app.CurrentWindow().SetResizable(true) + app.Window.Current().SetResizable(true) ctx.ClickedMenuItem().SetLabel("Lock WebviewWindow Resize") } }) @@ -116,9 +113,41 @@ func main() { ctx.ClickedMenuItem().SetLabel("Unhide the beatles!") } }) - app.SetMenu(menu) - app.NewWebviewWindow().SetBackgroundColour(application.NewRGB(33, 37, 41)) + myMenu.AddSeparator() + + coffee := myMenu.Add("Request Coffee").OnClick(func(*application.Context) { + println("Coffee dispatched. Productivity +10!") + }) + + myMenu.Add("Toggle coffee availability").OnClick(func(*application.Context) { + if coffee.Enabled() { + coffee.SetEnabled(false) + coffee.SetLabel("Coffee Machine Broken") + println("Alert: Developer morale critically low.") + } else { + coffee.SetEnabled(true) + coffee.SetLabel("Request Coffee") + println("All systems nominal. Coffee restored.") + } + }) + + myMenu.Add("Hide the coffee option").OnClick(func(ctx *application.Context) { + if coffee.Hidden() { + ctx.ClickedMenuItem().SetLabel("Hide the coffee option") + coffee.SetHidden(false) + println("Coffee menu item has been resurrected!") + } else { + coffee.SetHidden(true) + ctx.ClickedMenuItem().SetLabel("Unhide the coffee option") + println("The coffee option has vanished into the void.") + } + }) + + app.Menu.Set(menu) + + window := app.Window.New().SetBackgroundColour(application.NewRGB(33, 37, 41)) + window.SetMenu(menu) err := app.Run() diff --git a/v3/examples/notifications/README.md b/v3/examples/notifications/README.md new file mode 100644 index 000000000..ad12c3f40 --- /dev/null +++ b/v3/examples/notifications/README.md @@ -0,0 +1,59 @@ +# Welcome to Your New Wails3 Project! + +Congratulations on generating your Wails3 application! This README will guide you through the next steps to get your project up and running. + +## Getting Started + +1. Navigate to your project directory in the terminal. + +2. To run your application in development mode, use the following command: + + ``` + wails3 dev + ``` + + This will start your application and enable hot-reloading for both frontend and backend changes. + +3. To build your application for production, use: + + ``` + wails3 build + ``` + + This will create a production-ready executable in the `build` directory. + +## Exploring Wails3 Features + +Now that you have your project set up, it's time to explore the features that Wails3 offers: + +1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications. + +2. **Run an example**: To run any of the examples, navigate to the example's directory and use: + + ``` + go run . + ``` + + Note: Some examples may be under development during the alpha phase. + +3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3.wails.io/) for in-depth guides and API references. + +4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions). + +## Project Structure + +Take a moment to familiarize yourself with your project structure: + +- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript) +- `main.go`: The entry point of your Go backend +- `app.go`: Define your application structure and methods here +- `wails.json`: Configuration file for your Wails project + +## Next Steps + +1. Modify the frontend in the `frontend/` directory to create your desired UI. +2. Add backend functionality in `main.go`. +3. Use `wails3 dev` to see your changes in real-time. +4. When ready, build your application with `wails3 build`. + +Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community. diff --git a/v3/examples/notifications/Taskfile.yml b/v3/examples/notifications/Taskfile.yml new file mode 100644 index 000000000..1455cd70c --- /dev/null +++ b/v3/examples/notifications/Taskfile.yml @@ -0,0 +1,34 @@ +version: '3' + +includes: + common: ./build/Taskfile.yml + windows: ./build/windows/Taskfile.yml + darwin: ./build/darwin/Taskfile.yml + linux: ./build/linux/Taskfile.yml + +vars: + APP_NAME: "Notifications\\ Demo" + BIN_DIR: "bin" + VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' + +tasks: + build: + summary: Builds the application + cmds: + - task: "{{OS}}:build" + + package: + summary: Packages a production build of the application + cmds: + - task: "{{OS}}:package" + + run: + summary: Runs the application + cmds: + - task: "{{OS}}:run" + + dev: + summary: Runs the application in development mode + cmds: + - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} + diff --git a/v3/examples/notifications/build/Taskfile.yml b/v3/examples/notifications/build/Taskfile.yml new file mode 100644 index 000000000..5f3517efc --- /dev/null +++ b/v3/examples/notifications/build/Taskfile.yml @@ -0,0 +1,86 @@ +version: '3' + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + dir: frontend + sources: + - package.json + - package-lock.json + generates: + - node_modules/* + preconditions: + - sh: npm version + msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/" + cmds: + - npm install + + build:frontend: + label: build:frontend (PRODUCTION={{.PRODUCTION}}) + summary: Build the frontend project + dir: frontend + sources: + - "**/*" + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - npm run {{.BUILD_COMMAND}} -q + env: + PRODUCTION: '{{.PRODUCTION | default "false"}}' + vars: + BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build:dev{{end}}' + + + generate:bindings: + label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) + summary: Generates bindings for the frontend + deps: + - task: go:mod:tidy + sources: + - "**/*.[jt]s" + - exclude: frontend/**/* + - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true -ts + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` files from an image + dir: build + sources: + - "appicon.png" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico + + dev:frontend: + summary: Runs the frontend in development mode + dir: frontend + deps: + - task: install:frontend:deps + cmds: + - npm run dev -- --port {{.VITE_PORT}} --strictPort + + update:build-assets: + summary: Updates the build assets + dir: build + cmds: + - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . diff --git a/v3/examples/notifications/build/appicon.png b/v3/examples/notifications/build/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/v3/examples/notifications/build/appicon.png differ diff --git a/v3/examples/notifications/build/config.yml b/v3/examples/notifications/build/config.yml new file mode 100644 index 000000000..bc09a6d28 --- /dev/null +++ b/v3/examples/notifications/build/config.yml @@ -0,0 +1,62 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: '3' + +# This information is used to generate the build assets. +info: + companyName: "My Company" # The name of the company + productName: "My Product" # The name of the application + productIdentifier: "com.mycompany.myproduct" # The unique product identifier + description: "A program that does X" # The application description + copyright: "(c) 2025, My Company" # Copyright text + comments: "Some Product Comments" # Comments + version: "v0.0.1" # The application version + +# Dev mode configuration +dev_mode: + root_path: . + log_level: warn + debounce: 1000 + ignore: + dir: + - .git + - node_modules + - frontend + - bin + file: + - .DS_Store + - .gitignore + - .gitkeep + watched_extension: + - "*.go" + git_ignore: true + executes: + - cmd: wails3 task common:install:frontend:deps + type: once + - cmd: wails3 task common:dev:frontend + type: background + - cmd: go mod tidy + type: blocking + - cmd: wails3 task build + type: blocking + - cmd: wails3 task run + type: primary + +# File Associations +# More information at: https://v3.wails.io/noit/done/yet +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor + +# Other data +other: + - name: My Other Data \ No newline at end of file diff --git a/v3/examples/notifications/build/darwin/Info.dev.plist b/v3/examples/notifications/build/darwin/Info.dev.plist new file mode 100644 index 000000000..3a5b9735f --- /dev/null +++ b/v3/examples/notifications/build/darwin/Info.dev.plist @@ -0,0 +1,32 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + Notifications Demo + CFBundleIdentifier + com.wails.notifications-demo + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + Β© now, My Company + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + \ No newline at end of file diff --git a/v3/examples/notifications/build/darwin/Info.plist b/v3/examples/notifications/build/darwin/Info.plist new file mode 100644 index 000000000..464270019 --- /dev/null +++ b/v3/examples/notifications/build/darwin/Info.plist @@ -0,0 +1,27 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + Notifications Demo + CFBundleIdentifier + com.wails.notifications-demo + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + Β© now, My Company + + \ No newline at end of file diff --git a/v3/examples/notifications/build/darwin/Taskfile.yml b/v3/examples/notifications/build/darwin/Taskfile.yml new file mode 100644 index 000000000..3b6a9dc99 --- /dev/null +++ b/v3/examples/notifications/build/darwin/Taskfile.yml @@ -0,0 +1,80 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Creates a production build of the application + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: darwin + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + CGO_CFLAGS: "-mmacosx-version-min=10.15" + CGO_LDFLAGS: "-mmacosx-version-min=10.15" + MACOSX_DEPLOYMENT_TARGET: "10.15" + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + build:universal: + summary: Builds darwin universal binary (arm64 + amd64) + deps: + - task: build + vars: + ARCH: amd64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" + - task: build + vars: + ARCH: arm64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + cmds: + - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + package: + summary: Packages a production build of the application into a `.app` bundle + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:app:bundle + + package:universal: + summary: Packages darwin universal binary (arm64 + amd64) + deps: + - task: build:universal + cmds: + - task: create:app:bundle + + + create:app:bundle: + summary: Creates an `.app` bundle + cmds: + - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} + - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources + - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS + - cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents + + run: + cmds: + - mkdir -p {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/{MacOS,Resources} + - cp build/darwin/icons.icns {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/Resources + - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/MacOS + - cp build/darwin/Info.dev.plist {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/Info.plist + - codesign --force --deep --sign - {{.BIN_DIR}}/dev/{{.APP_NAME}}.app + - '{{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/MacOS/{{.APP_NAME}}' diff --git a/v3/examples/notifications/build/darwin/icons.icns b/v3/examples/notifications/build/darwin/icons.icns new file mode 100644 index 000000000..1b5bd4c86 Binary files /dev/null and b/v3/examples/notifications/build/darwin/icons.icns differ diff --git a/v3/examples/notifications/build/linux/Taskfile.yml b/v3/examples/notifications/build/linux/Taskfile.yml new file mode 100644 index 000000000..560cc9c92 --- /dev/null +++ b/v3/examples/notifications/build/linux/Taskfile.yml @@ -0,0 +1,119 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds the application for Linux + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + env: + GOOS: linux + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application for Linux + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:appimage + - task: create:deb + - task: create:rpm + - task: create:aur + + create:appimage: + summary: Creates an AppImage + dir: build/linux/appimage + deps: + - task: build + vars: + PRODUCTION: "true" + - task: generate:dotdesktop + cmds: + - cp {{.APP_BINARY}} {{.APP_NAME}} + - cp ../../appicon.png appicon.png + - wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build + vars: + APP_NAME: '{{.APP_NAME}}' + APP_BINARY: '../../../bin/{{.APP_NAME}}' + ICON: '../../appicon.png' + DESKTOP_FILE: '../{{.APP_NAME}}.desktop' + OUTPUT_DIR: '../../../bin' + + create:deb: + summary: Creates a deb package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:deb + + create:rpm: + summary: Creates a rpm package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:rpm + + create:aur: + summary: Creates a arch linux packager package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:aur + + generate:deb: + summary: Creates a deb package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:rpm: + summary: Creates a rpm package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:aur: + summary: Creates a arch linux packager package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:dotdesktop: + summary: Generates a `.desktop` file + dir: build + cmds: + - mkdir -p {{.ROOT_DIR}}/build/linux/appimage + - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}" + vars: + APP_NAME: '{{.APP_NAME}}' + EXEC: '{{.APP_NAME}}' + ICON: 'appicon' + CATEGORIES: 'Development;' + OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}' diff --git a/v3/examples/notifications/build/linux/appimage/build.sh b/v3/examples/notifications/build/linux/appimage/build.sh new file mode 100644 index 000000000..85901c34e --- /dev/null +++ b/v3/examples/notifications/build/linux/appimage/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) 2018-Present Lea Anthony +# SPDX-License-Identifier: MIT + +# Fail script on any error +set -euxo pipefail + +# Define variables +APP_DIR="${APP_NAME}.AppDir" + +# Create AppDir structure +mkdir -p "${APP_DIR}/usr/bin" +cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" +cp "${ICON_PATH}" "${APP_DIR}/" +cp "${DESKTOP_FILE}" "${APP_DIR}/" + +if [[ $(uname -m) == *x86_64* ]]; then + # Download linuxdeploy and make it executable + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + + # Run linuxdeploy to bundle the application + ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage +else + # Download linuxdeploy and make it executable (arm64) + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage + chmod +x linuxdeploy-aarch64.AppImage + + # Run linuxdeploy to bundle the application (arm64) + ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage +fi + +# Rename the generated AppImage +mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage" + diff --git a/v3/examples/notifications/build/linux/nfpm/nfpm.yaml b/v3/examples/notifications/build/linux/nfpm/nfpm.yaml new file mode 100644 index 000000000..c2cb7cd81 --- /dev/null +++ b/v3/examples/notifications/build/linux/nfpm/nfpm.yaml @@ -0,0 +1,50 @@ +# Feel free to remove those if you don't want/need to use them. +# Make sure to check the documentation at https://nfpm.goreleaser.com +# +# The lines below are called `modelines`. See `:help modeline` + +name: "notifications" +arch: ${GOARCH} +platform: "linux" +version: "0.1.0" +section: "default" +priority: "extra" +maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> +description: "My Product Description" +vendor: "My Company" +homepage: "https://wails.io" +license: "MIT" +release: "1" + +contents: + - src: "./bin/notifications" + dst: "/usr/local/bin/notifications" + - src: "./build/appicon.png" + dst: "/usr/share/icons/hicolor/128x128/apps/notifications.png" + - src: "./build/linux/notifications.desktop" + dst: "/usr/share/applications/notifications.desktop" + +depends: + - gtk3 + - libwebkit2gtk + +# replaces: +# - foobar +# provides: +# - bar +# depends: +# - gtk3 +# - libwebkit2gtk +# recommends: +# - whatever +# suggests: +# - something-else +# conflicts: +# - not-foo +# - not-bar +# changelog: "changelog.yaml" +# scripts: +# preinstall: ./build/linux/nfpm/scripts/preinstall.sh +# postinstall: ./build/linux/nfpm/scripts/postinstall.sh +# preremove: ./build/linux/nfpm/scripts/preremove.sh +# postremove: ./build/linux/nfpm/scripts/postremove.sh diff --git a/v3/examples/notifications/build/linux/nfpm/scripts/postinstall.sh b/v3/examples/notifications/build/linux/nfpm/scripts/postinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/notifications/build/linux/nfpm/scripts/postinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/notifications/build/linux/nfpm/scripts/postremove.sh b/v3/examples/notifications/build/linux/nfpm/scripts/postremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/notifications/build/linux/nfpm/scripts/postremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/notifications/build/linux/nfpm/scripts/preinstall.sh b/v3/examples/notifications/build/linux/nfpm/scripts/preinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/notifications/build/linux/nfpm/scripts/preinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/notifications/build/linux/nfpm/scripts/preremove.sh b/v3/examples/notifications/build/linux/nfpm/scripts/preremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/notifications/build/linux/nfpm/scripts/preremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/notifications/build/windows/Taskfile.yml b/v3/examples/notifications/build/windows/Taskfile.yml new file mode 100644 index 000000000..be6e4125e --- /dev/null +++ b/v3/examples/notifications/build/windows/Taskfile.yml @@ -0,0 +1,63 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds the application for Windows + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + env: + GOOS: windows + CGO_ENABLED: 0 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application into a `.exe` bundle + cmds: + - task: create:nsis:installer + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: '{{.ARCH | default ARCH}}' + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + vars: + ARCH: '{{.ARCH | default ARCH}}' + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + + run: + cmds: + - '{{.BIN_DIR}}\\{{.APP_NAME}}.exe' diff --git a/v3/examples/notifications/build/windows/icon.ico b/v3/examples/notifications/build/windows/icon.ico new file mode 100644 index 000000000..bfa0690b7 Binary files /dev/null and b/v3/examples/notifications/build/windows/icon.ico differ diff --git a/v3/examples/notifications/build/windows/info.json b/v3/examples/notifications/build/windows/info.json new file mode 100644 index 000000000..850b2b5b0 --- /dev/null +++ b/v3/examples/notifications/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "0.1.0" + }, + "info": { + "0000": { + "ProductVersion": "0.1.0", + "CompanyName": "My Company", + "FileDescription": "My Product Description", + "LegalCopyright": "Β© now, My Company", + "ProductName": "My Product", + "Comments": "This is a comment" + } + } +} \ No newline at end of file diff --git a/v3/examples/notifications/build/windows/nsis/project.nsi b/v3/examples/notifications/build/windows/nsis/project.nsi new file mode 100644 index 000000000..4cb18e04f --- /dev/null +++ b/v3/examples/notifications/build/windows/nsis/project.nsi @@ -0,0 +1,112 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the wails_tools.nsh file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "my-project" # Default "notifications" +## !define INFO_COMPANYNAME "My Company" # Default "My Company" +## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "Β© now, My Company" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/v3/examples/notifications/build/windows/nsis/wails_tools.nsh b/v3/examples/notifications/build/windows/nsis/wails_tools.nsh new file mode 100644 index 000000000..c47c784a4 --- /dev/null +++ b/v3/examples/notifications/build/windows/nsis/wails_tools.nsh @@ -0,0 +1,212 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "notifications" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "My Company" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "My Product" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "0.1.0" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "Β© now, My Company" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + +!macroend \ No newline at end of file diff --git a/v3/examples/notifications/build/windows/wails.exe.manifest b/v3/examples/notifications/build/windows/wails.exe.manifest new file mode 100644 index 000000000..0299e62ca --- /dev/null +++ b/v3/examples/notifications/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/v3/examples/notifications/frontend/Inter Font License.txt b/v3/examples/notifications/frontend/Inter Font License.txt new file mode 100644 index 000000000..b525cbf3a --- /dev/null +++ b/v3/examples/notifications/frontend/Inter Font License.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/index.ts b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/index.ts new file mode 100644 index 000000000..71eda3bb9 --- /dev/null +++ b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/index.ts @@ -0,0 +1,13 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as NotificationService from "./notificationservice.js"; +export { + NotificationService +}; + +export { + NotificationAction, + NotificationCategory, + NotificationOptions +} from "./models.js"; diff --git a/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/models.ts b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/models.ts new file mode 100644 index 000000000..d7f48edfe --- /dev/null +++ b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/models.ts @@ -0,0 +1,107 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +/** + * NotificationAction represents an action button for a notification. + */ +export class NotificationAction { + "id"?: string; + "title"?: string; + + /** + * (macOS-specific) + */ + "destructive"?: boolean; + + /** Creates a new NotificationAction instance. */ + constructor($$source: Partial = {}) { + + Object.assign(this, $$source); + } + + /** + * Creates a new NotificationAction instance from a string or object. + */ + static createFrom($$source: any = {}): NotificationAction { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new NotificationAction($$parsedSource as Partial); + } +} + +/** + * NotificationCategory groups actions for notifications. + */ +export class NotificationCategory { + "id"?: string; + "actions"?: NotificationAction[]; + "hasReplyField"?: boolean; + "replyPlaceholder"?: string; + "replyButtonTitle"?: string; + + /** Creates a new NotificationCategory instance. */ + constructor($$source: Partial = {}) { + + Object.assign(this, $$source); + } + + /** + * Creates a new NotificationCategory instance from a string or object. + */ + static createFrom($$source: any = {}): NotificationCategory { + const $$createField1_0 = $$createType1; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("actions" in $$parsedSource) { + $$parsedSource["actions"] = $$createField1_0($$parsedSource["actions"]); + } + return new NotificationCategory($$parsedSource as Partial); + } +} + +/** + * NotificationOptions contains configuration for a notification + */ +export class NotificationOptions { + "id": string; + "title": string; + + /** + * (macOS and Linux only) + */ + "subtitle"?: string; + "body"?: string; + "categoryId"?: string; + "data"?: { [_: string]: any }; + + /** Creates a new NotificationOptions instance. */ + constructor($$source: Partial = {}) { + if (!("id" in $$source)) { + this["id"] = ""; + } + if (!("title" in $$source)) { + this["title"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new NotificationOptions instance from a string or object. + */ + static createFrom($$source: any = {}): NotificationOptions { + const $$createField5_0 = $$createType2; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("data" in $$parsedSource) { + $$parsedSource["data"] = $$createField5_0($$parsedSource["data"]); + } + return new NotificationOptions($$parsedSource as Partial); + } +} + +// Private type creation functions +const $$createType0 = NotificationAction.createFrom; +const $$createType1 = $Create.Array($$createType0); +const $$createType2 = $Create.Map($Create.Any, $Create.Any); diff --git a/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/notificationservice.ts b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/notificationservice.ts new file mode 100644 index 000000000..859f3570f --- /dev/null +++ b/v3/examples/notifications/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/notifications/notificationservice.ts @@ -0,0 +1,62 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * Service represents the notifications service + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +export function CheckNotificationAuthorization(): $CancellablePromise { + return $Call.ByID(2216952893); +} + +export function RegisterNotificationCategory(category: $models.NotificationCategory): $CancellablePromise { + return $Call.ByID(2917562919, category); +} + +export function RemoveAllDeliveredNotifications(): $CancellablePromise { + return $Call.ByID(3956282340); +} + +export function RemoveAllPendingNotifications(): $CancellablePromise { + return $Call.ByID(108821341); +} + +export function RemoveDeliveredNotification(identifier: string): $CancellablePromise { + return $Call.ByID(975691940, identifier); +} + +export function RemoveNotification(identifier: string): $CancellablePromise { + return $Call.ByID(3966653866, identifier); +} + +export function RemoveNotificationCategory(categoryID: string): $CancellablePromise { + return $Call.ByID(2032615554, categoryID); +} + +export function RemovePendingNotification(identifier: string): $CancellablePromise { + return $Call.ByID(3729049703, identifier); +} + +/** + * Public methods that delegate to the implementation. + */ +export function RequestNotificationAuthorization(): $CancellablePromise { + return $Call.ByID(3933442950); +} + +export function SendNotification(options: $models.NotificationOptions): $CancellablePromise { + return $Call.ByID(3968228732, options); +} + +export function SendNotificationWithActions(options: $models.NotificationOptions): $CancellablePromise { + return $Call.ByID(1886542847, options); +} diff --git a/v3/examples/notifications/frontend/dist/Inter-Medium.ttf b/v3/examples/notifications/frontend/dist/Inter-Medium.ttf new file mode 100644 index 000000000..a01f3777a Binary files /dev/null and b/v3/examples/notifications/frontend/dist/Inter-Medium.ttf differ diff --git a/v3/examples/notifications/frontend/dist/assets/index-Dat4utuQ.js b/v3/examples/notifications/frontend/dist/assets/index-Dat4utuQ.js new file mode 100644 index 000000000..b1c054dfb --- /dev/null +++ b/v3/examples/notifications/frontend/dist/assets/index-Dat4utuQ.js @@ -0,0 +1,33 @@ +(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(o){if(o.ep)return;o.ep=!0;const i=n(o);fetch(o.href,i)}})();const fe="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";function oe(t=21){let e="",n=t|0;for(;n--;)e+=fe[Math.random()*64|0];return e}const pe=window.location.origin+"/wails/runtime",x=Object.freeze({Call:0,Clipboard:1,Application:2,Events:3,ContextMenu:4,Dialog:5,Window:6,Screens:7,System:8,Browser:9,CancelCall:10});let we=oe();function C(t,e=""){return function(n,r=null){return he(t,n,e,r)}}async function he(t,e,n,r){var o,i;let s=new URL(pe);s.searchParams.append("object",t.toString()),s.searchParams.append("method",e.toString()),r&&s.searchParams.append("args",JSON.stringify(r));let a={"x-wails-client-id":we};n&&(a["x-wails-window-name"]=n);let c=await fetch(s,{headers:a});if(!c.ok)throw new Error(await c.text());return((i=(o=c.headers.get("Content-Type"))===null||o===void 0?void 0:o.indexOf("application/json"))!==null&&i!==void 0?i:-1)!==-1?c.json():c.text()}C(x.System);const H=function(){var t,e,n,r,o;try{if(!((e=(t=window.chrome)===null||t===void 0?void 0:t.webview)===null||e===void 0)&&e.postMessage)return window.chrome.webview.postMessage.bind(window.chrome.webview);if(!((o=(r=(n=window.webkit)===null||n===void 0?void 0:n.messageHandlers)===null||r===void 0?void 0:r.external)===null||o===void 0)&&o.postMessage)return window.webkit.messageHandlers.external.postMessage.bind(window.webkit.messageHandlers.external)}catch{}return console.warn(` +%c⚠️ Browser Environment Detected %c + +%cOnly UI previews are available in the browser. For full functionality, please run the application in desktop mode. +More information at: https://v3.wails.io/learn/build/#using-a-browser-for-development +`,"background: #ffffff; color: #000000; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 2px solid #000000;","background: transparent;","color: #ffffff; font-style: italic; font-weight: bold;"),null}();function T(t){H==null||H(t)}function ie(){return window._wails.environment.OS==="windows"}function me(){return!!window._wails.environment.Debug}function ye(){return new MouseEvent("mousedown").buttons===0}function se(t){var e;return t.target instanceof HTMLElement?t.target:!(t.target instanceof HTMLElement)&&t.target instanceof Node&&(e=t.target.parentElement)!==null&&e!==void 0?e:document.body}document.addEventListener("DOMContentLoaded",()=>{});window.addEventListener("contextmenu",je);const ge=C(x.ContextMenu),be=0;function ve(t,e,n,r){ge(be,{id:t,x:e,y:n,data:r})}function je(t){const e=se(t),n=window.getComputedStyle(e).getPropertyValue("--custom-contextmenu").trim();if(n){t.preventDefault();const r=window.getComputedStyle(e).getPropertyValue("--custom-contextmenu-data");ve(n,t.clientX,t.clientY,r)}else Ee(t,e)}function Ee(t,e){if(me())return;switch(window.getComputedStyle(e).getPropertyValue("--default-contextmenu").trim()){case"show":return;case"hide":t.preventDefault();return}if(e.isContentEditable)return;const n=window.getSelection(),r=n&&n.toString().length>0;if(r)for(let o=0;o{F=t,F||(j=E=!1,u())};window.addEventListener("mousedown",Y,{capture:!0});window.addEventListener("mousemove",Y,{capture:!0});window.addEventListener("mouseup",Y,{capture:!0});for(const t of["click","contextmenu","dblclick"])window.addEventListener(t,ze,{capture:!0});function ze(t){(S||E)&&(t.stopImmediatePropagation(),t.stopPropagation(),t.preventDefault())}const I=0,Se=1,U=2;function Y(t){let e,n=t.buttons;switch(t.type){case"mousedown":e=I,P||(n=y|1<"u"||typeof e=="object"))try{var n=O.call(e);return(n===Ne||n===De||n===He||n===Re)&&e("")==null}catch{}return!1})}function Ue(t){if(V(t))return!0;if(!t||typeof t!="function"&&typeof t!="object")return!1;try{b(t,null,_)}catch(e){if(e!==R)return!1}return!$(t)&&B(t)}function qe(t){if(V(t))return!0;if(!t||typeof t!="function"&&typeof t!="object")return!1;if(Ae)return B(t);if($(t))return!1;var e=O.call(t);return e!==Oe&&e!==Te&&!/^\[object HTML/.test(e)?!1:B(t)}const m=b?Ue:qe;var q;class W extends Error{constructor(e,n){super(e,n),this.name="CancelError"}}class M extends Error{constructor(e,n,r){super((r??"Unhandled rejection in cancelled promise.")+" Reason: "+Fe(n),{cause:n}),this.promise=e,this.name="CancelledRejectionError"}}const p=Symbol("barrier"),G=Symbol("cancelImpl"),K=(q=Symbol.species)!==null&&q!==void 0?q:Symbol("speciesPolyfill");class l extends Promise{constructor(e,n){let r,o;if(super((c,f)=>{r=c,o=f}),this.constructor[K]!==Promise)throw new TypeError("CancellablePromise does not support transparent subclassing. Please refrain from overriding the [Symbol.species] static property.");let i={promise:this,resolve:r,reject:o,get oncancelled(){return n??null},set oncancelled(c){n=c??void 0}};const s={get root(){return s},resolving:!1,settled:!1};Object.defineProperties(this,{[p]:{configurable:!1,enumerable:!1,writable:!0,value:null},[G]:{configurable:!1,enumerable:!1,writable:!1,value:ae(i,s)}});const a=ue(i,s);try{e(le(i,s),a)}catch(c){s.resolving?console.log("Unhandled exception in CancellablePromise executor.",c):a(c)}}cancel(e){return new l(n=>{Promise.all([this[G](new W("Promise cancelled.",{cause:e})),_e(this)]).then(()=>n(),()=>n())})}cancelOn(e){return e.aborted?this.cancel(e.reason):e.addEventListener("abort",()=>void this.cancel(e.reason),{capture:!0}),this}then(e,n,r){if(!(this instanceof l))throw new TypeError("CancellablePromise.prototype.then called on an invalid object.");if(m(e)||(e=Q),m(n)||(n=Z),e===Q&&n==Z)return new l(i=>i(this));const o={};return this[p]=o,new l((i,s)=>{super.then(a=>{var c;this[p]===o&&(this[p]=null),(c=o.resolve)===null||c===void 0||c.call(o);try{i(e(a))}catch(f){s(f)}},a=>{var c;this[p]===o&&(this[p]=null),(c=o.resolve)===null||c===void 0||c.call(o);try{i(n(a))}catch(f){s(f)}})},async i=>{try{return r==null?void 0:r(i)}finally{await this.cancel(i)}})}catch(e,n){return this.then(void 0,e,n)}finally(e,n){if(!(this instanceof l))throw new TypeError("CancellablePromise.prototype.finally called on an invalid object.");return m(e)?this.then(r=>l.resolve(e()).then(()=>r),r=>l.resolve(e()).then(()=>{throw r}),n):this.then(e,e,n)}static get[K](){return Promise}static all(e){let n=Array.from(e);const r=n.length===0?l.resolve(n):new l((o,i)=>{Promise.all(n).then(o,i)},o=>L(r,n,o));return r}static allSettled(e){let n=Array.from(e);const r=n.length===0?l.resolve(n):new l((o,i)=>{Promise.allSettled(n).then(o,i)},o=>L(r,n,o));return r}static any(e){let n=Array.from(e);const r=n.length===0?l.resolve(n):new l((o,i)=>{Promise.any(n).then(o,i)},o=>L(r,n,o));return r}static race(e){let n=Array.from(e);const r=new l((o,i)=>{Promise.race(n).then(o,i)},o=>L(r,n,o));return r}static cancel(e){const n=new l(()=>{});return n.cancel(e),n}static timeout(e,n){const r=new l(()=>{});return AbortSignal&&typeof AbortSignal=="function"&&AbortSignal.timeout&&typeof AbortSignal.timeout=="function"?AbortSignal.timeout(e).addEventListener("abort",()=>void r.cancel(n)):setTimeout(()=>void r.cancel(n),e),r}static sleep(e,n){return new l(r=>{setTimeout(()=>r(n),e)})}static reject(e){return new l((n,r)=>r(e))}static resolve(e){return e instanceof l?e:new l(n=>n(e))}static withResolvers(){let e={oncancelled:null};return e.promise=new l((n,r)=>{e.resolve=n,e.reject=r},n=>{var r;(r=e.oncancelled)===null||r===void 0||r.call(e,n)}),e}}function ae(t,e){let n;return r=>{if(e.settled||(e.settled=!0,e.reason=r,t.reject(r),Promise.prototype.then.call(t.promise,void 0,o=>{if(o!==r)throw o})),!(!e.reason||!t.oncancelled))return n=new Promise(o=>{try{o(t.oncancelled(e.reason.cause))}catch(i){Promise.reject(new M(t.promise,i,"Unhandled exception in oncancelled callback."))}}).catch(o=>{Promise.reject(new M(t.promise,o,"Unhandled rejection in oncancelled callback."))}),t.oncancelled=null,n}}function le(t,e){return n=>{if(!e.resolving){if(e.resolving=!0,n===t.promise){if(e.settled)return;e.settled=!0,t.reject(new TypeError("A promise cannot be resolved with itself."));return}if(n!=null&&(typeof n=="object"||typeof n=="function")){let r;try{r=n.then}catch(o){e.settled=!0,t.reject(o);return}if(m(r)){try{let s=n.cancel;if(m(s)){const a=c=>{Reflect.apply(s,n,[c])};e.reason?ae(Object.assign(Object.assign({},t),{oncancelled:a}),e)(e.reason):t.oncancelled=a}}catch{}const o={root:e.root,resolving:!1,get settled(){return this.root.settled},set settled(s){this.root.settled=s},get reason(){return this.root.reason}},i=ue(t,o);try{Reflect.apply(r,n,[le(t,o),i])}catch(s){i(s)}return}}e.settled||(e.settled=!0,t.resolve(n))}}}function ue(t,e){return n=>{if(!e.resolving)if(e.resolving=!0,e.settled){try{if(n instanceof W&&e.reason instanceof W&&Object.is(n.cause,e.reason.cause))return}catch{}Promise.reject(new M(t.promise,n))}else e.settled=!0,t.reject(n)}}function L(t,e,n){const r=[];for(const o of e){let i;try{if(!m(o.then)||(i=o.cancel,!m(i)))continue}catch{continue}let s;try{s=Reflect.apply(i,o,[n])}catch(a){Promise.reject(new M(t,a,"Unhandled exception in cancel method."));continue}s&&r.push((s instanceof Promise?s:Promise.resolve(s)).catch(a=>{Promise.reject(new M(t,a,"Unhandled rejection in cancel method."))}))}return Promise.all(r)}function Q(t){return t}function Z(t){throw t}function Fe(t){try{if(t instanceof Error||typeof t!="object"||t.toString!==Object.prototype.toString)return""+t}catch{}try{return JSON.stringify(t)}catch{}try{return Object.prototype.toString.call(t)}catch{}return""}function _e(t){var e;let n=(e=t[p])!==null&&e!==void 0?e:{};return"promise"in n||Object.assign(n,g()),t[p]==null&&(n.resolve(),t[p]=n),n.promise}let g=Promise.withResolvers;g&&typeof g=="function"?g=g.bind(Promise):g=function(){let t,e;return{promise:new Promise((r,o)=>{t=r,e=o}),resolve:t,reject:e}};window._wails=window._wails||{};window._wails.callResultHandler=Xe;window._wails.callErrorHandler=Je;const Be=C(x.Call),We=C(x.CancelCall),v=new Map,Ye=0,$e=0;class Ve extends Error{constructor(e,n){super(e,n),this.name="RuntimeError"}}function Xe(t,e,n){const r=de(t);if(r)if(!e)r.resolve(void 0);else if(!n)r.resolve(e);else try{r.resolve(JSON.parse(e))}catch(o){r.reject(new TypeError("could not parse result: "+o.message,{cause:o}))}}function Je(t,e,n){const r=de(t);if(r)if(!n)r.reject(new Error(e));else{let o;try{o=JSON.parse(e)}catch(a){r.reject(new TypeError("could not parse error: "+a.message,{cause:a}));return}let i={};o.cause&&(i.cause=o.cause);let s;switch(o.kind){case"ReferenceError":s=new ReferenceError(o.message,i);break;case"TypeError":s=new TypeError(o.message,i);break;case"RuntimeError":s=new Ve(o.message,i);break;default:s=new Error(o.message,i);break}r.reject(s)}}function de(t){const e=v.get(t);return v.delete(t),e}function Ge(){let t;do t=oe();while(v.has(t));return t}function Ke(t){const e=Ge(),n=l.withResolvers();v.set(e,{resolve:n.resolve,reject:n.reject});const r=Be(Ye,Object.assign({"call-id":e},t));let o=!1;r.then(()=>{o=!0},s=>{v.delete(e),n.reject(s)});const i=()=>(v.delete(e),We($e,{"call-id":e}).catch(s=>{console.error("Error while requesting binding call cancellation:",s)}));return n.oncancelled=()=>o?i():r.then(i),n.promise}function k(t,...e){return Ke({methodID:t,args:e})}const w=new Map;class Qe{constructor(e,n,r){this.eventName=e,this.callback=n,this.maxCallbacks=r||-1}dispatch(e){try{this.callback(e)}catch(n){console.error(n)}return this.maxCallbacks===-1?!1:(this.maxCallbacks-=1,this.maxCallbacks===0)}}function Ze(t){let e=w.get(t.eventName);e&&(e=e.filter(n=>n!==t),e.length===0?w.delete(t.eventName):w.set(t.eventName,e))}window._wails=window._wails||{};window._wails.dispatchWailsEvent=tt;C(x.Events);class et{constructor(e,n=null){this.name=e,this.data=n}}function tt(t){let e=w.get(t.name);if(!e)return;let n=new et(t.name,t.data);"sender"in t&&(n.sender=t.sender),e=e.filter(r=>!r.dispatch(n)),e.length===0?w.delete(t.name):w.set(t.name,e)}function nt(t,e,n){let r=w.get(t)||[];const o=new Qe(t,e,n);return r.push(o),w.set(t,r),()=>Ze(o)}function rt(t,e){return nt(t,e,-1)}window._wails=window._wails||{};window._wails.invoke=T;T("wails:runtime:ready");function X(){return k(2216952893)}function ot(t){return k(2917562919,t)}function it(){return k(3933442950)}function st(t){return k(3968228732,t)}function ct(t){return k(1886542847,t)}const d=document.querySelector("#response");var ee;(ee=document.querySelector("#request"))==null||ee.addEventListener("click",async()=>{try{await it()?(d&&(d.innerHTML="

Notifications are now authorized.

"),console.info("Notifications are now authorized.")):(d&&(d.innerHTML="

Notifications are not authorized. You can attempt to request again or let the user know in the UI.

"),console.warn(`Notifications are not authorized. + You can attempt to request again or let the user know in the UI. +`))}catch(t){console.error(t)}});var te;(te=document.querySelector("#check"))==null||te.addEventListener("click",async()=>{try{await X()?(d&&(d.innerHTML="

Notifications are authorized.

"),console.info("Notifications are authorized.")):(d&&(d.innerHTML="

Notifications are not authorized. You can attempt to request again or let the user know in the UI.

"),console.warn(`Notifications are not authorized. + You can attempt to request again or let the user know in the UI. +`))}catch(t){console.error(t)}});var ne;(ne=document.querySelector("#basic"))==null||ne.addEventListener("click",async()=>{try{await X()?await st({id:crypto.randomUUID(),title:"Notification Title",subtitle:"Subtitle on macOS and Linux",body:"Body text of notification.",data:{"user-id":"user-123","message-id":"msg-123",timestamp:Date.now()}}):(d&&(d.innerHTML="

Notifications are not authorized. You can attempt to request again or let the user know in the UI.

"),console.warn(`Notifications are not authorized. + You can attempt to request again or let the user know in the UI. +`))}catch(t){console.error(t)}});var re;(re=document.querySelector("#complex"))==null||re.addEventListener("click",async()=>{try{if(await X()){const e="frontend-notification-id";await ot({id:e,actions:[{id:"VIEW",title:"View"},{id:"MARK_READ",title:"Mark as read"},{id:"DELETE",title:"Delete",destructive:!0}],hasReplyField:!0,replyPlaceholder:"Message...",replyButtonTitle:"Reply"}),await ct({id:crypto.randomUUID(),title:"Notification Title",subtitle:"Subtitle on macOS and Linux",body:"Body text of notification.",categoryId:e,data:{"user-id":"user-123","message-id":"msg-123",timestamp:Date.now()}})}else d&&(d.innerHTML="

Notifications are not authorized. You can attempt to request again or let the user know in the UI.

"),console.warn(`Notifications are not authorized. + You can attempt to request again or let the user know in the UI. +`)}catch(t){console.error(t)}});const at=rt("notification:action",t=>{console.info(`Recieved a ${t.name} event`);const{userInfo:e,...n}=t.data[0];console.info("Notification Response:"),console.table(n),console.info("Notification Response Metadata:"),console.table(e);const r=` +
Notification Response
+ + + ${Object.keys(n).map(i=>``).join("")} + + + ${Object.values(n).map(i=>``).join("")} + +
${i}
${i}
+
Notification Metadata
+ + + ${Object.keys(e).map(i=>``).join("")} + + + ${Object.values(e).map(i=>``).join("")} + +
${i}
${i}
+ `,o=document.querySelector("#response");o&&(o.innerHTML=r)});window.onbeforeunload=()=>at(); diff --git a/v3/examples/notifications/frontend/dist/index.html b/v3/examples/notifications/frontend/dist/index.html new file mode 100644 index 000000000..b7794f4d8 --- /dev/null +++ b/v3/examples/notifications/frontend/dist/index.html @@ -0,0 +1,32 @@ + + + + + + + + Wails App + + + +
+ +

Wails + Typescript + Desktop Notifications

+

Send notifications πŸ‘‡

+
+ + + + +
+ +
+ + diff --git a/v3/examples/notifications/frontend/dist/style.css b/v3/examples/notifications/frontend/dist/style.css new file mode 100644 index 000000000..074717bca --- /dev/null +++ b/v3/examples/notifications/frontend/dist/style.css @@ -0,0 +1,131 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +* { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +.controls { + display: flex; + gap: 1em; +} + +button { + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3em; +} + +h1, h3 { + line-height: 1.1; + text-align: center; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +.footer table { + font-size: 12px; + border-collapse: collapse; + margin: 0 auto; +} + +.footer table, th, td { + border: 1px solid #ddd; + padding: 0.5em; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} \ No newline at end of file diff --git a/v3/examples/notifications/frontend/dist/typescript.svg b/v3/examples/notifications/frontend/dist/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/v3/examples/notifications/frontend/dist/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/notifications/frontend/dist/wails.png b/v3/examples/notifications/frontend/dist/wails.png new file mode 100644 index 000000000..8bdf42483 Binary files /dev/null and b/v3/examples/notifications/frontend/dist/wails.png differ diff --git a/v3/examples/notifications/frontend/index.html b/v3/examples/notifications/frontend/index.html new file mode 100644 index 000000000..b873cd4f3 --- /dev/null +++ b/v3/examples/notifications/frontend/index.html @@ -0,0 +1,32 @@ + + + + + + + + Wails App + + +
+ +

Wails + Typescript + Desktop Notifications

+

Send notifications πŸ‘‡

+
+ + + + +
+ +
+ + + diff --git a/v3/examples/notifications/frontend/package.json b/v3/examples/notifications/frontend/package.json new file mode 100644 index 000000000..b39da7ece --- /dev/null +++ b/v3/examples/notifications/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build:dev": "tsc && vite build --minify false --mode development", + "build": "tsc && vite build --mode production", + "preview": "vite preview" + }, + "dependencies": { + "@wailsio/runtime": "latest" + }, + "devDependencies": { + "typescript": "^4.9.3", + "vite": "^5.0.0" + } +} diff --git a/v3/examples/notifications/frontend/public/Inter-Medium.ttf b/v3/examples/notifications/frontend/public/Inter-Medium.ttf new file mode 100644 index 000000000..a01f3777a Binary files /dev/null and b/v3/examples/notifications/frontend/public/Inter-Medium.ttf differ diff --git a/v3/examples/notifications/frontend/public/style.css b/v3/examples/notifications/frontend/public/style.css new file mode 100644 index 000000000..074717bca --- /dev/null +++ b/v3/examples/notifications/frontend/public/style.css @@ -0,0 +1,131 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +* { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +.controls { + display: flex; + gap: 1em; +} + +button { + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3em; +} + +h1, h3 { + line-height: 1.1; + text-align: center; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +.footer table { + font-size: 12px; + border-collapse: collapse; + margin: 0 auto; +} + +.footer table, th, td { + border: 1px solid #ddd; + padding: 0.5em; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} \ No newline at end of file diff --git a/v3/examples/notifications/frontend/public/typescript.svg b/v3/examples/notifications/frontend/public/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/v3/examples/notifications/frontend/public/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/notifications/frontend/public/wails.png b/v3/examples/notifications/frontend/public/wails.png new file mode 100644 index 000000000..8bdf42483 Binary files /dev/null and b/v3/examples/notifications/frontend/public/wails.png differ diff --git a/v3/examples/notifications/frontend/src/main.ts b/v3/examples/notifications/frontend/src/main.ts new file mode 100644 index 000000000..94bbb7df3 --- /dev/null +++ b/v3/examples/notifications/frontend/src/main.ts @@ -0,0 +1,129 @@ +import { Events } from "@wailsio/runtime"; +import { NotificationService } from "../bindings/github.com/wailsapp/wails/v3/pkg/services/notifications"; + +const footer = document.querySelector("#response"); + +document.querySelector("#request")?.addEventListener("click", async () => { + try { + const authorized = await NotificationService.RequestNotificationAuthorization(); + if (authorized) { + if (footer) footer.innerHTML = "

Notifications are now authorized.

"; + console.info("Notifications are now authorized."); + } else { + if (footer) footer.innerHTML = "

Notifications are not authorized. You can attempt to request again or let the user know in the UI.

"; + console.warn("Notifications are not authorized.\n You can attempt to request again or let the user know in the UI.\n"); + } + } catch (error) { + console.error(error); + } +}); + +document.querySelector("#check")?.addEventListener("click", async () => { + try { + const authorized = await NotificationService.CheckNotificationAuthorization(); + if (authorized) { + if (footer) footer.innerHTML = "

Notifications are authorized.

"; + console.info("Notifications are authorized."); + } else { + if (footer) footer.innerHTML = "

Notifications are not authorized. You can attempt to request again or let the user know in the UI.

"; + console.warn("Notifications are not authorized.\n You can attempt to request again or let the user know in the UI.\n"); + } + } catch (error) { + console.error(error); + } +}); + +document.querySelector("#basic")?.addEventListener("click", async () => { + try { + const authorized = await NotificationService.CheckNotificationAuthorization(); + if (authorized) { + await NotificationService.SendNotification({ + id: crypto.randomUUID(), + title: "Notification Title", + subtitle: "Subtitle on macOS and Linux", + body: "Body text of notification.", + data: { + "user-id": "user-123", + "message-id": "msg-123", + "timestamp": Date.now(), + }, + }); + } else { + if (footer) footer.innerHTML = "

Notifications are not authorized. You can attempt to request again or let the user know in the UI.

"; + console.warn("Notifications are not authorized.\n You can attempt to request again or let the user know in the UI.\n"); + } + } catch (error) { + console.error(error); + } +}); +document.querySelector("#complex")?.addEventListener("click", async () => { + try { + const authorized = await NotificationService.CheckNotificationAuthorization(); + if (authorized) { + const CategoryID = "frontend-notification-id"; + + await NotificationService.RegisterNotificationCategory({ + id: CategoryID, + actions: [ + { id: "VIEW", title: "View" }, + { id: "MARK_READ", title: "Mark as read" }, + { id: "DELETE", title: "Delete", destructive: true }, + ], + hasReplyField: true, + replyPlaceholder: "Message...", + replyButtonTitle: "Reply", + }); + + await NotificationService.SendNotificationWithActions({ + id: crypto.randomUUID(), + title: "Notification Title", + subtitle: "Subtitle on macOS and Linux", + body: "Body text of notification.", + categoryId: CategoryID, + data: { + "user-id": "user-123", + "message-id": "msg-123", + "timestamp": Date.now(), + }, + }); + } else { + if (footer) footer.innerHTML = "

Notifications are not authorized. You can attempt to request again or let the user know in the UI.

"; + console.warn("Notifications are not authorized.\n You can attempt to request again or let the user know in the UI.\n"); + } + } catch (error) { + console.error(error); + } +}); + +const unlisten = Events.On("notification:action", (response) => { + console.info(`Recieved a ${response.name} event`); + const { userInfo, ...base } = response.data[0]; + console.info("Notification Response:"); + console.table(base); + console.info("Notification Response Metadata:"); + console.table(userInfo); + const table = ` +
Notification Response
+ + + ${Object.keys(base).map(key => ``).join("")} + + + ${Object.values(base).map(value => ``).join("")} + +
${key}
${value}
+
Notification Metadata
+ + + ${Object.keys(userInfo).map(key => ``).join("")} + + + ${Object.values(userInfo).map(value => ``).join("")} + +
${key}
${value}
+ `; + const footer = document.querySelector("#response"); + if (footer) footer.innerHTML = table; +}); + +window.onbeforeunload = () => unlisten(); \ No newline at end of file diff --git a/v3/examples/notifications/frontend/src/vite-env.d.ts b/v3/examples/notifications/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/v3/examples/notifications/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/v3/examples/notifications/frontend/tsconfig.json b/v3/examples/notifications/frontend/tsconfig.json new file mode 100644 index 000000000..c267ecf24 --- /dev/null +++ b/v3/examples/notifications/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/v3/examples/notifications/go.mod b/v3/examples/notifications/go.mod new file mode 100644 index 000000000..ebd5f4fc0 --- /dev/null +++ b/v3/examples/notifications/go.mod @@ -0,0 +1,53 @@ +module notifications + +go 1.24.0 + +toolchain go1.24.1 + +require github.com/wailsapp/wails/v3 v3.0.0-dev + +require ( + dario.cat/mergo v1.0.1 // indirect + git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/cloudflare/circl v1.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.13.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/lmittmann/tint v1.0.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/wailsapp/go-webview2 v1.0.21 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) + +replace github.com/wailsapp/wails/v3 => ../.. diff --git a/v3/examples/notifications/go.sum b/v3/examples/notifications/go.sum new file mode 100644 index 000000000..6f8b2a29a --- /dev/null +++ b/v3/examples/notifications/go.sum @@ -0,0 +1,148 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= +github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= +github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= +github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v3/examples/notifications/main.go b/v3/examples/notifications/main.go new file mode 100644 index 000000000..c0e006652 --- /dev/null +++ b/v3/examples/notifications/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "embed" + _ "embed" + "fmt" + "log" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/services/notifications" +) + +// Wails uses Go's `embed` package to embed the frontend files into the binary. +// Any files in the frontend/dist folder will be embedded into the binary and +// made available to the frontend. +// See https://pkg.go.dev/embed for more information. + +//go:embed all:frontend/dist +var assets embed.FS + +// main function serves as the application's entry point. It initializes the application, creates a window, +// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and +// logs any error that might occur. +func main() { + // Create a new Notification Service + ns := notifications.New() + + // Create a new Wails application by providing the necessary options. + // Variables 'Name' and 'Description' are for application metadata. + // 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files. + // 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances. + // 'Mac' options tailor the application when running an macOS. + app := application.New(application.Options{ + Name: "Notifications Demo", + Description: "A demo of using desktop notifications with Wails", + Services: []application.Service{ + application.NewService(ns), + }, + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + // Create a new window with the necessary options. + // 'Title' is the title of the window. + // 'Mac' options tailor the window when running on macOS. + // 'BackgroundColour' is the background colour of the window. + // 'URL' is the URL that will be loaded into the webview. + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Window 1", + Name: "main", + Mac: application.MacWindow{ + InvisibleTitleBarHeight: 50, + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInset, + }, + BackgroundColour: application.NewRGB(27, 38, 54), + URL: "/", + }) + + // Pass a notification callback that will be called when a notification is actioned. + ns.OnNotificationResponse(func(result notifications.NotificationResult) { + if result.Error != nil { + println(fmt.Errorf("parsing notification result failed: %s", result.Error)) + } else { + fmt.Printf("Response: %+v\n", result.Response) + println("Sending response to frontend...") + app.Event.Emit("notification:action", result.Response) + } + }) + + // Run the application. This blocks until the application has been exited. + err := app.Run() + + // If an error occurred while running the application, log it and exit. + if err != nil { + log.Fatal(err) + } +} diff --git a/v3/examples/panic-handling/main.go b/v3/examples/panic-handling/main.go index 1b702d64f..cb19165be 100644 --- a/v3/examples/panic-handling/main.go +++ b/v3/examples/panic-handling/main.go @@ -48,7 +48,7 @@ func main() { }, }) - app.NewWebviewWindow(). + app.Window.New(). SetTitle("WebviewWindow 1"). Show() diff --git a/v3/examples/plain/main.go b/v3/examples/plain/main.go index 172609f80..3132d65b8 100644 --- a/v3/examples/plain/main.go +++ b/v3/examples/plain/main.go @@ -23,8 +23,9 @@ func main() { }), }, }) - // Create window - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + // Create window - Note: In future versions, window creation may return errors + // that should be checked. For now, window creation is deferred until app.Run() + app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Plain Bundle", CSS: `body { background-color: rgb(255, 255, 255); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; user-select: none; -ms-user-select: none; -webkit-user-select: none; } .main { color: white; margin: 20%; }`, Mac: application.MacWindow{ @@ -35,30 +36,44 @@ func main() { URL: "/", }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + // Create second window with direct HTML content + app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "HTML TEST", HTML: "

AWESOME!

", CSS: `body { background-color: rgb(255, 0, 0); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; user-select: none; -ms-user-select: none; -webkit-user-select: none; } .main { color: white; margin: 20%; }`, JS: `window.iamhere = function() { console.log("Hello World!"); }`, }) - app.OnEvent("clicked", func(_ *application.CustomEvent) { + // Store the cleanup function to remove event listener when needed + removeClickHandler := app.Event.On("clicked", func(_ *application.CustomEvent) { println("clicked") }) + // Note: In a real application, you would call removeClickHandler() when appropriate + _ = removeClickHandler // Acknowledge we're storing the cleanup function + // Use context-aware goroutine for graceful shutdown go func() { - time.Sleep(5 * time.Second) - - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ - Title: "Plain Bundle new Window from GoRoutine", - Width: 500, - Height: 500, - Mac: application.MacWindow{ - Backdrop: application.MacBackdropTranslucent, - TitleBar: application.MacTitleBarHiddenInsetUnified, - InvisibleTitleBarHeight: 50, - }, - }) + // Use a ticker instead of sleep to allow for cancellation + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + select { + case <-ticker.C: + // Create window after delay - in production, you should handle potential errors + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Plain Bundle new Window from GoRoutine", + Width: 500, + Height: 500, + Mac: application.MacWindow{ + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInsetUnified, + InvisibleTitleBarHeight: 50, + }, + }) + case <-app.Context().Done(): + // Application is shutting down, cancel the goroutine + return + } }() err := app.Run() diff --git a/v3/examples/raw-message/main.go b/v3/examples/raw-message/main.go index 7e67fe8ad..cd879ea5c 100644 --- a/v3/examples/raw-message/main.go +++ b/v3/examples/raw-message/main.go @@ -26,7 +26,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Window 1", Name: "Window 1", Mac: application.MacWindow{ diff --git a/v3/examples/screen/main.go b/v3/examples/screen/main.go index 9207280d9..75d0c8bd2 100644 --- a/v3/examples/screen/main.go +++ b/v3/examples/screen/main.go @@ -59,7 +59,7 @@ func main() { }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Screen Demo", Width: 800, Height: 600, diff --git a/v3/examples/screen/screens.go b/v3/examples/screen/screens.go index 0f1f599f6..a19afb14b 100644 --- a/v3/examples/screen/screens.go +++ b/v3/examples/screen/screens.go @@ -13,7 +13,7 @@ type ScreenService struct { func (s *ScreenService) GetSystemScreens() []*application.Screen { s.isExampleLayout = false - screens, _ := application.Get().GetScreens() + screens := application.Get().Screen.GetAll() return screens } @@ -29,7 +29,13 @@ func (s *ScreenService) ProcessExampleScreens(rawScreens []interface{}) []*appli } } - screens := []*application.Screen{} + // Prevent unbounded slice growth by limiting the number of screens + maxScreens := 32 // Reasonable limit for screen configurations + if len(rawScreens) > maxScreens { + rawScreens = rawScreens[:maxScreens] + } + + screens := make([]*application.Screen, 0, len(rawScreens)) for _, s := range rawScreens { s := s.(map[string]interface{}) @@ -52,7 +58,7 @@ func (s *ScreenService) ProcessExampleScreens(rawScreens []interface{}) []*appli } s.screenManager.LayoutScreens(screens) - return s.screenManager.Screens() + return s.screenManager.GetAll() } func (s *ScreenService) transformPoint(point application.Point, toDIP bool) application.Point { @@ -87,8 +93,10 @@ func (s *ScreenService) TransformPoint(point map[string]interface{}, toDIP bool) ptTransformed := s.transformPoint(pt, toDIP) ptDblTransformed := s.transformPoint(ptTransformed, !toDIP) - // double-transform multiple times to catch any double-rounding issues - for i := 0; i < 10; i++ { + // double-transform a limited number of times to catch any double-rounding issues + // Limit iterations to prevent potential performance issues + maxIterations := 3 // Reduced from 10 to limit computational overhead + for i := 0; i < maxIterations; i++ { ptTransformed = s.transformPoint(ptDblTransformed, toDIP) ptDblTransformed = s.transformPoint(ptTransformed, !toDIP) } diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/examples/services/hashes/service.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/examples/services/hashes/service.js index ed4a57d7d..f5c01b306 100644 --- a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/examples/services/hashes/service.js +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/examples/services/hashes/service.js @@ -4,7 +4,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "/wails/runtime.js"; +import {Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create} from "/wails/runtime.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports @@ -12,9 +12,8 @@ import * as $models from "./models.js"; /** * @param {string} s - * @returns {Promise<$models.Hashes> & { cancel(): void }} + * @returns {$CancellablePromise<$models.Hashes>} */ export function Generate(s) { - let $resultPromise = /** @type {any} */($Call.ByID(1123907498, s)); - return $resultPromise; + return $Call.ByID(1123907498, s); } diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/index.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/index.js index c4e79bfd6..d2bf43c94 100644 --- a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/index.js +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/index.js @@ -2,7 +2,7 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL // This file is automatically generated. DO NOT EDIT -import * as KeyValueStore from "./keyvaluestore.js"; +import * as Service from "./service.js"; export { - KeyValueStore + Service }; diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/keyvaluestore.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/keyvaluestore.js index 26d36cbb6..e69de29bb 100644 --- a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/keyvaluestore.js +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/keyvaluestore.js @@ -1,47 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL -// This file is automatically generated. DO NOT EDIT - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "/wails/runtime.js"; - -/** - * Delete deletes the key from the store. If AutoSave is true, the store is saved to disk. - * @param {string} key - * @returns {Promise & { cancel(): void }} - */ -export function Delete(key) { - let $resultPromise = /** @type {any} */($Call.ByID(1029952841, key)); - return $resultPromise; -} - -/** - * Get returns the value for the given key. If key is empty, the entire store is returned. - * @param {string} key - * @returns {Promise & { cancel(): void }} - */ -export function Get(key) { - let $resultPromise = /** @type {any} */($Call.ByID(3017738442, key)); - return $resultPromise; -} - -/** - * Save saves the store to disk - * @returns {Promise & { cancel(): void }} - */ -export function Save() { - let $resultPromise = /** @type {any} */($Call.ByID(840897339)); - return $resultPromise; -} - -/** - * Set sets the value for the given key. If AutoSave is true, the store is saved to disk. - * @param {string} key - * @param {any} value - * @returns {Promise & { cancel(): void }} - */ -export function Set(key, value) { - let $resultPromise = /** @type {any} */($Call.ByID(2329265830, key, value)); - return $resultPromise; -} diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/service.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/service.js new file mode 100644 index 000000000..b4285097c --- /dev/null +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/kvstore/service.js @@ -0,0 +1,69 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Call as $Call, Create as $Create} from "/wails/runtime.js"; + +/** + * Clear deletes all keys from the store. If AutoSave is true, the store is saved to disk. + * @returns {Promise & { cancel(): void }} + */ +export function Clear() { + let $resultPromise = /** @type {any} */($Call.ByID(816318109)); + return $resultPromise; +} + +/** + * Delete deletes the given key from the store. If AutoSave is true, the store is saved to disk. + * @param {string} key + * @returns {Promise & { cancel(): void }} + */ +export function Delete(key) { + let $resultPromise = /** @type {any} */($Call.ByID(2889946731, key)); + return $resultPromise; +} + +/** + * Get returns the value for the given key. If key is empty, the entire store is returned. + * @param {string} key + * @returns {Promise & { cancel(): void }} + */ +export function Get(key) { + let $resultPromise = /** @type {any} */($Call.ByID(376909388, key)); + return $resultPromise; +} + +/** + * Load loads the store from disk. + * If the store is in-memory, i.e. not associated with a file, Load has no effect. + * If the operation fails, a non-nil error is returned + * and the store's content and state at call time are preserved. + * @returns {Promise & { cancel(): void }} + */ +export function Load() { + let $resultPromise = /** @type {any} */($Call.ByID(1850778156)); + return $resultPromise; +} + +/** + * Save saves the store to disk. + * If the store is in-memory, i.e. not associated with a file, Save has no effect. + * @returns {Promise & { cancel(): void }} + */ +export function Save() { + let $resultPromise = /** @type {any} */($Call.ByID(3572737965)); + return $resultPromise; +} + +/** + * Set sets the value for the given key. If AutoSave is true, the store is saved to disk. + * @param {string} key + * @param {any} value + * @returns {Promise & { cancel(): void }} + */ +export function Set(key, value) { + let $resultPromise = /** @type {any} */($Call.ByID(2491766752, key, value)); + return $resultPromise; +} diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/index.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/index.js index d3c050f93..564a31eeb 100644 --- a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/index.js +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/index.js @@ -2,7 +2,11 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL // This file is automatically generated. DO NOT EDIT -import * as LoggerService from "./loggerservice.js"; +import * as Service from "./service.js"; export { - LoggerService + Service }; + +export { + Level +} from "./models.js"; diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/loggerservice.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/loggerservice.js deleted file mode 100644 index 0ee366a67..000000000 --- a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/loggerservice.js +++ /dev/null @@ -1,60 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL -// This file is automatically generated. DO NOT EDIT - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "/wails/runtime.js"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as slog$0 from "../../../../../../../log/slog/models.js"; - -/** - * @param {string} message - * @param {any[]} args - * @returns {Promise & { cancel(): void }} - */ -export function Debug(message, ...args) { - let $resultPromise = /** @type {any} */($Call.ByID(1384012895, message, args)); - return $resultPromise; -} - -/** - * @param {string} message - * @param {any[]} args - * @returns {Promise & { cancel(): void }} - */ -export function Error(message, ...args) { - let $resultPromise = /** @type {any} */($Call.ByID(1324251502, message, args)); - return $resultPromise; -} - -/** - * @param {string} message - * @param {any[]} args - * @returns {Promise & { cancel(): void }} - */ -export function Info(message, ...args) { - let $resultPromise = /** @type {any} */($Call.ByID(3712350036, message, args)); - return $resultPromise; -} - -/** - * @param {slog$0.Level} level - * @returns {Promise & { cancel(): void }} - */ -export function SetLogLevel(level) { - let $resultPromise = /** @type {any} */($Call.ByID(2521579448, level)); - return $resultPromise; -} - -/** - * @param {string} message - * @param {any[]} args - * @returns {Promise & { cancel(): void }} - */ -export function Warning(message, ...args) { - let $resultPromise = /** @type {any} */($Call.ByID(2902024404, message, args)); - return $resultPromise; -} diff --git a/v3/examples/services/assets/bindings/log/slog/models.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/models.js similarity index 62% rename from v3/examples/services/assets/bindings/log/slog/models.js rename to v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/models.js index ec976569d..d8579b51a 100644 --- a/v3/examples/services/assets/bindings/log/slog/models.js +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/models.js @@ -9,5 +9,18 @@ import {Create as $Create} from "/wails/runtime.js"; /** * A Level is the importance or severity of a log event. * The higher the level, the more important or severe the event. - * @typedef {any} Level + * + * Values are arbitrary, but there are four predefined ones. + * @typedef {number} Level */ + +/** + * Predefined constants for type Level. + * @namespace + */ +export const Level = { + Debug: -4, + Info: 0, + Warning: 4, + Error: 8, +}; diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/service.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/service.js new file mode 100644 index 000000000..e5128d9bc --- /dev/null +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/log/service.js @@ -0,0 +1,106 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Call as $Call, Create as $Create} from "/wails/runtime.js"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +/** + * DebugContext logs at level [Debug]. + * @param {string} message + * @param {any[]} args + * @returns {Promise & { cancel(): void }} + */ +function DebugContext(message, ...args) { + let $resultPromise = /** @type {any} */($Call.ByID(1481024990, message, args)); + return $resultPromise; +} + +/** + * ErrorContext logs at level [Error]. + * @param {string} message + * @param {any[]} args + * @returns {Promise & { cancel(): void }} + */ +function ErrorContext(message, ...args) { + let $resultPromise = /** @type {any} */($Call.ByID(4028761057, message, args)); + return $resultPromise; +} + +/** + * InfoContext logs at level [Info]. + * @param {string} message + * @param {any[]} args + * @returns {Promise & { cancel(): void }} + */ +function InfoContext(message, ...args) { + let $resultPromise = /** @type {any} */($Call.ByID(1400061359, message, args)); + return $resultPromise; +} + +/** + * Log emits a log record with the current time and the given level and message. + * The Record's attributes consist of the Logger's attributes followed by + * the attributes specified by args. + * + * The attribute arguments are processed as follows: + * - If an argument is a string and this is not the last argument, + * the following argument is treated as the value and the two are combined + * into an attribute. + * - Otherwise, the argument is treated as a value with key "!BADKEY". + * + * Log feeds the binding call context into the configured logger, + * so custom handlers may access context values, e.g. the current window. + * @param {$models.Level} level + * @param {string} message + * @param {any[]} args + * @returns {Promise & { cancel(): void }} + */ +export function Log(level, message, ...args) { + let $resultPromise = /** @type {any} */($Call.ByID(4156699346, level, message, args)); + return $resultPromise; +} + +/** + * LogLevel returns the currently configured log level, + * that is either the one configured initially + * or the last value passed to [Service.SetLogLevel]. + * @returns {Promise<$models.Level> & { cancel(): void }} + */ +export function LogLevel() { + let $resultPromise = /** @type {any} */($Call.ByID(4058368160)); + return $resultPromise; +} + +/** + * SetLogLevel changes the current log level. + * @param {$models.Level} level + * @returns {Promise & { cancel(): void }} + */ +export function SetLogLevel(level) { + let $resultPromise = /** @type {any} */($Call.ByID(3988219088, level)); + return $resultPromise; +} + +/** + * WarningContext logs at level [Warn]. + * @param {string} message + * @param {any[]} args + * @returns {Promise & { cancel(): void }} + */ +function WarningContext(message, ...args) { + let $resultPromise = /** @type {any} */($Call.ByID(52282975, message, args)); + return $resultPromise; +} + +export { + DebugContext as Debug, + InfoContext as Info, + WarningContext as Warning, + ErrorContext as Error, +}; diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/index.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/index.js index d2bf43c94..0918a51f0 100644 --- a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/index.js +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/index.js @@ -6,3 +6,17 @@ import * as Service from "./service.js"; export { Service }; + +import * as $models from "./models.js"; + +/** + * Row holds a single row in the result of a query. + * It is a key-value map where keys are column names. + * @typedef {$models.Row} Row + */ + +/** + * Rows holds the result of a query + * as an array of key-value maps where keys are column names. + * @typedef {$models.Rows} Rows + */ diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/models.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/models.js new file mode 100644 index 000000000..041151c86 --- /dev/null +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/models.js @@ -0,0 +1,25 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Create as $Create} from "/wails/runtime.js"; + +/** + * Row holds a single row in the result of a query. + * It is a key-value map where keys are column names. + * @typedef {{ [_: string]: any }} Row + */ + +/** + * Rows holds the result of a query + * as an array of key-value maps where keys are column names. + * @typedef {Row[]} Rows + */ + +/** + * Stmt wraps a prepared sql statement pointer. + * It provides the same methods as the [sql.Stmt] type. + * @typedef {string} Stmt + */ diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/service.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/service.js index ddbe76eb4..9330c1e4d 100644 --- a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/service.js +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/service.js @@ -6,8 +6,18 @@ // @ts-ignore: Unused imports import {Call as $Call, Create as $Create} from "/wails/runtime.js"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + /** - * @returns {Promise & { cancel(): void }} + * Close closes the current database connection if one is open, otherwise has no effect. + * Additionally, Close closes all open prepared statements associated to the connection. + * + * Even when a non-nil error is returned, + * the database service is left in a consistent state, + * ready for a call to [Service.Open]. + * @returns {Promise & { cancel(): void }} */ export function Close() { let $resultPromise = /** @type {any} */($Call.ByID(1888105376)); @@ -15,31 +25,104 @@ export function Close() { } /** + * ClosePrepared closes a prepared statement + * obtained with [Service.Prepare] or [Service.PrepareContext]. + * ClosePrepared is idempotent: + * it has no effect on prepared statements that are already closed. + * @param {$models.Stmt | null} stmt + * @returns {Promise & { cancel(): void }} + */ +function ClosePrepared(stmt) { + let $resultPromise = /** @type {any} */($Call.ByID(2526200629, stmt)); + return $resultPromise; +} + +/** + * ExecContext executes a query without returning any rows. + * It supports early cancellation. * @param {string} query * @param {any[]} args * @returns {Promise & { cancel(): void }} */ -export function Execute(query, ...args) { - let $resultPromise = /** @type {any} */($Call.ByID(3811930203, query, args)); +function ExecContext(query, ...args) { + let $resultPromise = /** @type {any} */($Call.ByID(674944556, query, args)); return $resultPromise; } /** - * @param {string} dbPath - * @returns {Promise & { cancel(): void }} + * ExecPrepared executes a prepared statement + * obtained with [Service.Prepare] or [Service.PrepareContext] + * without returning any rows. + * It supports early cancellation. + * @param {$models.Stmt | null} stmt + * @param {any[]} args + * @returns {Promise & { cancel(): void }} */ -export function Open(dbPath) { - let $resultPromise = /** @type {any} */($Call.ByID(2012175612, dbPath)); +function ExecPrepared(stmt, ...args) { + let $resultPromise = /** @type {any} */($Call.ByID(2086877656, stmt, args)); return $resultPromise; } /** + * Open validates the current configuration, + * closes the current connection if one is present, + * then opens and validates a new connection. + * + * Even when a non-nil error is returned, + * the database service is left in a consistent state, + * ready for a new call to Open. + * @returns {Promise & { cancel(): void }} + */ +export function Open() { + let $resultPromise = /** @type {any} */($Call.ByID(2012175612)); + return $resultPromise; +} + +/** + * PrepareContext creates a prepared statement for later queries or executions. + * Multiple queries or executions may be run concurrently from the returned statement. + * + * The caller must call the statement's Close method when it is no longer needed. + * Statements are closed automatically + * when the connection they are associated with is closed. + * + * PrepareContext supports early cancellation. + * @param {string} query + * @returns {Promise<$models.Stmt | null> & { cancel(): void }} + */ +function PrepareContext(query) { + let $resultPromise = /** @type {any} */($Call.ByID(570941694, query)); + return $resultPromise; +} + +/** + * QueryContext executes a query and returns a slice of key-value records, + * one per row, with column names as keys. + * It supports early cancellation, returning the slice of results fetched so far. * @param {string} query * @param {any[]} args - * @returns {Promise<{ [_: string]: any }[]> & { cancel(): void }} + * @returns {Promise<$models.Rows> & { cancel(): void }} */ -export function Select(query, ...args) { - let $resultPromise = /** @type {any} */($Call.ByID(2472933124, query, args)); +function QueryContext(query, ...args) { + let $resultPromise = /** @type {any} */($Call.ByID(4115542347, query, args)); + let $typingPromise = /** @type {any} */($resultPromise.then(($result) => { + return $$createType1($result); + })); + $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); + return $typingPromise; +} + +/** + * QueryPrepared executes a prepared statement + * obtained with [Service.Prepare] or [Service.PrepareContext] + * and returns a slice of key-value records, one per row, with column names as keys. + * It supports early cancellation, returning the slice of results fetched so far. + * @param {$models.Stmt | null} stmt + * @param {any[]} args + * @returns {Promise<$models.Rows> & { cancel(): void }} + */ +function QueryPrepared(stmt, ...args) { + let $resultPromise = /** @type {any} */($Call.ByID(3885083725, stmt, args)); let $typingPromise = /** @type {any} */($resultPromise.then(($result) => { return $$createType1($result); })); @@ -50,3 +133,35 @@ export function Select(query, ...args) { // Private type creation functions const $$createType0 = $Create.Map($Create.Any, $Create.Any); const $$createType1 = $Create.Array($$createType0); + +export { + ExecContext as Execute, + QueryContext as Query +}; + +import { Stmt } from "./stmt.js"; + +/** + * Prepare creates a prepared statement for later queries or executions. + * Multiple queries or executions may be run concurrently from the returned statement. + * + * The caller must call the statement's Close method when it is no longer needed. + * Statements are closed automatically + * when the connection they are associated with is closed. + * + * Prepare supports early cancellation. + * + * @param {string} query + * @returns {Promise & { cancel(): void }} + */ +export function Prepare(query) { + const promise = PrepareContext(query); + const wrapper = /** @type {any} */(promise.then(function (id) { + return id == null ? null : new Stmt( + ClosePrepared.bind(null, id), + ExecPrepared.bind(null, id), + QueryPrepared.bind(null, id)); + })); + wrapper.cancel = promise.cancel; + return wrapper; +} diff --git a/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/stmt.js b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/stmt.js new file mode 100644 index 000000000..948b0c3dd --- /dev/null +++ b/v3/examples/services/assets/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/stmt.js @@ -0,0 +1,79 @@ +//@ts-check + +//@ts-ignore: Unused imports +import * as $models from "./models.js"; + +const execSymbol = Symbol("exec"), + querySymbol = Symbol("query"), + closeSymbol = Symbol("close"); + +/** + * Stmt represents a prepared statement for later queries or executions. + * Multiple queries or executions may be run concurrently on the same statement. + * + * The caller must call the statement's Close method when it is no longer needed. + * Statements are closed automatically + * when the connection they are associated with is closed. + */ +export class Stmt { + /** + * Constructs a new prepared statement instance. + * @param {(...args: any[]) => Promise} close + * @param {(...args: any[]) => Promise & { cancel(): void }} exec + * @param {(...args: any[]) => Promise<$models.Rows> & { cancel(): void }} query + */ + constructor(close, exec, query) { + /** + * @member + * @private + * @type {typeof close} + */ + this[closeSymbol] = close; + + /** + * @member + * @private + * @type {typeof exec} + */ + this[execSymbol] = exec; + + /** + * @member + * @private + * @type {typeof query} + */ + this[querySymbol] = query; + } + + /** + * Closes the prepared statement. + * It has no effect when the statement is already closed. + * @returns {Promise} + */ + Close() { + return this[closeSymbol](); + } + + /** + * Executes the prepared statement without returning any rows. + * It supports early cancellation. + * + * @param {any[]} args + * @returns {Promise & { cancel(): void }} + */ + Exec(...args) { + return this[execSymbol](...args); + } + + /** + * Executes the prepared statement + * and returns a slice of key-value records, one per row, with column names as keys. + * It supports early cancellation, returning the array of results fetched so far. + * + * @param {any[]} args + * @returns {Promise<$models.Rows> & { cancel(): void }} + */ + Query(...args) { + return this[querySymbol](...args); + } +} diff --git a/v3/examples/services/assets/bindings/log/slog/index.js b/v3/examples/services/assets/bindings/log/slog/index.js deleted file mode 100644 index 45070abb0..000000000 --- a/v3/examples/services/assets/bindings/log/slog/index.js +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL -// This file is automatically generated. DO NOT EDIT - -import * as $models from "./models.js"; - -/** - * A Level is the importance or severity of a log event. - * The higher the level, the more important or severe the event. - * @typedef {$models.Level} Level - */ diff --git a/v3/examples/services/assets/index.html b/v3/examples/services/assets/index.html index 67746fdec..c44a0679a 100644 --- a/v3/examples/services/assets/index.html +++ b/v3/examples/services/assets/index.html @@ -6,10 +6,10 @@

Exception class that will be thrown in case the bound method returns an error. +The value of the RuntimeError#name property is "RuntimeError".

+

Hierarchy

Constructors

Properties

Constructors

  • Constructs a new RuntimeError instance.

    +

    Parameters

    • Optionalmessage: string

      The error message.

      +
    • Optionaloptions: ErrorOptions

      Options to be forwarded to the Error constructor.

      +

    Returns RuntimeError

Properties

cause?: unknown
message: string
name: string
stack?: string
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/CancelError.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/CancelError.html new file mode 100644 index 000000000..d4cb8d947 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/CancelError.html @@ -0,0 +1,13 @@ +CancelError | @wailsio/runtime

Exception class that will be used as rejection reason +in case a CancellablePromise is cancelled successfully.

+

The value of the name property is the string "CancelError". +The value of the cause property is the cause passed to the cancel method, if any.

+

Hierarchy

Constructors

Properties

Constructors

Properties

cause?: unknown
message: string
name: string
stack?: string
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/CancellablePromise.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/CancellablePromise.html new file mode 100644 index 000000000..3524aae7e --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/CancellablePromise.html @@ -0,0 +1,263 @@ +CancellablePromise | @wailsio/runtime

Class CancellablePromise<T>

A promise with an attached method for cancelling long-running operations (see CancellablePromise#cancel). +Cancellation can optionally be bound to an AbortSignal +for better composability (see CancellablePromise#cancelOn).

+

Cancelling a pending promise will result in an immediate rejection +with an instance of CancelError as reason, +but whoever started the promise will be responsible +for actually aborting the underlying operation. +To this purpose, the constructor and all chaining methods +accept optional cancellation callbacks.

+

If a CancellablePromise still resolves after having been cancelled, +the result will be discarded. If it rejects, the reason +will be reported as an unhandled rejection, +wrapped in a CancelledRejectionError instance. +To facilitate the handling of cancellation requests, +cancelled CancellablePromises will not report unhandled CancelErrors +whose cause field is the same as the one with which the current promise was cancelled.

+

All usual promise methods are defined and return a CancellablePromise +whose cancel method will cancel the parent operation as well, propagating the cancellation reason +upwards through promise chains. +Conversely, cancelling a promise will not automatically cancel dependent promises downstream:

+
let root = new CancellablePromise((resolve, reject) => { ... });
let child1 = root.then(() => { ... });
let child2 = child1.then(() => { ... });
let child3 = root.catch(() => { ... });
child1.cancel(); // Cancels child1 and root, but not child2 or child3 +
+ +

Cancelling a promise that has already settled is safe and has no consequence.

+

The cancel method returns a promise that always fulfills +after the whole chain has processed the cancel request +and all attached callbacks up to that moment have run.

+

All ES2024 promise methods (static and instance) are defined on CancellablePromise, +but actual availability may vary with OS/webview version.

+

In line with the proposal at https://github.com/tc39/proposal-rm-builtin-subclassing, +CancellablePromise does not support transparent subclassing. +Extenders should take care to provide their own method implementations. +This might be reconsidered in case the proposal is retired.

+

CancellablePromise is a wrapper around the DOM Promise object +and is compliant with the Promises/A+ specification +(it passes the compliance suite) +if so is the underlying implementation.

+

Type Parameters

  • T

Hierarchy

Implements

Constructors

  • Creates a new CancellablePromise.

    +

    Type Parameters

    • T

    Parameters

    • executor: CancellablePromiseExecutor<T>

      A callback used to initialize the promise. This callback is passed two arguments: +a resolve callback used to resolve the promise with a value +or the result of another promise (possibly cancellable), +and a reject callback used to reject the promise with a provided reason or error. +If the value provided to the resolve callback is a thenable and cancellable object +(it has a then and a cancel method), +cancellation requests will be forwarded to that object and the oncancelled will not be invoked anymore. +If any one of the two callbacks is called after the promise has been cancelled, +the provided values will be cancelled and resolved as usual, +but their results will be discarded. +However, if the resolution process ultimately ends up in a rejection +that is not due to cancellation, the rejection reason +will be wrapped in a CancelledRejectionError +and bubbled up as an unhandled rejection.

      +
    • Optionaloncancelled: CancellablePromiseCanceller

      It is the caller's responsibility to ensure that any operation +started by the executor is properly halted upon cancellation. +This optional callback can be used to that purpose. +It will be called synchronously with a cancellation cause +when cancellation is requested, after the promise has already rejected +with a CancelError, but before +any then/catch/finally callback runs. +If the callback returns a thenable, the promise returned from cancel +will only fulfill after the former has settled. +Unhandled exceptions or rejections from the callback will be wrapped +in a CancelledRejectionError and bubbled up as unhandled rejections. +If the resolve callback is called before cancellation with a cancellable promise, +cancellation requests on this promise will be diverted to that promise, +and the original oncancelled callback will be discarded.

      +

    Returns CancellablePromise<T>

Properties

"[toStringTag]": string
"[species]": PromiseConstructor

Methods

  • Cancels immediately the execution of the operation associated with this promise. +The promise rejects with a CancelError instance as reason, +with the CancelError#cause property set to the given argument, if any.

    +

    Has no effect if called after the promise has already settled; +repeated calls in particular are safe, but only the first one +will set the cancellation cause.

    +

    The CancelError exception need not be handled explicitly on the promises that are being cancelled: +cancelling a promise with no attached rejection handler does not trigger an unhandled rejection event. +Therefore, the following idioms are all equally correct:

    +
    new CancellablePromise((resolve, reject) => { ... }).cancel();
    new CancellablePromise((resolve, reject) => { ... }).then(...).cancel();
    new CancellablePromise((resolve, reject) => { ... }).then(...).catch(...).cancel(); +
    + +

    Whenever some cancelled promise in a chain rejects with a CancelError +with the same cancellation cause as itself, the error will be discarded silently. +However, the CancelError will still be delivered to all attached rejection handlers +added by then and related methods:

    +
    let cancellable = new CancellablePromise((resolve, reject) => { ... });
    cancellable.then(() => { ... }).catch(console.log);
    cancellable.cancel(); // A CancelError is printed to the console. +
    + +

    If the CancelError is not handled downstream by the time it reaches +a non-cancelled promise, it will trigger an unhandled rejection event, +just like normal rejections would:

    +
    let cancellable = new CancellablePromise((resolve, reject) => { ... });
    let chained = cancellable.then(() => { ... }).then(() => { ... }); // No catch...
    cancellable.cancel(); // Unhandled rejection event on chained! +
    + +

    Therefore, it is important to either cancel whole promise chains from their tail, +as shown in the correct idioms above, or take care of handling errors everywhere.

    +

    Parameters

    • Optionalcause: any

    Returns CancellablePromise<void>

    A cancellable promise that fulfills after the cancel callback (if any) +and all handlers attached up to the call to cancel have run. +If the cancel callback returns a thenable, the promise returned by cancel +will also wait for that thenable to settle. +This enables callers to wait for the cancelled operation to terminate +without being forced to handle potential errors at the call site.

    +
    cancellable.cancel().then(() => {
    // Cleanup finished, it's safe to do something else.
    }, (err) => {
    // Unreachable: the promise returned from cancel will never reject.
    }); +
    + +

    Note that the returned promise will not handle implicitly any rejection +that might have occurred already in the cancelled chain. +It will just track whether registered handlers have been executed or not. +Therefore, unhandled rejections will never be silently handled by calling cancel.

    +
  • Binds promise cancellation to the abort event of the given AbortSignal. +If the signal has already aborted, the promise will be cancelled immediately. +When either condition is verified, the cancellation cause will be set +to the signal's abort reason (see AbortSignal.reason).

    +

    Has no effect if called (or if the signal aborts) after the promise has already settled. +Only the first signal to abort will set the cancellation cause.

    +

    For more details about the cancellation process, +see cancel and the CancellablePromise constructor.

    +

    This method enables awaiting cancellable promises without having +to store them for future cancellation, e.g.:

    +
    await longRunningOperation().cancelOn(signal);
    +
    + +

    instead of:

    +
    let promiseToBeCancelled = longRunningOperation();
    await promiseToBeCancelled; +
    + +

    Parameters

    Returns CancellablePromise<T>

    This promise, for method chaining.

    +
  • Attaches a callback for only the rejection of the Promise.

    +

    The optional oncancelled argument will be invoked when the returned promise is cancelled, +with the same semantics as the oncancelled argument of the constructor. +When the parent promise rejects or is cancelled, the onrejected callback will run, +even after the returned promise has been cancelled: +in that case, should it reject or throw, the reason will be wrapped +in a CancelledRejectionError and bubbled up as an unhandled rejection.

    +

    It is equivalent to

    +
    cancellablePromise.then(undefined, onrejected, oncancelled);
    +
    + +

    and the same caveats apply.

    +

    Type Parameters

    • TResult = never

    Parameters

    Returns CancellablePromise<T | TResult>

    A Promise for the completion of the callback. +Cancellation requests on the returned promise +will propagate up the chain to the parent promise, +but not in the other direction.

    +

    The promise returned from cancel will fulfill only after all attached handlers +up the entire promise chain have been run.

    +

    If onrejected returns a cancellable promise, +cancellation requests will be diverted to it, +and the specified oncancelled callback will be discarded. +See then for more details.

    +
  • Attaches a callback that is invoked when the CancellablePromise is settled (fulfilled or rejected). The +resolved value cannot be accessed or modified from the callback. +The returned promise will settle in the same state as the original one +after the provided callback has completed execution, +unless the callback throws or returns a rejecting promise, +in which case the returned promise will reject as well.

    +

    The optional oncancelled argument will be invoked when the returned promise is cancelled, +with the same semantics as the oncancelled argument of the constructor. +Once the parent promise settles, the onfinally callback will run, +even after the returned promise has been cancelled: +in that case, should it reject or throw, the reason will be wrapped +in a CancelledRejectionError and bubbled up as an unhandled rejection.

    +

    This method is implemented in terms of then and the same caveats apply. +It is polyfilled, hence available in every OS/webview version.

    +

    Parameters

    Returns CancellablePromise<T>

    A Promise for the completion of the callback. +Cancellation requests on the returned promise +will propagate up the chain to the parent promise, +but not in the other direction.

    +

    The promise returned from cancel will fulfill only after all attached handlers +up the entire promise chain have been run.

    +

    If onfinally returns a cancellable promise, +cancellation requests will be diverted to it, +and the specified oncancelled callback will be discarded. +See then for more details.

    +
  • Attaches callbacks for the resolution and/or rejection of the CancellablePromise.

    +

    The optional oncancelled argument will be invoked when the returned promise is cancelled, +with the same semantics as the oncancelled argument of the constructor. +When the parent promise rejects or is cancelled, the onrejected callback will run, +even after the returned promise has been cancelled: +in that case, should it reject or throw, the reason will be wrapped +in a CancelledRejectionError and bubbled up as an unhandled rejection.

    +

    Type Parameters

    • TResult1 = T
    • TResult2 = never

    Parameters

    Returns CancellablePromise<TResult1 | TResult2>

    A CancellablePromise for the completion of whichever callback is executed. +The returned promise is hooked up to propagate cancellation requests up the chain, but not down:

    +
      +
    • if the parent promise is cancelled, the onrejected handler will be invoked with a CancelError +and the returned promise will resolve regularly with its result;
    • +
    • conversely, if the returned promise is cancelled, the parent promise is cancelled too; +the onrejected handler will still be invoked with the parent's CancelError, +but its result will be discarded +and the returned promise will reject with a CancelError as well.
    • +
    +

    The promise returned from cancel will fulfill only after all attached handlers +up the entire promise chain have been run.

    +

    If either callback returns a cancellable promise, +cancellation requests will be diverted to it, +and the specified oncancelled callback will be discarded.

    +

Static Methods

  • Creates a CancellablePromise that is resolved with an array of results +when all of the provided Promises resolve, or rejected when any Promise is rejected.

    +

    Every one of the provided objects that is a thenable and cancellable object +will be cancelled when the returned promise is cancelled, with the same cause.

    +

    Type Parameters

    • T

    Parameters

    Returns CancellablePromise<Awaited<T>[]>

  • Creates a Promise that is resolved with an array of results when all of the provided Promises +resolve, or rejected when any Promise is rejected.

    +

    Type Parameters

    • T extends [] | readonly unknown[]

    Parameters

    • values: T

      An array of Promises.

      +

    Returns CancellablePromise<
    Β Β Β Β { -readonly [P in string
    Β Β Β Β | number
    Β Β Β Β | symbol]: Awaited<T[P<P>]> },
    >

    A new Promise.

    +
  • Creates a CancellablePromise that is resolved with an array of results +when all of the provided Promises resolve or reject.

    +

    Every one of the provided objects that is a thenable and cancellable object +will be cancelled when the returned promise is cancelled, with the same cause.

    +

    Type Parameters

    • T

    Parameters

    Returns CancellablePromise<PromiseSettledResult<Awaited<T>>[]>

  • Creates a Promise that is resolved with an array of results when all +of the provided Promises resolve or reject.

    +

    Type Parameters

    • T extends [] | readonly unknown[]

    Parameters

    • values: T

      An array of Promises.

      +

    Returns CancellablePromise<
    Β Β Β Β {
    Β Β Β Β Β Β Β Β -readonly [P in string
    Β Β Β Β Β Β Β Β | number
    Β Β Β Β Β Β Β Β | symbol]: PromiseSettledResult<Awaited<T[P<P>]>>
    Β Β Β Β },
    >

    A new Promise.

    +
  • The any function returns a promise that is fulfilled by the first given promise to be fulfilled, +or rejected with an AggregateError containing an array of rejection reasons +if all of the given promises are rejected. +It resolves all elements of the passed iterable to promises as it runs this algorithm.

    +

    Every one of the provided objects that is a thenable and cancellable object +will be cancelled when the returned promise is cancelled, with the same cause.

    +

    Type Parameters

    • T

    Parameters

    Returns CancellablePromise<Awaited<T>>

  • The any function returns a promise that is fulfilled by the first given promise to be fulfilled, or rejected with an AggregateError containing an array of rejection reasons if all of the given promises are rejected. It resolves all elements of the passed iterable to promises as it runs this algorithm.

    +

    Type Parameters

    • T extends [] | readonly unknown[]

    Parameters

    • values: T

      An array or iterable of Promises.

      +

    Returns CancellablePromise<Awaited<T[number]>>

    A new Promise.

    +
  • Creates a new CancellablePromise that resolves after the specified timeout. +The returned promise can be cancelled without consequences.

    +

    Parameters

    • milliseconds: number

    Returns CancellablePromise<void>

  • Creates a new CancellablePromise that resolves after +the specified timeout, with the provided value. +The returned promise can be cancelled without consequences.

    +

    Type Parameters

    • T

    Parameters

    • milliseconds: number
    • value: T

    Returns CancellablePromise<T>

  • Creates a new CancellablePromise that cancels +after the specified timeout, with the provided cause.

    +

    If the AbortSignal.timeout factory method is available, +it is used to base the timeout on active time rather than elapsed time. +Otherwise, timeout falls back to setTimeout.

    +

    Type Parameters

    • T = never

    Parameters

    • milliseconds: number
    • Optionalcause: any

    Returns CancellablePromise<T>

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/CancelledRejectionError.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/CancelledRejectionError.html new file mode 100644 index 000000000..998dcabe8 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/CancelledRejectionError.html @@ -0,0 +1,21 @@ +CancelledRejectionError | @wailsio/runtime

Class CancelledRejectionError

Exception class that will be reported as an unhandled rejection +in case a CancellablePromise rejects after being cancelled, +or when the oncancelled callback throws or rejects.

+

The value of the name property is the string "CancelledRejectionError". +The value of the cause property is the reason the promise rejected with.

+

Because the original promise was cancelled, +a wrapper promise will be passed to the unhandled rejection listener instead. +The promise property holds a reference to the original promise.

+

Hierarchy

  • Error
    • CancelledRejectionError

Constructors

Properties

Constructors

  • Constructs a new CancelledRejectionError instance.

    +

    Parameters

    • promise: CancellablePromise<unknown>

      The promise that caused the error originally.

      +
    • Optionalreason: any

      The rejection reason.

      +
    • Optionalinfo: string

      An optional informative message specifying the circumstances in which the error was thrown. +Defaults to the string "Unhandled rejection in cancelled promise.".

      +

    Returns CancelledRejectionError

Properties

cause?: unknown
message: string
name: string
promise: CancellablePromise<unknown>

Holds a reference to the promise that was cancelled and then rejected.

+
stack?: string
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/Events.WailsEvent.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/Events.WailsEvent.html index ce9933534..57ba9c59f 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/Events.WailsEvent.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/Events.WailsEvent.html @@ -1,4 +1,10 @@ -WailsEvent | @wailsio/runtime

Constructors

Properties

Constructors

Properties

data: any
name: any

Generated using TypeDoc

\ No newline at end of file +WailsEvent | @wailsio/runtime

Represents a system event or a custom event emitted through wails-provided facilities.

+

Constructors

Properties

Constructors

Properties

data: any

Optional data associated with the emitted event.

+
name: string

The name of the event.

+
sender?: string

Name of the originating window. Omitted for application events. +Will be overridden if set manually.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/_internal_.Window.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/_internal_.Window.html new file mode 100644 index 000000000..a2794b5e0 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/classes/_internal_.Window.html @@ -0,0 +1,136 @@ +Window | @wailsio/runtime

Methods

  • Gets the specified window.

    +

    Parameters

    • name: string

      The name of the window to get.

      +

    Returns Window

    The corresponding window object.

    +
  • Returns true if the window is focused.

    +

    Returns Promise<boolean>

    Whether the window is currently focused.

    +
  • Returns true if the window is fullscreen.

    +

    Returns Promise<boolean>

    Whether the window is currently fullscreen.

    +
  • Returns true if the window is maximised.

    +

    Returns Promise<boolean>

    Whether the window is currently maximised.

    +
  • Returns true if the window is minimised.

    +

    Returns Promise<boolean>

    Whether the window is currently minimised.

    +
  • Returns true if the window is resizable.

    +

    Returns Promise<boolean>

    Whether the window is currently resizable.

    +
  • Restores the window to its previous state if it was previously minimised, maximised or fullscreen.

    +

    Returns Promise<void>

  • Sets the window to be always on top.

    +

    Parameters

    • alwaysOnTop: boolean

      Whether the window should stay on top.

      +

    Returns Promise<void>

  • Sets the background colour of the window.

    +

    Parameters

    • r: number

      The desired red component of the window background.

      +
    • g: number

      The desired green component of the window background.

      +
    • b: number

      The desired blue component of the window background.

      +
    • a: number

      The desired alpha component of the window background.

      +

    Returns Promise<void>

  • Removes the window frame and title bar.

    +

    Parameters

    • frameless: boolean

      Whether the window should be frameless.

      +

    Returns Promise<void>

  • Disables the system fullscreen button.

    +

    Parameters

    • enabled: boolean

      Whether the fullscreen button should be enabled.

      +

    Returns Promise<void>

  • Sets the maximum size of the window.

    +

    Parameters

    • width: number

      The desired maximum width of the window.

      +
    • height: number

      The desired maximum height of the window.

      +

    Returns Promise<void>

  • Sets the minimum size of the window.

    +

    Parameters

    • width: number

      The desired minimum width of the window.

      +
    • height: number

      The desired minimum height of the window.

      +

    Returns Promise<void>

  • Sets the absolute position of the window.

    +

    Parameters

    • x: number

      The desired horizontal absolute position of the window.

      +
    • y: number

      The desired vertical absolute position of the window.

      +

    Returns Promise<void>

  • Sets the relative position of the window to the screen.

    +

    Parameters

    • x: number

      The desired horizontal relative position of the window.

      +
    • y: number

      The desired vertical relative position of the window.

      +

    Returns Promise<void>

  • Sets whether the window is resizable.

    +

    Parameters

    • resizable: boolean

      Whether the window should be resizable.

      +

    Returns Promise<void>

  • Sets the size of the window.

    +

    Parameters

    • width: number

      The desired width of the window.

      +
    • height: number

      The desired height of the window.

      +

    Returns Promise<void>

  • Sets the title of the window.

    +

    Parameters

    • title: string

      The desired title of the window.

      +

    Returns Promise<void>

  • Sets the zoom level of the window.

    +

    Parameters

    • zoom: number

      The desired zoom level.

      +

    Returns Promise<void>

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Hide.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Hide.html index b33108196..4828abe86 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Hide.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Hide.html @@ -1,2 +1,2 @@ -Hide | @wailsio/runtime
  • Hides a certain method by calling the HideMethod function.

    -

    Returns Promise<void>

Generated using TypeDoc

\ No newline at end of file +Hide | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Quit.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Quit.html index b8c72b5f0..bb3ec1cb1 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Quit.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Quit.html @@ -1,2 +1,2 @@ -Quit | @wailsio/runtime
  • Calls the QuitMethod to terminate the program.

    -

    Returns Promise<void>

Generated using TypeDoc

\ No newline at end of file +Quit | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Show.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Show.html index e97c56612..83e6d02be 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Show.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Application.Show.html @@ -1,2 +1,2 @@ -Show | @wailsio/runtime
  • Calls the ShowMethod and returns the result.

    -

    Returns Promise<void>

Generated using TypeDoc

\ No newline at end of file +Show | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Browser.OpenURL.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Browser.OpenURL.html index d001c834c..c489dc6af 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Browser.OpenURL.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Browser.OpenURL.html @@ -1,3 +1,3 @@ -OpenURL | @wailsio/runtime
  • Open a browser window to the given URL

    -

    Parameters

    • url: string

      The URL to open

      -

    Returns Promise<string>

Generated using TypeDoc

\ No newline at end of file +OpenURL | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.ByID.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.ByID.html index 88efcba00..f155a72c4 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.ByID.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.ByID.html @@ -1,7 +1,6 @@ -ByID | @wailsio/runtime
  • Calls a method by its ID with the specified arguments.

    +ByID | @wailsio/runtime
    • Calls a method by its numeric ID with the specified arguments. +See Call for details.

      Parameters

      • methodID: number

        The ID of the method to call.

        -
      • Rest ...args: any[]

        The arguments to pass to the method.

        -

      Returns any

        -
      • The result of the method call.
      • -
      -

    Generated using TypeDoc

    \ No newline at end of file +
  • ...args: any[]

    The arguments to pass to the method.

    +

Returns CancellablePromise<any>

The result of the method call.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.ByName.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.ByName.html index 57ce6d7fe..231587484 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.ByName.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.ByName.html @@ -1,6 +1,6 @@ -ByName | @wailsio/runtime
  • Executes a method by name.

    -

    Parameters

    • methodName: string

      The name of the method in the format 'package.struct.method'.

      -
    • Rest ...args: any[]

      The arguments to pass to the method.

      -

    Returns any

    The result of the method execution.

    -

    Throws

    If the name is not a string or is not in the correct format.

    -

Generated using TypeDoc

\ No newline at end of file +ByName | @wailsio/runtime
  • Calls a bound method by name with the specified arguments. +See Call for details.

    +

    Parameters

    • methodName: string

      The name of the method in the format 'package.struct.method'.

      +
    • ...args: any[]

      The arguments to pass to the method.

      +

    Returns CancellablePromise<any>

    The result of the method call.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.Call.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.Call.html index 44dbce6c3..48ed3e3a0 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.Call.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.Call.html @@ -1,6 +1,9 @@ -Call | @wailsio/runtime
  • Call method.

    -

    Parameters

    • options: Object

      The options for the method.

      -

    Returns Object

      -
    • The result of the call.
    • -
    -

Generated using TypeDoc

\ No newline at end of file +Call | @wailsio/runtime
  • Call a bound method according to the given call options.

    +

    In case of failure, the returned promise will reject with an exception +among ReferenceError (unknown method), TypeError (wrong argument count or type), +RuntimeError (method returned an error), or other (network or internal errors). +The exception might have a "cause" field with the value returned +by the application- or service-level error marshaling functions.

    +

    Parameters

    Returns CancellablePromise<any>

    The result of the call.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.Plugin.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.Plugin.html deleted file mode 100644 index dff293734..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Call.Plugin.html +++ /dev/null @@ -1,8 +0,0 @@ -Plugin | @wailsio/runtime
  • Calls a method on a plugin.

    -

    Parameters

    • pluginName: string

      The name of the plugin.

      -
    • methodName: string

      The name of the method to call.

      -
    • Rest ...args: any[]

      The arguments to pass to the method.

      -

    Returns any

      -
    • The result of the method call.
    • -
    -

Generated using TypeDoc

\ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Clipboard.SetText.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Clipboard.SetText.html index 7b49863d1..681208368 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Clipboard.SetText.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Clipboard.SetText.html @@ -1,6 +1,4 @@ -SetText | @wailsio/runtime
  • Sets the text to the Clipboard.

    +SetText | @wailsio/runtime
    • Sets the text to the Clipboard.

      Parameters

      • text: string

        The text to be set to the Clipboard.

        -

      Returns Promise<any>

        -
      • A Promise that resolves when the operation is successful.
      • -
      -

    Generated using TypeDoc

    \ No newline at end of file +

Returns Promise<void>

A Promise that resolves when the operation is successful.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Clipboard.Text.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Clipboard.Text.html index 894bbc80f..4e0514fe5 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Clipboard.Text.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Clipboard.Text.html @@ -1,3 +1,3 @@ -Text | @wailsio/runtime
  • Get the Clipboard text

    -

    Returns Promise<string>

    A promise that resolves with the text from the Clipboard.

    -

Generated using TypeDoc

\ No newline at end of file +Text | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Any.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Any.html deleted file mode 100644 index 0217706d6..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Any.html +++ /dev/null @@ -1,2 +0,0 @@ -Any | @wailsio/runtime
  • Any is a dummy creation function for simple or unknown types.

    -

    Type Parameters

    • T

    Parameters

    • source: any

    Returns T

Generated using TypeDoc

\ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Array.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Array.html deleted file mode 100644 index ff34b11f1..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Array.html +++ /dev/null @@ -1,4 +0,0 @@ -Array | @wailsio/runtime
  • Array takes a creation function for an arbitrary type -and returns an in-place creation function for an array -whose elements are of that type.

    -

    Type Parameters

    • T

    Parameters

    • element: ((source) => T)
        • (source): T
        • Parameters

          • source: any

          Returns T

    Returns ((source) => T[])

      • (source): T[]
      • Parameters

        • source: any

        Returns T[]

Generated using TypeDoc

\ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.ByteSlice.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.ByteSlice.html deleted file mode 100644 index cab3c34af..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.ByteSlice.html +++ /dev/null @@ -1,3 +0,0 @@ -ByteSlice | @wailsio/runtime
  • ByteSlice is a creation function that replaces -null strings with empty strings.

    -

    Parameters

    • source: any

    Returns string

Generated using TypeDoc

\ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Map.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Map.html deleted file mode 100644 index f7af45bf3..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Map.html +++ /dev/null @@ -1,4 +0,0 @@ -Map | @wailsio/runtime
  • Map takes creation functions for two arbitrary types -and returns an in-place creation function for an object -whose keys and values are of those types.

    -

    Type Parameters

    • K
    • V

    Parameters

    • key: ((source) => K)
        • (source): K
        • Parameters

          • source: any

          Returns K

    • value: ((source) => V)
        • (source): V
        • Parameters

          • source: any

          Returns V

    Returns ((source) => {
    Β Β Β Β [_: K]: V;
    })

      • (source): {
        Β Β Β Β [_: K]: V;
        }
      • Parameters

        • source: any

        Returns {
        Β Β Β Β [_: K]: V;
        }

Generated using TypeDoc

\ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Nullable.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Nullable.html deleted file mode 100644 index 26b37c367..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Nullable.html +++ /dev/null @@ -1,3 +0,0 @@ -Nullable | @wailsio/runtime
  • Nullable takes a creation function for an arbitrary type -and returns a creation function for a nullable value of that type.

    -

    Type Parameters

    • T

    Parameters

    • element: ((source) => T)
        • (source): T
        • Parameters

          • source: any

          Returns T

    Returns ((source) => null | T)

      • (source): null | T
      • Parameters

        • source: any

        Returns null | T

Generated using TypeDoc

\ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Struct.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Struct.html deleted file mode 100644 index 800377936..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Create.Struct.html +++ /dev/null @@ -1,3 +0,0 @@ -Struct | @wailsio/runtime
  • Struct takes an object mapping field names to creation functions -and returns an in-place creation function for a struct.

    -

    Type Parameters

    • T extends {
      Β Β Β Β [_: string]: ((source) => any);
      }
    • U extends {
      Β Β Β Β [Key in string | number | symbol]?: ReturnType<T[Key]>
      }

    Parameters

    • createField: T

    Returns ((source) => U)

      • (source): U
      • Parameters

        • source: any

        Returns U

Generated using TypeDoc

\ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Error.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Error.html index 0672de6ac..1fbdcf82e 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Error.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Error.html @@ -1,5 +1,4 @@ -Error | @wailsio/runtime
  • Parameters

    Returns Promise<string>

      -
    • The label of the button pressed
    • -
    -

Generated using TypeDoc

\ No newline at end of file +Error | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Info.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Info.html index 3ad7968fe..e8606ad19 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Info.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Info.html @@ -1,5 +1,4 @@ -Info | @wailsio/runtime
  • Parameters

    Returns Promise<string>

      -
    • The label of the button pressed
    • -
    -

Generated using TypeDoc

\ No newline at end of file +Info | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.OpenFile.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.OpenFile.html index 6a50199ac..13ba2d544 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.OpenFile.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.OpenFile.html @@ -1,3 +1,10 @@ -OpenFile | @wailsio/runtime
  • Parameters

    Returns Promise<string | string[]>

    Returns selected file or list of files. Returns blank string if no file is selected.

    -

Generated using TypeDoc

\ No newline at end of file +OpenFile | @wailsio/runtime
  • Presents a file selection dialog to pick one or more files to open.

    +

    Parameters

    Returns Promise<string[]>

    Selected file or list of files, or a blank string/empty list if no file has been selected.

    +
  • Presents a file selection dialog to pick one or more files to open.

    +

    Parameters

    Returns Promise<string>

    Selected file or list of files, or a blank string/empty list if no file has been selected.

    +
  • Presents a file selection dialog to pick one or more files to open.

    +

    Parameters

    Returns Promise<string | string[]>

    Selected file or list of files, or a blank string/empty list if no file has been selected.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Question.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Question.html index 5531a7c07..cb7ad090a 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Question.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Question.html @@ -1,5 +1,4 @@ -Question | @wailsio/runtime
  • Parameters

    Returns Promise<string>

      -
    • The label of the button pressed
    • -
    -

Generated using TypeDoc

\ No newline at end of file +Question | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.SaveFile.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.SaveFile.html index 8bd6014ef..7e2ec1103 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.SaveFile.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.SaveFile.html @@ -1,3 +1,4 @@ -SaveFile | @wailsio/runtime
  • Parameters

    Returns Promise<string>

    Returns the selected file. Returns blank string if no file is selected.

    -

Generated using TypeDoc

\ No newline at end of file +SaveFile | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Warning.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Warning.html index 1bc95ef80..be1da407a 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Warning.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Dialogs.Warning.html @@ -1,5 +1,4 @@ -Warning | @wailsio/runtime
  • Parameters

    Returns Promise<string>

      -
    • The label of the button pressed
    • -
    -

Generated using TypeDoc

\ No newline at end of file +Warning | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Emit.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Emit.html index af707a219..472ec4758 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Emit.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Emit.html @@ -1,6 +1,5 @@ -Emit | @wailsio/runtime
  • Emits an event using the given event name.

    -

    Parameters

    Returns any

      -
    • The result of the emitted event.
    • -
    -

Generated using TypeDoc

\ No newline at end of file +Emit | @wailsio/runtime
  • Emits an event using the name and data.

    +

    Parameters

    • name: string

      the name of the event to emit.

      +
    • Optionaldata: any

      the data to be sent with the event.

      +

    Returns Promise<void>

    A promise that will be fulfilled once the event has been emitted.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Off.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Off.html index 54cbdae93..4be336e2a 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Off.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Off.html @@ -1,4 +1,3 @@ -Off | @wailsio/runtime
  • Removes event listeners for the specified event names.

    -

    Parameters

    • eventName: string

      The name of the event to remove listeners for.

      -
    • Rest ...additionalEventNames: string[]

      Additional event names to remove listeners for.

      -

    Returns undefined

Generated using TypeDoc

\ No newline at end of file +Off | @wailsio/runtime
  • Removes event listeners for the specified event names.

    +

    Parameters

    • ...eventNames: [string, ...string[]]

      The name of the events to remove listeners for.

      +

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.OffAll.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.OffAll.html index 9f5b06ca9..e1015a79b 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.OffAll.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.OffAll.html @@ -1,3 +1,2 @@ -OffAll | @wailsio/runtime
  • Removes all event listeners.

    -

    Returns void

    Function

    OffAll

    -

Generated using TypeDoc

\ No newline at end of file +OffAll | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.On.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.On.html index 47f71bd94..5d679cbd7 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.On.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.On.html @@ -1,7 +1,5 @@ -On | @wailsio/runtime
  • Registers a callback function to be executed when the specified event occurs.

    -

    Parameters

    • eventName: string

      The name of the event.

      -
    • callback: Function

      The callback function to be executed. It takes no parameters.

      -

    Returns Function

      -
    • A function that, when called, will unregister the callback from the event.
    • -
    -

Generated using TypeDoc

\ No newline at end of file +On | @wailsio/runtime
  • Registers a callback function to be executed when the specified event occurs.

    +

    Parameters

    • eventName: string

      The name of the event to register the callback for.

      +
    • callback: Callback

      The callback function to be called when the event is triggered.

      +

    Returns () => void

    A function that, when called, will unregister the callback from the event.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.OnMultiple.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.OnMultiple.html index 3d51fbd33..af4f37190 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.OnMultiple.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.OnMultiple.html @@ -1,8 +1,6 @@ -OnMultiple | @wailsio/runtime
  • Register a callback function to be called multiple times for a specific event.

    +OnMultiple | @wailsio/runtime
    • Register a callback function to be called multiple times for a specific event.

      Parameters

      • eventName: string

        The name of the event to register the callback for.

        -
      • callback: Function

        The callback function to be called when the event is triggered.

        +
      • callback: Callback

        The callback function to be called when the event is triggered.

      • maxCallbacks: number

        The maximum number of times the callback can be called for the event. Once the maximum number is reached, the callback will no longer be called.

        -

      Returns Function

        -
      • A function that, when called, will unregister the callback from the event.
      • -
      -

    Generated using TypeDoc

    \ No newline at end of file +

Returns () => void

A function that, when called, will unregister the callback from the event.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Once.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Once.html index 5e286a7be..33bf9a888 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Once.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.Once.html @@ -1,7 +1,5 @@ -Once | @wailsio/runtime
  • Registers a callback function to be executed only once for the specified event.

    -

    Parameters

    • eventName: string

      The name of the event.

      -
    • callback: Function

      The function to be executed when the event occurs.

      -

    Returns Function

      -
    • A function that, when called, will unregister the callback from the event.
    • -
    -

Generated using TypeDoc

\ No newline at end of file +Once | @wailsio/runtime
  • Registers a callback function to be executed only once for the specified event.

    +

    Parameters

    • eventName: string

      The name of the event to register the callback for.

      +
    • callback: Callback

      The callback function to be called when the event is triggered.

      +

    Returns () => void

    A function that, when called, will unregister the callback from the event.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.setup.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.setup.html deleted file mode 100644 index 0620de3a1..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Events.setup.html +++ /dev/null @@ -1 +0,0 @@ -setup | @wailsio/runtime

Generated using TypeDoc

\ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Flags.GetFlag.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Flags.GetFlag.html index 563c4cbfe..2c56d0f1b 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Flags.GetFlag.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Flags.GetFlag.html @@ -1,6 +1,4 @@ -GetFlag | @wailsio/runtime
  • Retrieves the value associated with the specified key from the flag map.

    -

    Parameters

    • keyString: string

      The key to retrieve the value for.

      -

    Returns any

      -
    • The value associated with the specified key.
    • -
    -

Generated using TypeDoc

\ No newline at end of file +GetFlag | @wailsio/runtime
  • Retrieves the value associated with the specified key from the flag map.

    +

    Parameters

    • key: string

      The key to retrieve the value for.

      +

    Returns any

    The value associated with the specified key.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetAll.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetAll.html index a34fc12c4..45d99bb96 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetAll.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetAll.html @@ -1,3 +1,3 @@ -GetAll | @wailsio/runtime
  • Gets all screens.

    -

    Returns Promise<Screen[]>

    A promise that resolves to an array of Screen objects.

    -

Generated using TypeDoc

\ No newline at end of file +GetAll | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetCurrent.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetCurrent.html index af6bae7ef..b042871d5 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetCurrent.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetCurrent.html @@ -1,3 +1,3 @@ -GetCurrent | @wailsio/runtime
  • Gets the current active screen.

    -

    Returns Promise<Screen>

    A promise that resolves with the current active screen.

    -

Generated using TypeDoc

\ No newline at end of file +GetCurrent | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetPrimary.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetPrimary.html index da6e35d2c..a7bb9f8c7 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetPrimary.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/Screens.GetPrimary.html @@ -1,3 +1,3 @@ -GetPrimary | @wailsio/runtime
  • Gets the primary screen.

    -

    Returns Promise<Screen>

    A promise that resolves to the primary screen.

    -

Generated using TypeDoc

\ No newline at end of file +GetPrimary | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.Capabilities.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.Capabilities.html index 593529d86..9127e3a45 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.Capabilities.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.Capabilities.html @@ -1,4 +1,3 @@ -Capabilities | @wailsio/runtime
  • Fetches the capabilities of the application from the server.

    -

    Returns Promise<Object>

    A promise that resolves to an object containing the capabilities.

    -

    Async

    Function

    Capabilities

    -

Generated using TypeDoc

\ No newline at end of file +Capabilities | @wailsio/runtime
  • Fetches the capabilities of the application from the server.

    +

    Returns Promise<Record<string, any>>

    A promise that resolves to an object containing the capabilities.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.Environment.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.Environment.html index e18e37a81..b98671991 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.Environment.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.Environment.html @@ -1,5 +1,3 @@ -Environment | @wailsio/runtime
  • Returns Promise<EnvironmentInfo>

      -
    • A promise that resolves to an object containing OS and system architecture.
    • -
    -

    Function

    Retrieves environment details.

    -

Generated using TypeDoc

\ No newline at end of file +Environment | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsAMD64.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsAMD64.html index b7ab83f45..2d6df4336 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsAMD64.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsAMD64.html @@ -1,3 +1,3 @@ -IsAMD64 | @wailsio/runtime
  • Checks if the current environment architecture is AMD64.

    +IsAMD64 | @wailsio/runtime
    • Checks if the current environment architecture is AMD64.

      Returns boolean

      True if the current environment architecture is AMD64, false otherwise.

      -

    Generated using TypeDoc

    \ No newline at end of file +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsARM.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsARM.html index 280713f3b..5083d6364 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsARM.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsARM.html @@ -1,3 +1,3 @@ -IsARM | @wailsio/runtime
  • Checks if the current architecture is ARM.

    +IsARM | @wailsio/runtime
    • Checks if the current architecture is ARM.

      Returns boolean

      True if the current architecture is ARM, false otherwise.

      -

    Generated using TypeDoc

    \ No newline at end of file +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsARM64.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsARM64.html index b056c3125..eba6cd3e9 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsARM64.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsARM64.html @@ -1,5 +1,3 @@ -IsARM64 | @wailsio/runtime
  • Checks if the current environment is ARM64 architecture.

    -

    Returns boolean

      -
    • Returns true if the environment is ARM64 architecture, otherwise returns false.
    • -
    -

Generated using TypeDoc

\ No newline at end of file +IsARM64 | @wailsio/runtime
  • Checks if the current environment is ARM64 architecture.

    +

    Returns boolean

    Returns true if the environment is ARM64 architecture, otherwise returns false.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsDarkMode.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsDarkMode.html index a9a70ec5e..493882fb0 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsDarkMode.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsDarkMode.html @@ -1,5 +1,3 @@ -IsDarkMode | @wailsio/runtime
  • Returns Promise<boolean>

      -
    • A promise that resolves to a boolean value indicating if the system is in dark mode.
    • -
    -

    Function

    Retrieves the system dark mode status.

    -

Generated using TypeDoc

\ No newline at end of file +IsDarkMode | @wailsio/runtime
  • Retrieves the system dark mode status.

    +

    Returns Promise<boolean>

    A promise that resolves to a boolean value indicating if the system is in dark mode.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsDebug.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsDebug.html index 07b2e4569..0c0370239 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsDebug.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsDebug.html @@ -1 +1,3 @@ -IsDebug | @wailsio/runtime

Generated using TypeDoc

\ No newline at end of file +IsDebug | @wailsio/runtime
  • Reports whether the app is being run in debug mode.

    +

    Returns boolean

    True if the app is being run in debug mode.

    +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsLinux.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsLinux.html index 556563fb6..43e1498c5 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsLinux.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsLinux.html @@ -1,3 +1,3 @@ -IsLinux | @wailsio/runtime
  • Checks if the current operating system is Linux.

    +IsLinux | @wailsio/runtime
    • Checks if the current operating system is Linux.

      Returns boolean

      Returns true if the current operating system is Linux, false otherwise.

      -

    Generated using TypeDoc

    \ No newline at end of file +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsMac.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsMac.html index f702a9d12..d366be072 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsMac.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsMac.html @@ -1,3 +1,3 @@ -IsMac | @wailsio/runtime
  • Checks if the current environment is a macOS operating system.

    +IsMac | @wailsio/runtime
    • Checks if the current environment is a macOS operating system.

      Returns boolean

      True if the environment is macOS, false otherwise.

      -

    Generated using TypeDoc

    \ No newline at end of file +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsWindows.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsWindows.html index a2c1801e5..8948d39dc 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsWindows.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.IsWindows.html @@ -1,3 +1,3 @@ -IsWindows | @wailsio/runtime
  • Checks if the current operating system is Windows.

    +IsWindows | @wailsio/runtime
    • Checks if the current operating system is Windows.

      Returns boolean

      True if the operating system is Windows, otherwise false.

      -

    Generated using TypeDoc

    \ No newline at end of file +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.invoke.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.invoke.html index 662b2a3da..d7455476c 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.invoke.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/System.invoke.html @@ -1 +1 @@ -invoke | @wailsio/runtime
  • Parameters

    • msg: any

    Returns any

Generated using TypeDoc

\ No newline at end of file +invoke | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/WML.Enable.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/WML.Enable.html index 2659bfd80..46bffbe7d 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/WML.Enable.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/WML.Enable.html @@ -1,2 +1,2 @@ -Enable | @wailsio/runtime
  • Schedules an automatic reload of WML to be performed as soon as the document is fully loaded.

    -

    Returns void

Generated using TypeDoc

\ No newline at end of file +Enable | @wailsio/runtime
  • Schedules an automatic reload of WML to be performed as soon as the document is fully loaded.

    +

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/WML.Reload.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/WML.Reload.html index 3659194ea..41478c379 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/WML.Reload.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/WML.Reload.html @@ -1,2 +1,2 @@ -Reload | @wailsio/runtime
  • Reloads the WML page by adding necessary event listeners and browser listeners.

    -

    Returns void

Generated using TypeDoc

\ No newline at end of file +Reload | @wailsio/runtime
  • Reloads the WML page by adding necessary event listeners and browser listeners.

    +

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/init.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/init.html deleted file mode 100644 index 7ae37c231..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/functions/init.html +++ /dev/null @@ -1 +0,0 @@ -init | @wailsio/runtime

Generated using TypeDoc

\ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/hierarchy.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/hierarchy.html new file mode 100644 index 000000000..298304741 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/hierarchy.html @@ -0,0 +1 @@ +@wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/index.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/index.html index eba5c3b45..90f345405 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/index.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/index.html @@ -1,5 +1,7 @@ -@wailsio/runtime

@wailsio/runtime

README

The main.js file in this directory is the entrypoint for the runtime.js file that may be +@wailsio/runtime

@wailsio/runtime

README

The index.js file in the compiled directory is the entrypoint for the runtime.js file that may be loaded at runtime. This will add window.wails and window._wails to the global scope.

NOTE: It is preferable to use the @wailsio/runtime package to use the runtime.

-

After updating any files in this directory, you must run wails3 task build:runtime to regenerate the compiled JS.

-

Generated using TypeDoc

\ No newline at end of file +

⚠️ Do not rebuild the runtime manually after updating TS code: +the CI pipeline will take care of this. +PRs that touch build artifacts will be blocked from merging.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/CancellablePromiseLike.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/CancellablePromiseLike.html new file mode 100644 index 000000000..e7a8ca6f3 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/CancellablePromiseLike.html @@ -0,0 +1,3 @@ +CancellablePromiseLike | @wailsio/runtime

Interface CancellablePromiseLike<T>

interface CancellablePromiseLike<T> {
Β Β Β Β cancel(cause?: any): void | PromiseLike<void>;
Β Β Β Β then<TResult1 = T, TResult2 = never>(
Β Β Β Β Β Β Β Β onfulfilled?:
Β Β Β Β Β Β Β Β Β Β Β Β | null
Β Β Β Β Β Β Β Β Β Β Β Β | (
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β value: T,
Β Β Β Β Β Β Β Β Β Β Β Β ) => TResult1 | PromiseLike<TResult1> | CancellablePromiseLike<TResult1>,
Β Β Β Β Β Β Β Β onrejected?:
Β Β Β Β Β Β Β Β Β Β Β Β | null
Β Β Β Β Β Β Β Β Β Β Β Β | (
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β reason: any,
Β Β Β Β Β Β Β Β Β Β Β Β ) => TResult2 | PromiseLike<TResult2> | CancellablePromiseLike<TResult2>,
Β Β Β Β ): CancellablePromiseLike<TResult1 | TResult2>;
}

Type Parameters

  • T

Implemented by

Methods

Methods

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/CancellablePromiseWithResolvers.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/CancellablePromiseWithResolvers.html new file mode 100644 index 000000000..3bd162eae --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/CancellablePromiseWithResolvers.html @@ -0,0 +1,7 @@ +CancellablePromiseWithResolvers | @wailsio/runtime

Interface CancellablePromiseWithResolvers<T>

Wraps a cancellable promise along with its resolution methods. +The oncancelled field will be null initially but may be set to provide a custom cancellation function.

+
interface CancellablePromiseWithResolvers<T> {
Β Β Β Β oncancelled: null | CancellablePromiseCanceller;
Β Β Β Β promise: CancellablePromise<T>;
Β Β Β Β reject: CancellablePromiseRejector;
Β Β Β Β resolve: CancellablePromiseResolver<T>;
}

Type Parameters

  • T

Properties

oncancelled: null | CancellablePromiseCanceller
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.Button.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.Button.html index 0a8b97861..925410339 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.Button.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.Button.html @@ -1,7 +1,7 @@ -Button | @wailsio/runtime
interface Button {
Β Β Β Β IsCancel: undefined | boolean;
Β Β Β Β IsDefault: undefined | boolean;
Β Β Β Β Label: undefined | string;
}

Properties

Properties

IsCancel: undefined | boolean

True if the button should cancel an operation when clicked.

-
IsDefault: undefined | boolean

True if the button should be the default action when the user presses enter.

-
Label: undefined | string

Text that appears within the button.

-

Generated using TypeDoc

\ No newline at end of file +Button | @wailsio/runtime
interface Button {
Β Β Β Β IsCancel?: boolean;
Β Β Β Β IsDefault?: boolean;
Β Β Β Β Label?: string;
}

Properties

IsCancel?: boolean

True if the button should cancel an operation when clicked.

+
IsDefault?: boolean

True if the button should be the default action when the user presses enter.

+
Label?: string

Text that appears within the button.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.FileFilter.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.FileFilter.html index 149e12992..09958a9cd 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.FileFilter.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.FileFilter.html @@ -1,5 +1,5 @@ -FileFilter | @wailsio/runtime
interface FileFilter {
Β Β Β Β DisplayName: undefined | string;
Β Β Β Β Pattern: undefined | string;
}

Properties

Properties

DisplayName: undefined | string

Display name for the filter, it could be "Text Files", "Images" etc.

-
Pattern: undefined | string

Pattern to match for the filter, e.g. ".txt;.md" for text markdown files.

-

Generated using TypeDoc

\ No newline at end of file +FileFilter | @wailsio/runtime
interface FileFilter {
Β Β Β Β DisplayName?: string;
Β Β Β Β Pattern?: string;
}

Properties

Properties

DisplayName?: string

Display name for the filter, it could be "Text Files", "Images" etc.

+
Pattern?: string

Pattern to match for the filter, e.g. ".txt;.md" for text markdown files.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.MessageDialogOptions.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.MessageDialogOptions.html index 8d9843d86..27ccf9d7a 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.MessageDialogOptions.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.MessageDialogOptions.html @@ -1,9 +1,9 @@ -MessageDialogOptions | @wailsio/runtime

Interface MessageDialogOptions

interface MessageDialogOptions {
Β Β Β Β Buttons: undefined | Button[];
Β Β Β Β Detached: undefined | boolean;
Β Β Β Β Message: undefined | string;
Β Β Β Β Title: undefined | string;
}

Properties

Properties

Buttons: undefined | Button[]

Array of button options to show in the dialog.

-
Detached: undefined | boolean

True if the dialog should appear detached from the main window (if applicable).

-
Message: undefined | string

The main message to show in the dialog.

-
Title: undefined | string

The title of the dialog window.

-

Generated using TypeDoc

\ No newline at end of file +MessageDialogOptions | @wailsio/runtime

Interface MessageDialogOptions

interface MessageDialogOptions {
Β Β Β Β Buttons?: Button[];
Β Β Β Β Detached?: boolean;
Β Β Β Β Message?: string;
Β Β Β Β Title?: string;
}

Properties

Buttons?: Button[]

Array of button options to show in the dialog.

+
Detached?: boolean

True if the dialog should appear detached from the main window (if applicable).

+
Message?: string

The main message to show in the dialog.

+
Title?: string

The title of the dialog window.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.OpenFileDialogOptions.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.OpenFileDialogOptions.html index 52f6b11ea..3dd409a4f 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.OpenFileDialogOptions.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.OpenFileDialogOptions.html @@ -1,33 +1,33 @@ -OpenFileDialogOptions | @wailsio/runtime

Interface OpenFileDialogOptions

interface OpenFileDialogOptions {
Β Β Β Β AllowsMultipleSelection: undefined | boolean;
Β Β Β Β AllowsOtherFiletypes: undefined | boolean;
Β Β Β Β ButtonText: undefined | string;
Β Β Β Β CanChooseDirectories: undefined | boolean;
Β Β Β Β CanChooseFiles: undefined | boolean;
Β Β Β Β CanCreateDirectories: undefined | boolean;
Β Β Β Β CanSelectHiddenExtension: undefined | boolean;
Β Β Β Β Detached: undefined | boolean;
Β Β Β Β Directory: undefined | string;
Β Β Β Β Filters: undefined | FileFilter[];
Β Β Β Β HideExtension: undefined | boolean;
Β Β Β Β Message: undefined | string;
Β Β Β Β ResolvesAliases: undefined | boolean;
Β Β Β Β ShowHiddenFiles: undefined | boolean;
Β Β Β Β Title: undefined | string;
Β Β Β Β TreatsFilePackagesAsDirectories: undefined | boolean;
}

Properties

AllowsMultipleSelection: undefined | boolean

Indicates if multiple selection is allowed.

-
AllowsOtherFiletypes: undefined | boolean

Indicates if other file types are allowed.

-
ButtonText: undefined | string

Text to display on the button.

-
CanChooseDirectories: undefined | boolean

Indicates if directories can be chosen.

-
CanChooseFiles: undefined | boolean

Indicates if files can be chosen.

-
CanCreateDirectories: undefined | boolean

Indicates if directories can be created.

-
CanSelectHiddenExtension: undefined | boolean

Indicates if hidden extensions can be selected.

-
Detached: undefined | boolean

Indicates if the dialog should appear detached from the main window.

-
Directory: undefined | string

Directory to open in the dialog.

-
Filters: undefined | FileFilter[]

Array of file filters.

-
HideExtension: undefined | boolean

Indicates if the extension should be hidden.

-
Message: undefined | string

Message to show in the dialog.

-
ResolvesAliases: undefined | boolean

Indicates if aliases should be resolved.

-
ShowHiddenFiles: undefined | boolean

Indicates if hidden files should be shown.

-
Title: undefined | string

Title of the dialog.

-
TreatsFilePackagesAsDirectories: undefined | boolean

Indicates if file packages should be treated as directories.

-

Generated using TypeDoc

\ No newline at end of file +OpenFileDialogOptions | @wailsio/runtime

Interface OpenFileDialogOptions

interface OpenFileDialogOptions {
Β Β Β Β AllowsMultipleSelection?: boolean;
Β Β Β Β AllowsOtherFiletypes?: boolean;
Β Β Β Β ButtonText?: string;
Β Β Β Β CanChooseDirectories?: boolean;
Β Β Β Β CanChooseFiles?: boolean;
Β Β Β Β CanCreateDirectories?: boolean;
Β Β Β Β CanSelectHiddenExtension?: boolean;
Β Β Β Β Detached?: boolean;
Β Β Β Β Directory?: string;
Β Β Β Β Filters?: FileFilter[];
Β Β Β Β HideExtension?: boolean;
Β Β Β Β Message?: string;
Β Β Β Β ResolvesAliases?: boolean;
Β Β Β Β ShowHiddenFiles?: boolean;
Β Β Β Β Title?: string;
Β Β Β Β TreatsFilePackagesAsDirectories?: boolean;
}

Properties

AllowsMultipleSelection?: boolean

Indicates if multiple selection is allowed.

+
AllowsOtherFiletypes?: boolean

Indicates if other file types are allowed.

+
ButtonText?: string

Text to display on the button.

+
CanChooseDirectories?: boolean

Indicates if directories can be chosen.

+
CanChooseFiles?: boolean

Indicates if files can be chosen.

+
CanCreateDirectories?: boolean

Indicates if directories can be created.

+
CanSelectHiddenExtension?: boolean

Indicates if hidden extensions can be selected.

+
Detached?: boolean

Indicates if the dialog should appear detached from the main window.

+
Directory?: string

Directory to open in the dialog.

+
Filters?: FileFilter[]

Array of file filters.

+
HideExtension?: boolean

Indicates if the extension should be hidden.

+
Message?: string

Message to show in the dialog.

+
ResolvesAliases?: boolean

Indicates if aliases should be resolved.

+
ShowHiddenFiles?: boolean

Indicates if hidden files should be shown.

+
Title?: string

Title of the dialog.

+
TreatsFilePackagesAsDirectories?: boolean

Indicates if file packages should be treated as directories.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.SaveFileDialogOptions.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.SaveFileDialogOptions.html index a51cc257b..6752aefc3 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.SaveFileDialogOptions.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Dialogs.SaveFileDialogOptions.html @@ -1,35 +1,33 @@ -SaveFileDialogOptions | @wailsio/runtime

Interface SaveFileDialogOptions

interface SaveFileDialogOptions {
Β Β Β Β AllowsMultipleSelection: undefined | boolean;
Β Β Β Β AllowsOtherFiletypes: undefined | boolean;
Β Β Β Β ButtonText: undefined | string;
Β Β Β Β CanChooseDirectories: undefined | boolean;
Β Β Β Β CanChooseFiles: undefined | boolean;
Β Β Β Β CanCreateDirectories: undefined | boolean;
Β Β Β Β CanSelectHiddenExtension: undefined | boolean;
Β Β Β Β Detached: undefined | boolean;
Β Β Β Β Directory: undefined | string;
Β Β Β Β Filename: undefined | string;
Β Β Β Β Filters: undefined | FileFilter[];
Β Β Β Β HideExtension: undefined | boolean;
Β Β Β Β Message: undefined | string;
Β Β Β Β ResolvesAliases: undefined | boolean;
Β Β Β Β ShowHiddenFiles: undefined | boolean;
Β Β Β Β Title: undefined | string;
Β Β Β Β TreatsFilePackagesAsDirectories: undefined | boolean;
}

Properties

AllowsMultipleSelection: undefined | boolean

Indicates if multiple selection is allowed.

-
AllowsOtherFiletypes: undefined | boolean

Indicates if other file types are allowed.

-
ButtonText: undefined | string

Text to display on the button.

-
CanChooseDirectories: undefined | boolean

Indicates if directories can be chosen.

-
CanChooseFiles: undefined | boolean

Indicates if files can be chosen.

-
CanCreateDirectories: undefined | boolean

Indicates if directories can be created.

-
CanSelectHiddenExtension: undefined | boolean

Indicates if hidden extensions can be selected.

-
Detached: undefined | boolean

Indicates if the dialog should appear detached from the main window.

-
Directory: undefined | string

Directory to open in the dialog.

-
Filename: undefined | string

Default filename to use in the dialog.

-
Filters: undefined | FileFilter[]

Array of file filters.

-
HideExtension: undefined | boolean

Indicates if the extension should be hidden.

-
Message: undefined | string

Message to show in the dialog.

-
ResolvesAliases: undefined | boolean

Indicates if aliases should be resolved.

-
ShowHiddenFiles: undefined | boolean

Indicates if hidden files should be shown.

-
Title: undefined | string

Title of the dialog.

-
TreatsFilePackagesAsDirectories: undefined | boolean

Indicates if file packages should be treated as directories.

-

Generated using TypeDoc

\ No newline at end of file +SaveFileDialogOptions | @wailsio/runtime

Interface SaveFileDialogOptions

interface SaveFileDialogOptions {
Β Β Β Β AllowsOtherFiletypes?: boolean;
Β Β Β Β ButtonText?: string;
Β Β Β Β CanChooseDirectories?: boolean;
Β Β Β Β CanChooseFiles?: boolean;
Β Β Β Β CanCreateDirectories?: boolean;
Β Β Β Β CanSelectHiddenExtension?: boolean;
Β Β Β Β Detached?: boolean;
Β Β Β Β Directory?: string;
Β Β Β Β Filename?: string;
Β Β Β Β Filters?: FileFilter[];
Β Β Β Β HideExtension?: boolean;
Β Β Β Β Message?: string;
Β Β Β Β ResolvesAliases?: boolean;
Β Β Β Β ShowHiddenFiles?: boolean;
Β Β Β Β Title?: string;
Β Β Β Β TreatsFilePackagesAsDirectories?: boolean;
}

Properties

AllowsOtherFiletypes?: boolean

Indicates if other file types are allowed.

+
ButtonText?: string

Text to display on the button.

+
CanChooseDirectories?: boolean

Indicates if directories can be chosen.

+
CanChooseFiles?: boolean

Indicates if files can be chosen.

+
CanCreateDirectories?: boolean

Indicates if directories can be created.

+
CanSelectHiddenExtension?: boolean

Indicates if hidden extensions can be selected.

+
Detached?: boolean

Indicates if the dialog should appear detached from the main window.

+
Directory?: string

Directory to open in the dialog.

+
Filename?: string

Default filename to use in the dialog.

+
Filters?: FileFilter[]

Array of file filters.

+
HideExtension?: boolean

Indicates if the extension should be hidden.

+
Message?: string

Message to show in the dialog.

+
ResolvesAliases?: boolean

Indicates if aliases should be resolved.

+
ShowHiddenFiles?: boolean

Indicates if hidden files should be shown.

+
Title?: string

Title of the dialog.

+
TreatsFilePackagesAsDirectories?: boolean

Indicates if file packages should be treated as directories.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Rect.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Rect.html index 09b621d07..3c3b36416 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Rect.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Rect.html @@ -1,9 +1,9 @@ -Rect | @wailsio/runtime
interface Rect {
Β Β Β Β Height: number;
Β Β Β Β Width: number;
Β Β Β Β X: number;
Β Β Β Β Y: number;
}

Properties

Properties

Height: number

The height of the rectangle.

-
Width: number

The width of the rectangle.

-
X: number

The X coordinate of the origin.

-
Y: number

The Y coordinate of the origin.

-

Generated using TypeDoc

\ No newline at end of file +Rect | @wailsio/runtime
interface Rect {
Β Β Β Β Height: number;
Β Β Β Β Width: number;
Β Β Β Β X: number;
Β Β Β Β Y: number;
}

Properties

Properties

Height: number

The height of the rectangle.

+
Width: number

The width of the rectangle.

+
X: number

The X coordinate of the origin.

+
Y: number

The Y coordinate of the origin.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Screen.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Screen.html index cc548efe2..78e0df769 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Screen.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Screen.html @@ -1,25 +1,25 @@ -Screen | @wailsio/runtime
interface Screen {
Β Β Β Β Bounds: Rect;
Β Β Β Β ID: string;
Β Β Β Β IsPrimary: boolean;
Β Β Β Β Name: string;
Β Β Β Β PhysicalBounds: Rect;
Β Β Β Β PhysicalWorkArea: Rect;
Β Β Β Β Rotation: number;
Β Β Β Β ScaleFactor: number;
Β Β Β Β Size: Size;
Β Β Β Β WorkArea: Rect;
Β Β Β Β X: number;
Β Β Β Β Y: number;
}

Properties

Bounds: Rect

Contains the bounds of the screen in terms of X, Y, Width, and Height.

-
ID: string

Unique identifier for the screen.

-
IsPrimary: boolean

True if this is the primary monitor selected by the user in the operating system.

-
Name: string

Human readable name of the screen.

-
PhysicalBounds: Rect

Contains the physical bounds of the screen in terms of X, Y, Width, and Height (before scaling).

-
PhysicalWorkArea: Rect

Contains the physical WorkArea of the screen (before scaling).

-
Rotation: number

The rotation of the screen.

-
ScaleFactor: number

The scale factor of the screen (DPI/96). 1 = standard DPI, 2 = HiDPI (Retina), etc.

-
Size: Size

Contains the width and height of the screen.

-
WorkArea: Rect

Contains the area of the screen that is actually usable (excluding taskbar and other system UI).

-
X: number

The X coordinate of the screen.

-
Y: number

The Y coordinate of the screen.

-

Generated using TypeDoc

\ No newline at end of file +Screen | @wailsio/runtime
interface Screen {
Β Β Β Β Bounds: Rect;
Β Β Β Β ID: string;
Β Β Β Β IsPrimary: boolean;
Β Β Β Β Name: string;
Β Β Β Β PhysicalBounds: Rect;
Β Β Β Β PhysicalWorkArea: Rect;
Β Β Β Β Rotation: number;
Β Β Β Β ScaleFactor: number;
Β Β Β Β Size: Screens.Size;
Β Β Β Β WorkArea: Rect;
Β Β Β Β X: number;
Β Β Β Β Y: number;
}

Properties

Bounds: Rect

Contains the bounds of the screen in terms of X, Y, Width, and Height.

+
ID: string

Unique identifier for the screen.

+
IsPrimary: boolean

True if this is the primary monitor selected by the user in the operating system.

+
Name: string

Human-readable name of the screen.

+
PhysicalBounds: Rect

Contains the physical bounds of the screen in terms of X, Y, Width, and Height (before scaling).

+
PhysicalWorkArea: Rect

Contains the physical WorkArea of the screen (before scaling).

+
Rotation: number

The rotation of the screen.

+
ScaleFactor: number

The scale factor of the screen (DPI/96). 1 = standard DPI, 2 = HiDPI (Retina), etc.

+

Contains the width and height of the screen.

+
WorkArea: Rect

Contains the area of the screen that is actually usable (excluding taskbar and other system UI).

+
X: number

The X coordinate of the screen.

+
Y: number

The Y coordinate of the screen.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Size.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Size.html index b5d89c97b..03f72fcaf 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Size.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/Screens.Size.html @@ -1,5 +1,5 @@ -Size | @wailsio/runtime
interface Size {
Β Β Β Β Height: number;
Β Β Β Β Width: number;
}

Properties

Properties

Height: number

The height.

-
Width: number

The width.

-

Generated using TypeDoc

\ No newline at end of file +Size | @wailsio/runtime
interface Size {
Β Β Β Β Height: number;
Β Β Β Β Width: number;
}

Properties

Properties

Height: number

The height of a rectangular area.

+
Width: number

The width of a rectangular area.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/System.EnvironmentInfo.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/System.EnvironmentInfo.html index 9771266f3..891e7dcaa 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/System.EnvironmentInfo.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/System.EnvironmentInfo.html @@ -1,11 +1,11 @@ -EnvironmentInfo | @wailsio/runtime

Interface EnvironmentInfo

interface EnvironmentInfo {
Β Β Β Β Arch: string;
Β Β Β Β Debug: boolean;
Β Β Β Β OS: string;
Β Β Β Β OSInfo: OSInfo;
Β Β Β Β PlatformInfo: Object;
}

Properties

Properties

Arch: string

The architecture of the system.

-
Debug: boolean

True if the application is running in debug mode, otherwise false.

-
OS: string

The operating system in use.

-
OSInfo: OSInfo

Details of the operating system.

-
PlatformInfo: Object

Additional platform information.

-

Generated using TypeDoc

\ No newline at end of file +EnvironmentInfo | @wailsio/runtime

Interface EnvironmentInfo

interface EnvironmentInfo {
Β Β Β Β Arch: string;
Β Β Β Β Debug: boolean;
Β Β Β Β OS: string;
Β Β Β Β OSInfo: OSInfo;
Β Β Β Β PlatformInfo: Record<string, any>;
}

Properties

Properties

Arch: string

The architecture of the system.

+
Debug: boolean

True if the application is running in debug mode, otherwise false.

+
OS: string

The operating system in use.

+
OSInfo: OSInfo

Details of the operating system.

+
PlatformInfo: Record<string, any>

Additional platform information.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/System.OSInfo.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/System.OSInfo.html index 15b0b3697..3f2b84f9e 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/System.OSInfo.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/System.OSInfo.html @@ -1,9 +1,9 @@ -OSInfo | @wailsio/runtime
interface OSInfo {
Β Β Β Β Branding: string;
Β Β Β Β ID: string;
Β Β Β Β Name: string;
Β Β Β Β Version: string;
}

Properties

Properties

Branding: string

The branding of the OS.

-
ID: string

The ID of the OS.

-
Name: string

The name of the OS.

-
Version: string

The version of the OS.

-

Generated using TypeDoc

\ No newline at end of file +OSInfo | @wailsio/runtime
interface OSInfo {
Β Β Β Β Branding: string;
Β Β Β Β ID: string;
Β Β Β Β Name: string;
Β Β Β Β Version: string;
}

Properties

Properties

Branding: string

The branding of the OS.

+
ID: string

The ID of the OS.

+
Name: string

The name of the OS.

+
Version: string

The version of the OS.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.AddEventListenerOptions.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.AddEventListenerOptions.html new file mode 100644 index 000000000..639fc6f43 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.AddEventListenerOptions.html @@ -0,0 +1,5 @@ +AddEventListenerOptions | @wailsio/runtime

Interface AddEventListenerOptions

interface AddEventListenerOptions {
Β Β Β Β capture?: boolean;
Β Β Β Β once?: boolean;
Β Β Β Β passive?: boolean;
Β Β Β Β signal?: AbortSignal;
}

Hierarchy (View Summary)

Properties

capture?: boolean
once?: boolean
passive?: boolean
signal?: AbortSignal
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ArrayBufferView.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ArrayBufferView.html new file mode 100644 index 000000000..34cc95d72 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ArrayBufferView.html @@ -0,0 +1,7 @@ +ArrayBufferView | @wailsio/runtime

Interface ArrayBufferView<TArrayBuffer>

interface ArrayBufferView<
Β Β Β Β TArrayBuffer extends ArrayBufferLike = ArrayBufferLike,
> {
Β Β Β Β buffer: TArrayBuffer;
Β Β Β Β byteLength: number;
Β Β Β Β byteOffset: number;
}

Type Parameters

Properties

buffer: TArrayBuffer

The ArrayBuffer instance referenced by the array.

+
byteLength: number

The length in bytes of the array.

+
byteOffset: number

The offset in bytes of the array.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Blob.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Blob.html new file mode 100644 index 000000000..051fea2a3 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Blob.html @@ -0,0 +1,17 @@ +Blob | @wailsio/runtime

A file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system.

+

MDN Reference

+
interface Blob {
Β Β Β Β size: number;
Β Β Β Β type: string;
Β Β Β Β arrayBuffer(): Promise<ArrayBuffer>;
Β Β Β Β bytes(): Promise<Uint8Array<ArrayBufferLike>>;
Β Β Β Β slice(start?: number, end?: number, contentType?: string): Blob;
Β Β Β Β stream(): ReadableStream<Uint8Array<ArrayBufferLike>>;
Β Β Β Β text(): Promise<string>;
}

Properties

Methods

Properties

size: number
type: string

Methods

  • Parameters

    • Optionalstart: number
    • Optionalend: number
    • OptionalcontentType: string

    Returns Blob

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.BlobPropertyBag.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.BlobPropertyBag.html new file mode 100644 index 000000000..6fdfdc2d7 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.BlobPropertyBag.html @@ -0,0 +1,3 @@ +BlobPropertyBag | @wailsio/runtime
interface BlobPropertyBag {
Β Β Β Β endings?: EndingType;
Β Β Β Β type?: string;
}

Properties

Properties

endings?: EndingType
type?: string
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ErrorOptions.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ErrorOptions.html new file mode 100644 index 000000000..12c1c1dec --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ErrorOptions.html @@ -0,0 +1,2 @@ +ErrorOptions | @wailsio/runtime
interface ErrorOptions {
Β Β Β Β cause?: unknown;
}

Properties

Properties

cause?: unknown
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Event.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Event.html new file mode 100644 index 000000000..658cc399a --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Event.html @@ -0,0 +1,57 @@ +Event | @wailsio/runtime

An event which takes place in the DOM.

+

MDN Reference

+
interface Event {
Β Β Β Β AT_TARGET: 2;
Β Β Β Β bubbles: boolean;
Β Β Β Β BUBBLING_PHASE: 3;
Β Β Β Β cancelable: boolean;
Β Β Β Β cancelBubble: boolean;
Β Β Β Β CAPTURING_PHASE: 1;
Β Β Β Β composed: boolean;
Β Β Β Β currentTarget: null | EventTarget;
Β Β Β Β defaultPrevented: boolean;
Β Β Β Β eventPhase: number;
Β Β Β Β isTrusted: boolean;
Β Β Β Β NONE: 0;
Β Β Β Β returnValue: boolean;
Β Β Β Β srcElement: null | EventTarget;
Β Β Β Β target: null | EventTarget;
Β Β Β Β timeStamp: number;
Β Β Β Β type: string;
Β Β Β Β composedPath(): EventTarget[];
Β Β Β Β initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void;
Β Β Β Β preventDefault(): void;
Β Β Β Β stopImmediatePropagation(): void;
Β Β Β Β stopPropagation(): void;
}

Properties

AT_TARGET: 2
bubbles: boolean

Returns true or false depending on how event was initialized. True if event goes through its target's ancestors in reverse tree order, and false otherwise.

+

MDN Reference

+
BUBBLING_PHASE: 3
cancelable: boolean

Returns true or false depending on how event was initialized. Its return value does not always carry meaning, but true can indicate that part of the operation during which event was dispatched, can be canceled by invoking the preventDefault() method.

+

MDN Reference

+
cancelBubble: boolean

MDN Reference

+
CAPTURING_PHASE: 1
composed: boolean

Returns true or false depending on how event was initialized. True if event invokes listeners past a ShadowRoot node that is the root of its target, and false otherwise.

+

MDN Reference

+
currentTarget: null | EventTarget

Returns the object whose event listener's callback is currently being invoked.

+

MDN Reference

+
defaultPrevented: boolean

Returns true if preventDefault() was invoked successfully to indicate cancelation, and false otherwise.

+

MDN Reference

+
eventPhase: number

Returns the event's phase, which is one of NONE, CAPTURING_PHASE, AT_TARGET, and BUBBLING_PHASE.

+

MDN Reference

+
isTrusted: boolean

Returns true if event was dispatched by the user agent, and false otherwise.

+

MDN Reference

+
NONE: 0
returnValue: boolean

MDN Reference

+
srcElement: null | EventTarget

MDN Reference

+
target: null | EventTarget

Returns the object to which event is dispatched (its target).

+

MDN Reference

+
timeStamp: number

Returns the event's timestamp as the number of milliseconds measured relative to the time origin.

+

MDN Reference

+
type: string

Returns the type of event, e.g. "click", "hashchange", or "submit".

+

MDN Reference

+

Methods

  • Returns the invocation target objects of event's path (objects on which listeners will be invoked), except for any nodes in shadow trees of which the shadow root's mode is "closed" that are not reachable from event's currentTarget.

    +

    MDN Reference

    +

    Returns EventTarget[]

  • Parameters

    • type: string
    • Optionalbubbles: boolean
    • Optionalcancelable: boolean

    Returns void

    MDN Reference

    +
  • If invoked when the cancelable attribute value is true, and while executing a listener for the event with passive set to false, signals to the operation that caused event to be dispatched that it needs to be canceled.

    +

    MDN Reference

    +

    Returns void

  • Invoking this method prevents event from reaching any registered event listeners after the current one finishes running and, when dispatched in a tree, also prevents event from reaching any other objects.

    +

    MDN Reference

    +

    Returns void

  • When dispatched in a tree, invoking this method prevents event from reaching any objects other than the current object.

    +

    MDN Reference

    +

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventInit.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventInit.html new file mode 100644 index 000000000..27cc94ba8 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventInit.html @@ -0,0 +1,4 @@ +EventInit | @wailsio/runtime
interface EventInit {
Β Β Β Β bubbles?: boolean;
Β Β Β Β cancelable?: boolean;
Β Β Β Β composed?: boolean;
}

Properties

bubbles?: boolean
cancelable?: boolean
composed?: boolean
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventListener.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventListener.html new file mode 100644 index 000000000..aa2f55376 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventListener.html @@ -0,0 +1 @@ +EventListener | @wailsio/runtime
  • Parameters

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventListenerObject.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventListenerObject.html new file mode 100644 index 000000000..c644929fd --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventListenerObject.html @@ -0,0 +1,2 @@ +EventListenerObject | @wailsio/runtime
interface EventListenerObject {
Β Β Β Β handleEvent(object: Event): void;
}

Methods

Methods

  • Parameters

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventListenerOptions.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventListenerOptions.html new file mode 100644 index 000000000..5aa0f9a40 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventListenerOptions.html @@ -0,0 +1,2 @@ +EventListenerOptions | @wailsio/runtime
interface EventListenerOptions {
Β Β Β Β capture?: boolean;
}

Hierarchy (View Summary)

Properties

Properties

capture?: boolean
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventTarget.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventTarget.html new file mode 100644 index 000000000..949e7f417 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.EventTarget.html @@ -0,0 +1,18 @@ +EventTarget | @wailsio/runtime

EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them.

+

MDN Reference

+
interface EventTarget {
Β Β Β Β addEventListener(
Β Β Β Β Β Β Β Β type: string,
Β Β Β Β Β Β Β Β callback: null | EventListenerOrEventListenerObject,
Β Β Β Β Β Β Β Β options?: boolean | AddEventListenerOptions,
Β Β Β Β ): void;
Β Β Β Β dispatchEvent(event: Event): boolean;
Β Β Β Β removeEventListener(
Β Β Β Β Β Β Β Β type: string,
Β Β Β Β Β Β Β Β callback: null | EventListenerOrEventListenerObject,
Β Β Β Β Β Β Β Β options?: boolean | EventListenerOptions,
Β Β Β Β ): void;
}

Hierarchy (View Summary)

Methods

  • Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.

    +

    The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture.

    +

    When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET.

    +

    When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in Β§ 2.8 Observing event listeners.

    +

    When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed.

    +

    If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted.

    +

    The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture.

    +

    MDN Reference

    +

    Parameters

    Returns void

  • Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise.

    +

    MDN Reference

    +

    Parameters

    Returns boolean

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Iterable.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Iterable.html new file mode 100644 index 000000000..02cc4cf80 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Iterable.html @@ -0,0 +1,2 @@ +Iterable | @wailsio/runtime

Interface Iterable<T, TReturn, TNext>

interface Iterable<T, TReturn = any, TNext = any> {
Β Β Β Β "[iterator]"(): Iterator<T, TReturn, TNext>;
}

Type Parameters

  • T
  • TReturn = any
  • TNext = any

Methods

Methods

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.MediaSource.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.MediaSource.html new file mode 100644 index 000000000..a5d04858e --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.MediaSource.html @@ -0,0 +1,49 @@ +MediaSource | @wailsio/runtime

This Media Source Extensions API interface represents a source of media data for an HTMLMediaElement object. A MediaSource object can be attached to a HTMLMediaElement to be played in the user agent.

+

MDN Reference

+
interface MediaSource {
Β Β Β Β activeSourceBuffers: SourceBufferList;
Β Β Β Β duration: number;
Β Β Β Β onsourceclose: null | (this: MediaSource, ev: Event) => any;
Β Β Β Β onsourceended: null | (this: MediaSource, ev: Event) => any;
Β Β Β Β onsourceopen: null | (this: MediaSource, ev: Event) => any;
Β Β Β Β readyState: ReadyState;
Β Β Β Β sourceBuffers: SourceBufferList;
Β Β Β Β addEventListener<K extends keyof MediaSourceEventMap>(
Β Β Β Β Β Β Β Β type: K,
Β Β Β Β Β Β Β Β listener: (this: MediaSource, ev: MediaSourceEventMap[K]) => any,
Β Β Β Β Β Β Β Β options?: boolean | AddEventListenerOptions,
Β Β Β Β ): void;
Β Β Β Β addEventListener(
Β Β Β Β Β Β Β Β type: string,
Β Β Β Β Β Β Β Β listener: EventListenerOrEventListenerObject,
Β Β Β Β Β Β Β Β options?: boolean | AddEventListenerOptions,
Β Β Β Β ): void;
Β Β Β Β addSourceBuffer(type: string): SourceBuffer;
Β Β Β Β clearLiveSeekableRange(): void;
Β Β Β Β dispatchEvent(event: Event): boolean;
Β Β Β Β endOfStream(error?: EndOfStreamError): void;
Β Β Β Β removeEventListener<K extends keyof MediaSourceEventMap>(
Β Β Β Β Β Β Β Β type: K,
Β Β Β Β Β Β Β Β listener: (this: MediaSource, ev: MediaSourceEventMap[K]) => any,
Β Β Β Β Β Β Β Β options?: boolean | EventListenerOptions,
Β Β Β Β ): void;
Β Β Β Β removeEventListener(
Β Β Β Β Β Β Β Β type: string,
Β Β Β Β Β Β Β Β listener: EventListenerOrEventListenerObject,
Β Β Β Β Β Β Β Β options?: boolean | EventListenerOptions,
Β Β Β Β ): void;
Β Β Β Β removeSourceBuffer(sourceBuffer: SourceBuffer): void;
Β Β Β Β setLiveSeekableRange(start: number, end: number): void;
}

Hierarchy (View Summary)

Properties

activeSourceBuffers: SourceBufferList
duration: number
onsourceclose: null | (this: MediaSource, ev: Event) => any
onsourceended: null | (this: MediaSource, ev: Event) => any
onsourceopen: null | (this: MediaSource, ev: Event) => any
readyState: ReadyState
sourceBuffers: SourceBufferList

Methods

  • Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.

    +

    The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture.

    +

    When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET.

    +

    When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in Β§ 2.8 Observing event listeners.

    +

    When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed.

    +

    If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted.

    +

    The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture.

    +

    MDN Reference

    +

    Type Parameters

    Parameters

    Returns void

  • Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.

    +

    The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture.

    +

    When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET.

    +

    When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in Β§ 2.8 Observing event listeners.

    +

    When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed.

    +

    If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted.

    +

    The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture.

    +

    MDN Reference

    +

    Parameters

    Returns void

  • Returns void

  • Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise.

    +

    MDN Reference

    +

    Parameters

    Returns boolean

  • Parameters

    • start: number
    • end: number

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.MediaSourceEventMap.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.MediaSourceEventMap.html new file mode 100644 index 000000000..ece981957 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.MediaSourceEventMap.html @@ -0,0 +1,4 @@ +MediaSourceEventMap | @wailsio/runtime
interface MediaSourceEventMap {
Β Β Β Β sourceclose: Event;
Β Β Β Β sourceended: Event;
Β Β Β Β sourceopen: Event;
}

Properties

sourceclose: Event
sourceended: Event
sourceopen: Event
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Position.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Position.html new file mode 100644 index 000000000..ca5d07d94 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Position.html @@ -0,0 +1,6 @@ +Position | @wailsio/runtime

A record describing the position of a window.

+
interface Position {
Β Β Β Β x: number;
Β Β Β Β y: number;
}

Properties

x +y +

Properties

x: number

The horizontal position of the window.

+
y: number

The vertical position of the window.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseFulfilledResult.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseFulfilledResult.html new file mode 100644 index 000000000..0f1002ee8 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseFulfilledResult.html @@ -0,0 +1,3 @@ +PromiseFulfilledResult | @wailsio/runtime

Interface PromiseFulfilledResult<T>

interface PromiseFulfilledResult<T> {
Β Β Β Β status: "fulfilled";
Β Β Β Β value: T;
}

Type Parameters

  • T

Properties

Properties

status: "fulfilled"
value: T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseLike.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseLike.html new file mode 100644 index 000000000..4add9f58e --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseLike.html @@ -0,0 +1,6 @@ +PromiseLike | @wailsio/runtime
interface PromiseLike<T> {
Β Β Β Β then<TResult1 = T, TResult2 = never>(
Β Β Β Β Β Β Β Β onfulfilled?: null | (value: T) => TResult1 | PromiseLike<TResult1>,
Β Β Β Β Β Β Β Β onrejected?: null | (reason: any) => TResult2 | PromiseLike<TResult2>,
Β Β Β Β ): PromiseLike<TResult1 | TResult2>;
}

Type Parameters

  • T

Implemented by

Methods

Methods

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseRejectedResult.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseRejectedResult.html new file mode 100644 index 000000000..a364e257f --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseRejectedResult.html @@ -0,0 +1,3 @@ +PromiseRejectedResult | @wailsio/runtime
interface PromiseRejectedResult {
Β Β Β Β reason: any;
Β Β Β Β status: "rejected";
}

Properties

Properties

reason: any
status: "rejected"
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseWithResolvers.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseWithResolvers.html new file mode 100644 index 000000000..b942f497b --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.PromiseWithResolvers.html @@ -0,0 +1,4 @@ +PromiseWithResolvers | @wailsio/runtime

Interface PromiseWithResolvers<T>

interface PromiseWithResolvers<T> {
Β Β Β Β promise: Promise<T>;
Β Β Β Β reject: (reason?: any) => void;
Β Β Β Β resolve: (value: T | PromiseLike<T>) => void;
}

Type Parameters

  • T

Properties

Properties

promise: Promise<T>
reject: (reason?: any) => void
resolve: (value: T | PromiseLike<T>) => void
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.QueuingStrategy.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.QueuingStrategy.html new file mode 100644 index 000000000..149c55d8c --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.QueuingStrategy.html @@ -0,0 +1,3 @@ +QueuingStrategy | @wailsio/runtime

Interface QueuingStrategy<T>

interface QueuingStrategy<T = any> {
Β Β Β Β highWaterMark?: number;
Β Β Β Β size?: QueuingStrategySize<T>;
}

Type Parameters

  • T = any

Properties

Properties

highWaterMark?: number
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.QueuingStrategySize.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.QueuingStrategySize.html new file mode 100644 index 000000000..5f653c14e --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.QueuingStrategySize.html @@ -0,0 +1 @@ +QueuingStrategySize | @wailsio/runtime

Interface QueuingStrategySize<T>

Type Parameters

  • T = any
  • Parameters

    • chunk: T

    Returns number

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableByteStreamController.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableByteStreamController.html new file mode 100644 index 000000000..5dce85cc7 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableByteStreamController.html @@ -0,0 +1,12 @@ +ReadableByteStreamController | @wailsio/runtime

Interface ReadableByteStreamController

interface ReadableByteStreamController {
Β Β Β Β byobRequest: null | ReadableStreamBYOBRequest;
Β Β Β Β desiredSize: null | number;
Β Β Β Β close(): void;
Β Β Β Β enqueue(chunk: ArrayBufferView): void;
Β Β Β Β error(e?: any): void;
}

Properties

Methods

Properties

byobRequest: null | ReadableStreamBYOBRequest
desiredSize: null | number

Methods

  • Returns void

  • Parameters

    • Optionale: any

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStream.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStream.html new file mode 100644 index 000000000..b7d1939f3 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStream.html @@ -0,0 +1,15 @@ +ReadableStream | @wailsio/runtime

Interface ReadableStream<R>

This Streams API interface represents a readable stream of byte data. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object.

+

MDN Reference

+
interface ReadableStream<R = any> {
Β Β Β Β locked: boolean;
Β Β Β Β cancel(reason?: any): Promise<void>;
Β Β Β Β getReader(options: { mode: "byob" }): ReadableStreamBYOBReader;
Β Β Β Β getReader(): ReadableStreamDefaultReader<R>;
Β Β Β Β getReader(
Β Β Β Β Β Β Β Β options?: ReadableStreamGetReaderOptions,
Β Β Β Β ): ReadableStreamReader<R>;
Β Β Β Β pipeThrough<T>(
Β Β Β Β Β Β Β Β transform: ReadableWritablePair<T, R>,
Β Β Β Β Β Β Β Β options?: StreamPipeOptions,
Β Β Β Β ): ReadableStream<T>;
Β Β Β Β pipeTo(
Β Β Β Β Β Β Β Β destination: WritableStream<R>,
Β Β Β Β Β Β Β Β options?: StreamPipeOptions,
Β Β Β Β ): Promise<void>;
Β Β Β Β tee(): [ReadableStream<R>, ReadableStream<R>];
}

Type Parameters

  • R = any

Properties

Methods

Properties

locked: boolean

Methods

  • Parameters

    • Optionalreason: any

    Returns Promise<void>

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamBYOBReader.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamBYOBReader.html new file mode 100644 index 000000000..d88369e3d --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamBYOBReader.html @@ -0,0 +1,10 @@ +ReadableStreamBYOBReader | @wailsio/runtime

Interface ReadableStreamBYOBReader

interface ReadableStreamBYOBReader {
Β Β Β Β closed: Promise<undefined>;
Β Β Β Β cancel(reason?: any): Promise<void>;
Β Β Β Β read<T extends ArrayBufferView<ArrayBufferLike>>(
Β Β Β Β Β Β Β Β view: T,
Β Β Β Β ): Promise<ReadableStreamReadResult<T>>;
Β Β Β Β releaseLock(): void;
}

Hierarchy (View Summary)

Properties

Methods

Properties

closed: Promise<undefined>

Methods

  • Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamBYOBRequest.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamBYOBRequest.html new file mode 100644 index 000000000..a0085f9a8 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamBYOBRequest.html @@ -0,0 +1,8 @@ +ReadableStreamBYOBRequest | @wailsio/runtime

Interface ReadableStreamBYOBRequest

interface ReadableStreamBYOBRequest {
Β Β Β Β view: null | ArrayBufferView<ArrayBufferLike>;
Β Β Β Β respond(bytesWritten: number): void;
Β Β Β Β respondWithNewView(view: ArrayBufferView): void;
}

Properties

Methods

Properties

Methods

  • Parameters

    • bytesWritten: number

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamDefaultController.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamDefaultController.html new file mode 100644 index 000000000..0dc9a2850 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamDefaultController.html @@ -0,0 +1,10 @@ +ReadableStreamDefaultController | @wailsio/runtime

Interface ReadableStreamDefaultController<R>

interface ReadableStreamDefaultController<R = any> {
Β Β Β Β desiredSize: null | number;
Β Β Β Β close(): void;
Β Β Β Β enqueue(chunk?: R): void;
Β Β Β Β error(e?: any): void;
}

Type Parameters

  • R = any

Properties

Methods

Properties

desiredSize: null | number

Methods

  • Returns void

  • Parameters

    • Optionalchunk: R

    Returns void

  • Parameters

    • Optionale: any

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamDefaultReader.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamDefaultReader.html new file mode 100644 index 000000000..58669a5e9 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamDefaultReader.html @@ -0,0 +1,10 @@ +ReadableStreamDefaultReader | @wailsio/runtime

Interface ReadableStreamDefaultReader<R>

interface ReadableStreamDefaultReader<R = any> {
Β Β Β Β closed: Promise<undefined>;
Β Β Β Β cancel(reason?: any): Promise<void>;
Β Β Β Β read(): Promise<ReadableStreamReadResult<R>>;
Β Β Β Β releaseLock(): void;
}

Type Parameters

  • R = any

Hierarchy (View Summary)

Properties

Methods

Properties

closed: Promise<undefined>

Methods

  • Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamGenericReader.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamGenericReader.html new file mode 100644 index 000000000..abd2e7267 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamGenericReader.html @@ -0,0 +1,5 @@ +ReadableStreamGenericReader | @wailsio/runtime

Interface ReadableStreamGenericReader

interface ReadableStreamGenericReader {
Β Β Β Β closed: Promise<undefined>;
Β Β Β Β cancel(reason?: any): Promise<void>;
}

Hierarchy (View Summary)

Properties

Methods

Properties

closed: Promise<undefined>

Methods

  • Parameters

    • Optionalreason: any

    Returns Promise<void>

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamGetReaderOptions.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamGetReaderOptions.html new file mode 100644 index 000000000..f71b8b580 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamGetReaderOptions.html @@ -0,0 +1,4 @@ +ReadableStreamGetReaderOptions | @wailsio/runtime

Interface ReadableStreamGetReaderOptions

interface ReadableStreamGetReaderOptions {
Β Β Β Β mode?: "byob";
}

Properties

Properties

mode?: "byob"

Creates a ReadableStreamBYOBReader and locks the stream to the new reader.

+

This call behaves the same way as the no-argument variant, except that it only works on readable byte streams, i.e. streams which were constructed specifically with the ability to handle "bring your own buffer" reading. The returned BYOB reader provides the ability to directly read individual chunks from the stream via its read() method, into developer-supplied buffers, allowing more precise control over allocation.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamReadDoneResult.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamReadDoneResult.html new file mode 100644 index 000000000..d844ef013 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamReadDoneResult.html @@ -0,0 +1,3 @@ +ReadableStreamReadDoneResult | @wailsio/runtime

Interface ReadableStreamReadDoneResult<T>

interface ReadableStreamReadDoneResult<T> {
Β Β Β Β done: true;
Β Β Β Β value?: T;
}

Type Parameters

  • T

Properties

Properties

done: true
value?: T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamReadValueResult.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamReadValueResult.html new file mode 100644 index 000000000..a9b44ff4e --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableStreamReadValueResult.html @@ -0,0 +1,3 @@ +ReadableStreamReadValueResult | @wailsio/runtime

Interface ReadableStreamReadValueResult<T>

interface ReadableStreamReadValueResult<T> {
Β Β Β Β done: false;
Β Β Β Β value: T;
}

Type Parameters

  • T

Properties

Properties

done: false
value: T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableWritablePair.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableWritablePair.html new file mode 100644 index 000000000..33668f35b --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.ReadableWritablePair.html @@ -0,0 +1,5 @@ +ReadableWritablePair | @wailsio/runtime

Interface ReadableWritablePair<R, W>

interface ReadableWritablePair<R = any, W = any> {
Β Β Β Β readable: ReadableStream<R>;
Β Β Β Β writable: WritableStream<W>;
}

Type Parameters

  • R = any
  • W = any

Properties

Properties

readable: ReadableStream<R>
writable: WritableStream<W>

Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use.

+

Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Size.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Size.html new file mode 100644 index 000000000..b9e72c441 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.Size.html @@ -0,0 +1,6 @@ +Size | @wailsio/runtime

A record describing the size of a window.

+
interface Size {
Β Β Β Β height: number;
Β Β Β Β width: number;
}

Properties

Properties

height: number

The height of the window.

+
width: number

The width of the window.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.SourceBuffer.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.SourceBuffer.html new file mode 100644 index 000000000..b68ef8337 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.SourceBuffer.html @@ -0,0 +1,53 @@ +SourceBuffer | @wailsio/runtime

A chunk of media to be passed into an HTMLMediaElement and played, via a MediaSourceΒ object. This can be made up of one or several media segments.

+

MDN Reference

+
interface SourceBuffer {
Β Β Β Β appendWindowEnd: number;
Β Β Β Β appendWindowStart: number;
Β Β Β Β buffered: TimeRanges;
Β Β Β Β mode: AppendMode;
Β Β Β Β onabort: null | (this: SourceBuffer, ev: Event) => any;
Β Β Β Β onerror: null | (this: SourceBuffer, ev: Event) => any;
Β Β Β Β onupdate: null | (this: SourceBuffer, ev: Event) => any;
Β Β Β Β onupdateend: null | (this: SourceBuffer, ev: Event) => any;
Β Β Β Β onupdatestart: null | (this: SourceBuffer, ev: Event) => any;
Β Β Β Β timestampOffset: number;
Β Β Β Β updating: boolean;
Β Β Β Β abort(): void;
Β Β Β Β addEventListener<K extends keyof SourceBufferEventMap>(
Β Β Β Β Β Β Β Β type: K,
Β Β Β Β Β Β Β Β listener: (this: SourceBuffer, ev: SourceBufferEventMap[K]) => any,
Β Β Β Β Β Β Β Β options?: boolean | AddEventListenerOptions,
Β Β Β Β ): void;
Β Β Β Β addEventListener(
Β Β Β Β Β Β Β Β type: string,
Β Β Β Β Β Β Β Β listener: EventListenerOrEventListenerObject,
Β Β Β Β Β Β Β Β options?: boolean | AddEventListenerOptions,
Β Β Β Β ): void;
Β Β Β Β appendBuffer(data: BufferSource): void;
Β Β Β Β changeType(type: string): void;
Β Β Β Β dispatchEvent(event: Event): boolean;
Β Β Β Β remove(start: number, end: number): void;
Β Β Β Β removeEventListener<K extends keyof SourceBufferEventMap>(
Β Β Β Β Β Β Β Β type: K,
Β Β Β Β Β Β Β Β listener: (this: SourceBuffer, ev: SourceBufferEventMap[K]) => any,
Β Β Β Β Β Β Β Β options?: boolean | EventListenerOptions,
Β Β Β Β ): void;
Β Β Β Β removeEventListener(
Β Β Β Β Β Β Β Β type: string,
Β Β Β Β Β Β Β Β listener: EventListenerOrEventListenerObject,
Β Β Β Β Β Β Β Β options?: boolean | EventListenerOptions,
Β Β Β Β ): void;
}

Hierarchy (View Summary)

Properties

appendWindowEnd: number
appendWindowStart: number
buffered: TimeRanges
onabort: null | (this: SourceBuffer, ev: Event) => any
onerror: null | (this: SourceBuffer, ev: Event) => any
onupdate: null | (this: SourceBuffer, ev: Event) => any
onupdateend: null | (this: SourceBuffer, ev: Event) => any
onupdatestart: null | (this: SourceBuffer, ev: Event) => any
timestampOffset: number
updating: boolean

Methods

  • Returns void

  • Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.

    +

    The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture.

    +

    When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET.

    +

    When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in Β§ 2.8 Observing event listeners.

    +

    When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed.

    +

    If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted.

    +

    The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture.

    +

    MDN Reference

    +

    Type Parameters

    Parameters

    Returns void

  • Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.

    +

    The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture.

    +

    When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET.

    +

    When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in Β§ 2.8 Observing event listeners.

    +

    When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed.

    +

    If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted.

    +

    The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture.

    +

    MDN Reference

    +

    Parameters

    Returns void

  • Parameters

    • type: string

    Returns void

  • Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise.

    +

    MDN Reference

    +

    Parameters

    Returns boolean

  • Parameters

    • start: number
    • end: number

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.SourceBufferEventMap.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.SourceBufferEventMap.html new file mode 100644 index 000000000..95e9fee8d --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.SourceBufferEventMap.html @@ -0,0 +1,6 @@ +SourceBufferEventMap | @wailsio/runtime
interface SourceBufferEventMap {
Β Β Β Β abort: Event;
Β Β Β Β error: Event;
Β Β Β Β update: Event;
Β Β Β Β updateend: Event;
Β Β Β Β updatestart: Event;
}

Properties

abort: Event
error: Event
update: Event
updateend: Event
updatestart: Event
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.StreamPipeOptions.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.StreamPipeOptions.html new file mode 100644 index 000000000..ec92b448b --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.StreamPipeOptions.html @@ -0,0 +1,13 @@ +StreamPipeOptions | @wailsio/runtime
interface StreamPipeOptions {
Β Β Β Β preventAbort?: boolean;
Β Β Β Β preventCancel?: boolean;
Β Β Β Β preventClose?: boolean;
Β Β Β Β signal?: AbortSignal;
}

Properties

preventAbort?: boolean
preventCancel?: boolean
preventClose?: boolean

Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered.

+

Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader.

+

Errors and closures of the source and destination streams propagate as follows:

+

An error in this source readable stream will abort destination, unless preventAbort is truthy. The returned promise will be rejected with the source's error, or with any error that occurs during aborting the destination.

+

An error in destination will cancel this source readable stream, unless preventCancel is truthy. The returned promise will be rejected with the destination's error, or with any error that occurs during canceling the source.

+

When this source readable stream closes, destination will be closed, unless preventClose is truthy. The returned promise will be fulfilled once this process completes, unless an error is encountered while closing the destination, in which case it will be rejected with that error.

+

If destination starts out closed or closing, this source readable stream will be canceled, unless preventCancel is true. The returned promise will be rejected with an error indicating piping to a closed stream failed, or with any error that occurs during canceling the source.

+

The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set.

+
signal?: AbortSignal
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.TimeRanges.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.TimeRanges.html new file mode 100644 index 000000000..f93dd4e77 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.TimeRanges.html @@ -0,0 +1,14 @@ +TimeRanges | @wailsio/runtime

Used to represent a set of time ranges, primarily for the purpose of tracking which portions of media have been buffered when loading it for use by the

+

MDN Reference

+
interface TimeRanges {
Β Β Β Β length: number;
Β Β Β Β end(index: number): number;
Β Β Β Β start(index: number): number;
}

Properties

Methods

Properties

length: number

Returns the number of ranges in the object.

+

MDN Reference

+

Methods

  • Returns the time for the end of the range with the given index.

    +

    Throws an "IndexSizeError" DOMException if the index is out of range.

    +

    MDN Reference

    +

    Parameters

    • index: number

    Returns number

  • Returns the time for the start of the range with the given index.

    +

    Throws an "IndexSizeError" DOMException if the index is out of range.

    +

    MDN Reference

    +

    Parameters

    • index: number

    Returns number

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.URL.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.URL.html new file mode 100644 index 000000000..171f46a55 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.URL.html @@ -0,0 +1,30 @@ +URL | @wailsio/runtime

The URLΒ interface represents an object providing static methods used for creating object URLs.

+

MDN Reference

+
interface URL {
Β Β Β Β hash: string;
Β Β Β Β host: string;
Β Β Β Β hostname: string;
Β Β Β Β href: string;
Β Β Β Β origin: string;
Β Β Β Β password: string;
Β Β Β Β pathname: string;
Β Β Β Β port: string;
Β Β Β Β protocol: string;
Β Β Β Β search: string;
Β Β Β Β searchParams: URLSearchParams;
Β Β Β Β username: string;
Β Β Β Β toJSON(): string;
Β Β Β Β toString(): string;
}

Properties

hash: string
host: string
hostname: string
href: string
origin: string
password: string
pathname: string
port: string
protocol: string
search: string
searchParams: URLSearchParams
username: string

Methods

  • Returns string

  • Returns string

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingByteSource.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingByteSource.html new file mode 100644 index 000000000..f4d07fbf6 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingByteSource.html @@ -0,0 +1,6 @@ +UnderlyingByteSource | @wailsio/runtime
interface UnderlyingByteSource {
Β Β Β Β autoAllocateChunkSize?: number;
Β Β Β Β cancel?: UnderlyingSourceCancelCallback;
Β Β Β Β pull?: (
Β Β Β Β Β Β Β Β controller: ReadableByteStreamController,
Β Β Β Β ) => void | PromiseLike<void>;
Β Β Β Β start?: (controller: ReadableByteStreamController) => any;
Β Β Β Β type: "bytes";
}

Properties

autoAllocateChunkSize?: number
pull?: (controller: ReadableByteStreamController) => void | PromiseLike<void>
start?: (controller: ReadableByteStreamController) => any
type: "bytes"
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingDefaultSource.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingDefaultSource.html new file mode 100644 index 000000000..d282894c4 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingDefaultSource.html @@ -0,0 +1,5 @@ +UnderlyingDefaultSource | @wailsio/runtime

Interface UnderlyingDefaultSource<R>

interface UnderlyingDefaultSource<R = any> {
Β Β Β Β cancel?: UnderlyingSourceCancelCallback;
Β Β Β Β pull?: (
Β Β Β Β Β Β Β Β controller: ReadableStreamDefaultController<R>,
Β Β Β Β ) => void | PromiseLike<void>;
Β Β Β Β start?: (controller: ReadableStreamDefaultController<R>) => any;
Β Β Β Β type?: undefined;
}

Type Parameters

  • R = any

Properties

Properties

pull?: (
Β Β Β Β controller: ReadableStreamDefaultController<R>,
) => void | PromiseLike<void>
start?: (controller: ReadableStreamDefaultController<R>) => any
type?: undefined
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSink.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSink.html new file mode 100644 index 000000000..d6da3c8d4 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSink.html @@ -0,0 +1,6 @@ +UnderlyingSink | @wailsio/runtime

Interface UnderlyingSink<W>

interface UnderlyingSink<W = any> {
Β Β Β Β abort?: UnderlyingSinkAbortCallback;
Β Β Β Β close?: UnderlyingSinkCloseCallback;
Β Β Β Β start?: UnderlyingSinkStartCallback;
Β Β Β Β type?: undefined;
Β Β Β Β write?: UnderlyingSinkWriteCallback<W>;
}

Type Parameters

  • W = any

Properties

Properties

type?: undefined
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkAbortCallback.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkAbortCallback.html new file mode 100644 index 000000000..e45d9d445 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkAbortCallback.html @@ -0,0 +1 @@ +UnderlyingSinkAbortCallback | @wailsio/runtime

Interface UnderlyingSinkAbortCallback

  • Parameters

    • Optionalreason: any

    Returns void | PromiseLike<void>

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkCloseCallback.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkCloseCallback.html new file mode 100644 index 000000000..06c0a3608 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkCloseCallback.html @@ -0,0 +1 @@ +UnderlyingSinkCloseCallback | @wailsio/runtime

Interface UnderlyingSinkCloseCallback

  • Returns void | PromiseLike<void>

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkStartCallback.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkStartCallback.html new file mode 100644 index 000000000..b3fa49c5c --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkStartCallback.html @@ -0,0 +1 @@ +UnderlyingSinkStartCallback | @wailsio/runtime

Interface UnderlyingSinkStartCallback

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkWriteCallback.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkWriteCallback.html new file mode 100644 index 000000000..81ad323e5 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSinkWriteCallback.html @@ -0,0 +1 @@ +UnderlyingSinkWriteCallback | @wailsio/runtime

Interface UnderlyingSinkWriteCallback<W>

Type Parameters

  • W
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSource.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSource.html new file mode 100644 index 000000000..c95e2903c --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSource.html @@ -0,0 +1,6 @@ +UnderlyingSource | @wailsio/runtime

Interface UnderlyingSource<R>

interface UnderlyingSource<R = any> {
Β Β Β Β autoAllocateChunkSize?: number;
Β Β Β Β cancel?: UnderlyingSourceCancelCallback;
Β Β Β Β pull?: UnderlyingSourcePullCallback<R>;
Β Β Β Β start?: UnderlyingSourceStartCallback<R>;
Β Β Β Β type?: "bytes";
}

Type Parameters

  • R = any

Properties

autoAllocateChunkSize?: number
type?: "bytes"
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSourceCancelCallback.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSourceCancelCallback.html new file mode 100644 index 000000000..978b039e9 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSourceCancelCallback.html @@ -0,0 +1 @@ +UnderlyingSourceCancelCallback | @wailsio/runtime

Interface UnderlyingSourceCancelCallback

  • Parameters

    • Optionalreason: any

    Returns void | PromiseLike<void>

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSourcePullCallback.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSourcePullCallback.html new file mode 100644 index 000000000..8779e3a64 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSourcePullCallback.html @@ -0,0 +1 @@ +UnderlyingSourcePullCallback | @wailsio/runtime

Interface UnderlyingSourcePullCallback<R>

Type Parameters

  • R
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSourceStartCallback.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSourceStartCallback.html new file mode 100644 index 000000000..acfdef628 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.UnderlyingSourceStartCallback.html @@ -0,0 +1 @@ +UnderlyingSourceStartCallback | @wailsio/runtime

Interface UnderlyingSourceStartCallback<R>

Type Parameters

  • R
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.WritableStream.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.WritableStream.html new file mode 100644 index 000000000..fcaeff279 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.WritableStream.html @@ -0,0 +1,11 @@ +WritableStream | @wailsio/runtime

Interface WritableStream<W>

This Streams API interface providesΒ a standard abstraction for writing streaming data to a destination, known as a sink. This object comes with built-in backpressure and queuing.

+

MDN Reference

+
interface WritableStream<W = any> {
Β Β Β Β locked: boolean;
Β Β Β Β abort(reason?: any): Promise<void>;
Β Β Β Β close(): Promise<void>;
Β Β Β Β getWriter(): WritableStreamDefaultWriter<W>;
}

Type Parameters

  • W = any

Properties

Methods

Properties

locked: boolean

Methods

  • Parameters

    • Optionalreason: any

    Returns Promise<void>

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.WritableStreamDefaultController.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.WritableStreamDefaultController.html new file mode 100644 index 000000000..c51cc6c16 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.WritableStreamDefaultController.html @@ -0,0 +1,7 @@ +WritableStreamDefaultController | @wailsio/runtime

Interface WritableStreamDefaultController

This Streams API interface represents a controller allowing control of aΒ WritableStream's state. When constructing a WritableStream, the underlying sink is given a corresponding WritableStreamDefaultController instance to manipulate.

+

MDN Reference

+
interface WritableStreamDefaultController {
Β Β Β Β signal: AbortSignal;
Β Β Β Β error(e?: any): void;
}

Properties

Methods

Properties

signal: AbortSignal

Methods

  • Parameters

    • Optionale: any

    Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.WritableStreamDefaultWriter.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.WritableStreamDefaultWriter.html new file mode 100644 index 000000000..d0d4f5ed7 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/interfaces/_internal_.WritableStreamDefaultWriter.html @@ -0,0 +1,17 @@ +WritableStreamDefaultWriter | @wailsio/runtime

Interface WritableStreamDefaultWriter<W>

This Streams API interface is the object returned by WritableStream.getWriter() and once created locks the < writer to the WritableStream ensuring that no other streams can write to the underlying sink.

+

MDN Reference

+
interface WritableStreamDefaultWriter<W = any> {
Β Β Β Β closed: Promise<undefined>;
Β Β Β Β desiredSize: null | number;
Β Β Β Β ready: Promise<undefined>;
Β Β Β Β abort(reason?: any): Promise<void>;
Β Β Β Β close(): Promise<void>;
Β Β Β Β releaseLock(): void;
Β Β Β Β write(chunk?: W): Promise<void>;
}

Type Parameters

  • W = any

Properties

closed: Promise<undefined>
desiredSize: null | number
ready: Promise<undefined>

Methods

  • Parameters

    • Optionalreason: any

    Returns Promise<void>

  • Returns void

  • Parameters

    • Optionalchunk: W

    Returns Promise<void>

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules.html index 8f724e236..4248cf28d 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules.html @@ -1,14 +1 @@ -@wailsio/runtime

@wailsio/runtime

Index

Namespaces

Variables

Functions

Generated using TypeDoc

\ No newline at end of file +@wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Application.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Application.html index 789688e85..e6b5d57c5 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Application.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Application.html @@ -1,4 +1 @@ -Application | @wailsio/runtime

Namespace Application

Index

Functions

Generated using TypeDoc

\ No newline at end of file +Application | @wailsio/runtime

Namespace Application

Functions

Hide
Quit
Show
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Browser.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Browser.html index 27f624a65..757bf8469 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Browser.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Browser.html @@ -1,2 +1 @@ -Browser | @wailsio/runtime

Namespace Browser

Index

Functions

Generated using TypeDoc

\ No newline at end of file +Browser | @wailsio/runtime

Namespace Browser

Functions

OpenURL
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Call.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Call.html index 9f266235a..8d06195ea 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Call.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Call.html @@ -1,5 +1 @@ -Call | @wailsio/runtime

Namespace Call

Index

Functions

Generated using TypeDoc

\ No newline at end of file +Call | @wailsio/runtime

Namespace Call

Classes

RuntimeError

Type Aliases

CallOptions

Functions

ByID
ByName
Call
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Clipboard.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Clipboard.html index a256cfcba..1d15bfa14 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Clipboard.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Clipboard.html @@ -1,3 +1 @@ -Clipboard | @wailsio/runtime

Namespace Clipboard

Index

Functions

Generated using TypeDoc

\ No newline at end of file +Clipboard | @wailsio/runtime

Namespace Clipboard

Functions

SetText
Text
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Create.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Create.html deleted file mode 100644 index f1f6f57bb..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Create.html +++ /dev/null @@ -1,7 +0,0 @@ -Create | @wailsio/runtime

Namespace Create

Index

Functions

Generated using TypeDoc

\ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Dialogs.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Dialogs.html index 92c8f0efc..256f30a47 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Dialogs.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Dialogs.html @@ -1,12 +1 @@ -Dialogs | @wailsio/runtime

Generated using TypeDoc

\ No newline at end of file +Dialogs | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Events.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Events.html index 1146df181..cd12fa65d 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Events.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Events.html @@ -1,10 +1 @@ -Events | @wailsio/runtime

Namespace Events

Index

Classes

Variables

Functions

Generated using TypeDoc

\ No newline at end of file +Events | @wailsio/runtime

Namespace Events

Classes

WailsEvent

Type Aliases

Callback

Variables

Types

Functions

Emit
Off
OffAll
On
Once
OnMultiple
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Flags.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Flags.html index 0fd35b980..2004c8692 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Flags.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Flags.html @@ -1,2 +1 @@ -Flags | @wailsio/runtime

Namespace Flags

Index

Functions

Generated using TypeDoc

\ No newline at end of file +Flags | @wailsio/runtime

Namespace Flags

Functions

GetFlag
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Screens.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Screens.html index 006e2f6a6..f25eaabb3 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Screens.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/Screens.html @@ -1,7 +1 @@ -Screens | @wailsio/runtime

Namespace Screens

Index

Interfaces

Functions

Generated using TypeDoc

\ No newline at end of file +Screens | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/System.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/System.html index 30d3be1f1..bc891a645 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/System.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/System.html @@ -1,14 +1 @@ -System | @wailsio/runtime

Generated using TypeDoc

\ No newline at end of file +System | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/WML.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/WML.html index 2ec16bd38..e2d7d3912 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/WML.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/WML.html @@ -1,3 +1 @@ -WML | @wailsio/runtime

Namespace WML

Index

Functions

Generated using TypeDoc

\ No newline at end of file +WML | @wailsio/runtime

Namespace WML

Functions

Enable
Reload
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/_internal_.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/_internal_.html new file mode 100644 index 000000000..9707760fd --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/modules/_internal_.html @@ -0,0 +1 @@ +<internal> | @wailsio/runtime

Classes

Window

Interfaces

AddEventListenerOptions
ArrayBufferView
Blob
BlobPropertyBag
ErrorOptions
Event
EventInit
EventListener
EventListenerObject
EventListenerOptions
EventTarget
Iterable
MediaSource
MediaSourceEventMap
Position
PromiseFulfilledResult
PromiseLike
PromiseRejectedResult
PromiseWithResolvers
QueuingStrategy
QueuingStrategySize
ReadableByteStreamController
ReadableStream
ReadableStreamBYOBReader
ReadableStreamBYOBRequest
ReadableStreamDefaultController
ReadableStreamDefaultReader
ReadableStreamGenericReader
ReadableStreamGetReaderOptions
ReadableStreamReadDoneResult
ReadableStreamReadValueResult
ReadableWritablePair
Size
SourceBuffer
SourceBufferEventMap
StreamPipeOptions
TimeRanges
UnderlyingByteSource
UnderlyingDefaultSource
UnderlyingSink
UnderlyingSinkAbortCallback
UnderlyingSinkCloseCallback
UnderlyingSinkStartCallback
UnderlyingSinkWriteCallback
UnderlyingSource
UnderlyingSourceCancelCallback
UnderlyingSourcePullCallback
UnderlyingSourceStartCallback
URL
WritableStream
WritableStreamDefaultController
WritableStreamDefaultWriter

Type Aliases

AppendMode
ArrayBufferLike
Awaited
BlobPart
BufferSource
CancellablePromiseCanceller
CancellablePromiseExecutor
CancellablePromiseRejector
CancellablePromiseResolver
EndingType
EndOfStreamError
EventListenerOrEventListenerObject
Partial
PromiseSettledResult
ReadableStreamController
ReadableStreamReader
ReadableStreamReadResult
Readonly
ReadyState
Record

Variables

Blob
Event
EventTarget
MediaSource
ReadableByteStreamController
ReadableStream
ReadableStreamBYOBReader
ReadableStreamBYOBRequest
ReadableStreamDefaultController
ReadableStreamDefaultReader
SourceBuffer
TimeRanges
URL
WritableStream
WritableStreamDefaultController
WritableStreamDefaultWriter
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/Call.CallOptions.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/Call.CallOptions.html new file mode 100644 index 000000000..8bb2ba3c9 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/Call.CallOptions.html @@ -0,0 +1,9 @@ +CallOptions | @wailsio/runtime

Type Alias CallOptions

CallOptions:
Β Β Β Β | { args: any[]; methodID: number; methodName?: never }
Β Β Β Β | { args: any[]; methodID?: never; methodName: string }

Holds all required information for a binding call. +May provide either a method ID or a method name, but not both.

+

Type declaration

  • { args: any[]; methodID: number; methodName?: never }
    • args: any[]

      Arguments to be passed into the bound method.

      +
    • methodID: number

      The numeric ID of the bound method to call.

      +
    • OptionalmethodName?: never

      The fully qualified name of the bound method to call.

      +
  • { args: any[]; methodID?: never; methodName: string }
    • args: any[]

      Arguments to be passed into the bound method.

      +
    • OptionalmethodID?: never

      The numeric ID of the bound method to call.

      +
    • methodName: string

      The fully qualified name of the bound method to call.

      +
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/Events.Callback.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/Events.Callback.html new file mode 100644 index 000000000..03c56fff2 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/Events.Callback.html @@ -0,0 +1,2 @@ +Callback | @wailsio/runtime
Callback: (ev: WailsEvent) => void

The type of handlers for a given event.

+

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.AppendMode.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.AppendMode.html new file mode 100644 index 000000000..21279131a --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.AppendMode.html @@ -0,0 +1 @@ +AppendMode | @wailsio/runtime
AppendMode: "segments" | "sequence"
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ArrayBufferLike.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ArrayBufferLike.html new file mode 100644 index 000000000..27533a820 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ArrayBufferLike.html @@ -0,0 +1 @@ +ArrayBufferLike | @wailsio/runtime
ArrayBufferLike: ArrayBufferTypes[keyof ArrayBufferTypes]
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Awaited.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Awaited.html new file mode 100644 index 000000000..7d51c3785 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Awaited.html @@ -0,0 +1,2 @@ +Awaited | @wailsio/runtime
Awaited: T extends null
| undefined
Β Β Β Β ? T
Β Β Β Β : T extends object & { then(onfulfilled: F, ...args: _): any }
Β Β Β Β Β Β Β Β ? F extends (value: infer V, ...args: infer _) => any
Β Β Β Β Β Β Β Β Β Β Β Β ? Awaited<V>
Β Β Β Β Β Β Β Β Β Β Β Β : never
Β Β Β Β Β Β Β Β : T

Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to never. This emulates the behavior of await.

+

Type Parameters

  • T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.BlobPart.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.BlobPart.html new file mode 100644 index 000000000..116be5557 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.BlobPart.html @@ -0,0 +1 @@ +BlobPart | @wailsio/runtime
BlobPart: BufferSource | Blob | string
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.BufferSource.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.BufferSource.html new file mode 100644 index 000000000..c4ad275f4 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.BufferSource.html @@ -0,0 +1 @@ +BufferSource | @wailsio/runtime
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseCanceller.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseCanceller.html new file mode 100644 index 000000000..3258a70d0 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseCanceller.html @@ -0,0 +1 @@ +CancellablePromiseCanceller | @wailsio/runtime

Type Alias CancellablePromiseCanceller

CancellablePromiseCanceller: (cause?: any) => void | PromiseLike<void>

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseExecutor.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseExecutor.html new file mode 100644 index 000000000..85b0f8526 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseExecutor.html @@ -0,0 +1 @@ +CancellablePromiseExecutor | @wailsio/runtime

Type Alias CancellablePromiseExecutor<T>

CancellablePromiseExecutor: (
Β Β Β Β resolve: CancellablePromiseResolver<T>,
Β Β Β Β reject: CancellablePromiseRejector,
) => void

Type Parameters

  • T

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseRejector.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseRejector.html new file mode 100644 index 000000000..b75c628c3 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseRejector.html @@ -0,0 +1 @@ +CancellablePromiseRejector | @wailsio/runtime

Type Alias CancellablePromiseRejector

CancellablePromiseRejector: (reason?: any) => void

Type declaration

    • (reason?: any): void
    • Parameters

      • Optionalreason: any

      Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseResolver.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseResolver.html new file mode 100644 index 000000000..81f2b806b --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.CancellablePromiseResolver.html @@ -0,0 +1 @@ +CancellablePromiseResolver | @wailsio/runtime

Type Alias CancellablePromiseResolver<T>

CancellablePromiseResolver: (
Β Β Β Β value: T | PromiseLike<T> | CancellablePromiseLike<T>,
) => void

Type Parameters

  • T

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.EndOfStreamError.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.EndOfStreamError.html new file mode 100644 index 000000000..df2e1e6b6 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.EndOfStreamError.html @@ -0,0 +1 @@ +EndOfStreamError | @wailsio/runtime
EndOfStreamError: "decode" | "network"
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.EndingType.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.EndingType.html new file mode 100644 index 000000000..97b2a0bfe --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.EndingType.html @@ -0,0 +1 @@ +EndingType | @wailsio/runtime
EndingType: "native" | "transparent"
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.EventListenerOrEventListenerObject.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.EventListenerOrEventListenerObject.html new file mode 100644 index 000000000..c0e2bfa6d --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.EventListenerOrEventListenerObject.html @@ -0,0 +1 @@ +EventListenerOrEventListenerObject | @wailsio/runtime

Type Alias EventListenerOrEventListenerObject

EventListenerOrEventListenerObject: EventListener | EventListenerObject
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Partial.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Partial.html new file mode 100644 index 000000000..c2c89e799 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Partial.html @@ -0,0 +1,2 @@ +Partial | @wailsio/runtime
Partial: { [P in keyof T]?: T[P] }

Make all properties in T optional

+

Type Parameters

  • T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.PromiseSettledResult.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.PromiseSettledResult.html new file mode 100644 index 000000000..c71a7894b --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.PromiseSettledResult.html @@ -0,0 +1 @@ +PromiseSettledResult | @wailsio/runtime

Type Alias PromiseSettledResult<T>

Type Parameters

  • T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadableStreamController.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadableStreamController.html new file mode 100644 index 000000000..03ff250f6 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadableStreamController.html @@ -0,0 +1 @@ +ReadableStreamController | @wailsio/runtime

Type Alias ReadableStreamController<T>

ReadableStreamController:
Β Β Β Β | ReadableStreamDefaultController<T>
Β Β Β Β | ReadableByteStreamController

Type Parameters

  • T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadableStreamReadResult.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadableStreamReadResult.html new file mode 100644 index 000000000..300a4a370 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadableStreamReadResult.html @@ -0,0 +1 @@ +ReadableStreamReadResult | @wailsio/runtime

Type Alias ReadableStreamReadResult<T>

ReadableStreamReadResult:
Β Β Β Β | ReadableStreamReadValueResult<T>
Β Β Β Β | ReadableStreamReadDoneResult<T>

Type Parameters

  • T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadableStreamReader.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadableStreamReader.html new file mode 100644 index 000000000..a6eb2921a --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadableStreamReader.html @@ -0,0 +1 @@ +ReadableStreamReader | @wailsio/runtime

Type Alias ReadableStreamReader<T>

Type Parameters

  • T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Readonly.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Readonly.html new file mode 100644 index 000000000..3f66bd1ba --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Readonly.html @@ -0,0 +1,2 @@ +Readonly | @wailsio/runtime
Readonly: { readonly [P in keyof T]: T[P] }

Make all properties in T readonly

+

Type Parameters

  • T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadyState.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadyState.html new file mode 100644 index 000000000..cab35e873 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.ReadyState.html @@ -0,0 +1 @@ +ReadyState | @wailsio/runtime
ReadyState: "closed" | "ended" | "open"
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Record.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Record.html new file mode 100644 index 000000000..8930f14b9 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/types/_internal_.Record.html @@ -0,0 +1,2 @@ +Record | @wailsio/runtime

Type Alias Record<K, T>

Record: { [P in K]: T }

Construct a type with a set of properties K of type T

+

Type Parameters

  • K extends keyof any
  • T
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/Events.Types.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/Events.Types.html index e8b79328c..c00bf128a 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/Events.Types.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/Events.Types.html @@ -1 +1 @@ -Types | @wailsio/runtime

Variable TypesConst

Types: {
Β Β Β Β Common: {
Β Β Β Β Β Β Β Β ApplicationOpenedWithFile: string;
Β Β Β Β Β Β Β Β ApplicationStarted: string;
Β Β Β Β Β Β Β Β ThemeChanged: string;
Β Β Β Β Β Β Β Β WindowClosing: string;
Β Β Β Β Β Β Β Β WindowDPIChanged: string;
Β Β Β Β Β Β Β Β WindowDidMove: string;
Β Β Β Β Β Β Β Β WindowDidResize: string;
Β Β Β Β Β Β Β Β WindowFilesDropped: string;
Β Β Β Β Β Β Β Β WindowFocus: string;
Β Β Β Β Β Β Β Β WindowFullscreen: string;
Β Β Β Β Β Β Β Β WindowHide: string;
Β Β Β Β Β Β Β Β WindowLostFocus: string;
Β Β Β Β Β Β Β Β WindowMaximise: string;
Β Β Β Β Β Β Β Β WindowMinimise: string;
Β Β Β Β Β Β Β Β WindowRestore: string;
Β Β Β Β Β Β Β Β WindowRuntimeReady: string;
Β Β Β Β Β Β Β Β WindowShow: string;
Β Β Β Β Β Β Β Β WindowUnFullscreen: string;
Β Β Β Β Β Β Β Β WindowUnMaximise: string;
Β Β Β Β Β Β Β Β WindowUnMinimise: string;
Β Β Β Β Β Β Β Β WindowZoom: string;
Β Β Β Β Β Β Β Β WindowZoomIn: string;
Β Β Β Β Β Β Β Β WindowZoomOut: string;
Β Β Β Β Β Β Β Β WindowZoomReset: string;
Β Β Β Β };
Β Β Β Β Linux: {
Β Β Β Β Β Β Β Β ApplicationStartup: string;
Β Β Β Β Β Β Β Β SystemThemeChanged: string;
Β Β Β Β Β Β Β Β WindowDeleteEvent: string;
Β Β Β Β Β Β Β Β WindowDidMove: string;
Β Β Β Β Β Β Β Β WindowDidResize: string;
Β Β Β Β Β Β Β Β WindowFocusIn: string;
Β Β Β Β Β Β Β Β WindowFocusOut: string;
Β Β Β Β Β Β Β Β WindowLoadChanged: string;
Β Β Β Β };
Β Β Β Β Mac: {
Β Β Β Β Β Β Β Β ApplicationDidBecomeActive: string;
Β Β Β Β Β Β Β Β ApplicationDidChangeBackingProperties: string;
Β Β Β Β Β Β Β Β ApplicationDidChangeEffectiveAppearance: string;
Β Β Β Β Β Β Β Β ApplicationDidChangeIcon: string;
Β Β Β Β Β Β Β Β ApplicationDidChangeOcclusionState: string;
Β Β Β Β Β Β Β Β ApplicationDidChangeScreenParameters: string;
Β Β Β Β Β Β Β Β ApplicationDidChangeStatusBarFrame: string;
Β Β Β Β Β Β Β Β ApplicationDidChangeStatusBarOrientation: string;
Β Β Β Β Β Β Β Β ApplicationDidChangeTheme: string;
Β Β Β Β Β Β Β Β ApplicationDidFinishLaunching: string;
Β Β Β Β Β Β Β Β ApplicationDidHide: string;
Β Β Β Β Β Β Β Β ApplicationDidResignActive: string;
Β Β Β Β Β Β Β Β ApplicationDidUnhide: string;
Β Β Β Β Β Β Β Β ApplicationDidUpdate: string;
Β Β Β Β Β Β Β Β ApplicationShouldHandleReopen: string;
Β Β Β Β Β Β Β Β ApplicationWillBecomeActive: string;
Β Β Β Β Β Β Β Β ApplicationWillFinishLaunching: string;
Β Β Β Β Β Β Β Β ApplicationWillHide: string;
Β Β Β Β Β Β Β Β ApplicationWillResignActive: string;
Β Β Β Β Β Β Β Β ApplicationWillTerminate: string;
Β Β Β Β Β Β Β Β ApplicationWillUnhide: string;
Β Β Β Β Β Β Β Β ApplicationWillUpdate: string;
Β Β Β Β Β Β Β Β MenuDidAddItem: string;
Β Β Β Β Β Β Β Β MenuDidBeginTracking: string;
Β Β Β Β Β Β Β Β MenuDidClose: string;
Β Β Β Β Β Β Β Β MenuDidDisplayItem: string;
Β Β Β Β Β Β Β Β MenuDidEndTracking: string;
Β Β Β Β Β Β Β Β MenuDidHighlightItem: string;
Β Β Β Β Β Β Β Β MenuDidOpen: string;
Β Β Β Β Β Β Β Β MenuDidPopUp: string;
Β Β Β Β Β Β Β Β MenuDidRemoveItem: string;
Β Β Β Β Β Β Β Β MenuDidSendAction: string;
Β Β Β Β Β Β Β Β MenuDidSendActionToItem: string;
Β Β Β Β Β Β Β Β MenuDidUpdate: string;
Β Β Β Β Β Β Β Β MenuWillAddItem: string;
Β Β Β Β Β Β Β Β MenuWillBeginTracking: string;
Β Β Β Β Β Β Β Β MenuWillDisplayItem: string;
Β Β Β Β Β Β Β Β MenuWillEndTracking: string;
Β Β Β Β Β Β Β Β MenuWillHighlightItem: string;
Β Β Β Β Β Β Β Β MenuWillOpen: string;
Β Β Β Β Β Β Β Β MenuWillPopUp: string;
Β Β Β Β Β Β Β Β MenuWillRemoveItem: string;
Β Β Β Β Β Β Β Β MenuWillSendAction: string;
Β Β Β Β Β Β Β Β MenuWillSendActionToItem: string;
Β Β Β Β Β Β Β Β MenuWillUpdate: string;
Β Β Β Β Β Β Β Β WebViewDidCommitNavigation: string;
Β Β Β Β Β Β Β Β WebViewDidFinishNavigation: string;
Β Β Β Β Β Β Β Β WebViewDidReceiveServerRedirectForProvisionalNavigation: string;
Β Β Β Β Β Β Β Β WebViewDidStartProvisionalNavigation: string;
Β Β Β Β Β Β Β Β WindowDidBecomeKey: string;
Β Β Β Β Β Β Β Β WindowDidBecomeMain: string;
Β Β Β Β Β Β Β Β WindowDidBeginSheet: string;
Β Β Β Β Β Β Β Β WindowDidChangeAlpha: string;
Β Β Β Β Β Β Β Β WindowDidChangeBackingLocation: string;
Β Β Β Β Β Β Β Β WindowDidChangeBackingProperties: string;
Β Β Β Β Β Β Β Β WindowDidChangeCollectionBehavior: string;
Β Β Β Β Β Β Β Β WindowDidChangeEffectiveAppearance: string;
Β Β Β Β Β Β Β Β WindowDidChangeOcclusionState: string;
Β Β Β Β Β Β Β Β WindowDidChangeOrderingMode: string;
Β Β Β Β Β Β Β Β WindowDidChangeScreen: string;
Β Β Β Β Β Β Β Β WindowDidChangeScreenParameters: string;
Β Β Β Β Β Β Β Β WindowDidChangeScreenProfile: string;
Β Β Β Β Β Β Β Β WindowDidChangeScreenSpace: string;
Β Β Β Β Β Β Β Β WindowDidChangeScreenSpaceProperties: string;
Β Β Β Β Β Β Β Β WindowDidChangeSharingType: string;
Β Β Β Β Β Β Β Β WindowDidChangeSpace: string;
Β Β Β Β Β Β Β Β WindowDidChangeSpaceOrderingMode: string;
Β Β Β Β Β Β Β Β WindowDidChangeTitle: string;
Β Β Β Β Β Β Β Β WindowDidChangeToolbar: string;
Β Β Β Β Β Β Β Β WindowDidDeminiaturize: string;
Β Β Β Β Β Β Β Β WindowDidEndSheet: string;
Β Β Β Β Β Β Β Β WindowDidEnterFullScreen: string;
Β Β Β Β Β Β Β Β WindowDidEnterVersionBrowser: string;
Β Β Β Β Β Β Β Β WindowDidExitFullScreen: string;
Β Β Β Β Β Β Β Β WindowDidExitVersionBrowser: string;
Β Β Β Β Β Β Β Β WindowDidExpose: string;
Β Β Β Β Β Β Β Β WindowDidFocus: string;
Β Β Β Β Β Β Β Β WindowDidMiniaturize: string;
Β Β Β Β Β Β Β Β WindowDidMove: string;
Β Β Β Β Β Β Β Β WindowDidOrderOffScreen: string;
Β Β Β Β Β Β Β Β WindowDidOrderOnScreen: string;
Β Β Β Β Β Β Β Β WindowDidResignKey: string;
Β Β Β Β Β Β Β Β WindowDidResignMain: string;
Β Β Β Β Β Β Β Β WindowDidResize: string;
Β Β Β Β Β Β Β Β WindowDidUpdate: string;
Β Β Β Β Β Β Β Β WindowDidUpdateAlpha: string;
Β Β Β Β Β Β Β Β WindowDidUpdateCollectionBehavior: string;
Β Β Β Β Β Β Β Β WindowDidUpdateCollectionProperties: string;
Β Β Β Β Β Β Β Β WindowDidUpdateShadow: string;
Β Β Β Β Β Β Β Β WindowDidUpdateTitle: string;
Β Β Β Β Β Β Β Β WindowDidUpdateToolbar: string;
Β Β Β Β Β Β Β Β WindowDidZoom: string;
Β Β Β Β Β Β Β Β WindowFileDraggingEntered: string;
Β Β Β Β Β Β Β Β WindowFileDraggingExited: string;
Β Β Β Β Β Β Β Β WindowFileDraggingPerformed: string;
Β Β Β Β Β Β Β Β WindowHide: string;
Β Β Β Β Β Β Β Β WindowMaximise: string;
Β Β Β Β Β Β Β Β WindowMinimise: string;
Β Β Β Β Β Β Β Β WindowShouldClose: string;
Β Β Β Β Β Β Β Β WindowShow: string;
Β Β Β Β Β Β Β Β WindowUnMaximise: string;
Β Β Β Β Β Β Β Β WindowUnMinimise: string;
Β Β Β Β Β Β Β Β WindowWillBecomeKey: string;
Β Β Β Β Β Β Β Β WindowWillBecomeMain: string;
Β Β Β Β Β Β Β Β WindowWillBeginSheet: string;
Β Β Β Β Β Β Β Β WindowWillChangeOrderingMode: string;
Β Β Β Β Β Β Β Β WindowWillClose: string;
Β Β Β Β Β Β Β Β WindowWillDeminiaturize: string;
Β Β Β Β Β Β Β Β WindowWillEnterFullScreen: string;
Β Β Β Β Β Β Β Β WindowWillEnterVersionBrowser: string;
Β Β Β Β Β Β Β Β WindowWillExitFullScreen: string;
Β Β Β Β Β Β Β Β WindowWillExitVersionBrowser: string;
Β Β Β Β Β Β Β Β WindowWillFocus: string;
Β Β Β Β Β Β Β Β WindowWillMiniaturize: string;
Β Β Β Β Β Β Β Β WindowWillMove: string;
Β Β Β Β Β Β Β Β WindowWillOrderOffScreen: string;
Β Β Β Β Β Β Β Β WindowWillOrderOnScreen: string;
Β Β Β Β Β Β Β Β WindowWillResignMain: string;
Β Β Β Β Β Β Β Β WindowWillResize: string;
Β Β Β Β Β Β Β Β WindowWillUnfocus: string;
Β Β Β Β Β Β Β Β WindowWillUpdate: string;
Β Β Β Β Β Β Β Β WindowWillUpdateAlpha: string;
Β Β Β Β Β Β Β Β WindowWillUpdateCollectionBehavior: string;
Β Β Β Β Β Β Β Β WindowWillUpdateCollectionProperties: string;
Β Β Β Β Β Β Β Β WindowWillUpdateShadow: string;
Β Β Β Β Β Β Β Β WindowWillUpdateTitle: string;
Β Β Β Β Β Β Β Β WindowWillUpdateToolbar: string;
Β Β Β Β Β Β Β Β WindowWillUpdateVisibility: string;
Β Β Β Β Β Β Β Β WindowWillUseStandardFrame: string;
Β Β Β Β Β Β Β Β WindowZoomIn: string;
Β Β Β Β Β Β Β Β WindowZoomOut: string;
Β Β Β Β Β Β Β Β WindowZoomReset: string;
Β Β Β Β };
Β Β Β Β Windows: {
Β Β Β Β Β Β Β Β APMPowerSettingChange: string;
Β Β Β Β Β Β Β Β APMPowerStatusChange: string;
Β Β Β Β Β Β Β Β APMResumeAutomatic: string;
Β Β Β Β Β Β Β Β APMResumeSuspend: string;
Β Β Β Β Β Β Β Β APMSuspend: string;
Β Β Β Β Β Β Β Β ApplicationStarted: string;
Β Β Β Β Β Β Β Β SystemThemeChanged: string;
Β Β Β Β Β Β Β Β WebViewNavigationCompleted: string;
Β Β Β Β Β Β Β Β WindowActive: string;
Β Β Β Β Β Β Β Β WindowBackgroundErase: string;
Β Β Β Β Β Β Β Β WindowClickActive: string;
Β Β Β Β Β Β Β Β WindowClosing: string;
Β Β Β Β Β Β Β Β WindowDPIChanged: string;
Β Β Β Β Β Β Β Β WindowDidMove: string;
Β Β Β Β Β Β Β Β WindowDidResize: string;
Β Β Β Β Β Β Β Β WindowDragDrop: string;
Β Β Β Β Β Β Β Β WindowDragEnter: string;
Β Β Β Β Β Β Β Β WindowDragLeave: string;
Β Β Β Β Β Β Β Β WindowDragOver: string;
Β Β Β Β Β Β Β Β WindowEndMove: string;
Β Β Β Β Β Β Β Β WindowEndResize: string;
Β Β Β Β Β Β Β Β WindowFullscreen: string;
Β Β Β Β Β Β Β Β WindowHide: string;
Β Β Β Β Β Β Β Β WindowInactive: string;
Β Β Β Β Β Β Β Β WindowKeyDown: string;
Β Β Β Β Β Β Β Β WindowKeyUp: string;
Β Β Β Β Β Β Β Β WindowKillFocus: string;
Β Β Β Β Β Β Β Β WindowMaximise: string;
Β Β Β Β Β Β Β Β WindowMinimise: string;
Β Β Β Β Β Β Β Β WindowNonClientHit: string;
Β Β Β Β Β Β Β Β WindowNonClientMouseDown: string;
Β Β Β Β Β Β Β Β WindowNonClientMouseLeave: string;
Β Β Β Β Β Β Β Β WindowNonClientMouseMove: string;
Β Β Β Β Β Β Β Β WindowNonClientMouseUp: string;
Β Β Β Β Β Β Β Β WindowPaint: string;
Β Β Β Β Β Β Β Β WindowRestore: string;
Β Β Β Β Β Β Β Β WindowSetFocus: string;
Β Β Β Β Β Β Β Β WindowShow: string;
Β Β Β Β Β Β Β Β WindowStartMove: string;
Β Β Β Β Β Β Β Β WindowStartResize: string;
Β Β Β Β Β Β Β Β WindowUnFullscreen: string;
Β Β Β Β Β Β Β Β WindowUnMaximise: string;
Β Β Β Β Β Β Β Β WindowUnMinimise: string;
Β Β Β Β Β Β Β Β WindowZOrderChanged: string;
Β Β Β Β };
} = EventTypes

Type declaration

  • Common: {
    Β Β Β Β ApplicationOpenedWithFile: string;
    Β Β Β Β ApplicationStarted: string;
    Β Β Β Β ThemeChanged: string;
    Β Β Β Β WindowClosing: string;
    Β Β Β Β WindowDPIChanged: string;
    Β Β Β Β WindowDidMove: string;
    Β Β Β Β WindowDidResize: string;
    Β Β Β Β WindowFilesDropped: string;
    Β Β Β Β WindowFocus: string;
    Β Β Β Β WindowFullscreen: string;
    Β Β Β Β WindowHide: string;
    Β Β Β Β WindowLostFocus: string;
    Β Β Β Β WindowMaximise: string;
    Β Β Β Β WindowMinimise: string;
    Β Β Β Β WindowRestore: string;
    Β Β Β Β WindowRuntimeReady: string;
    Β Β Β Β WindowShow: string;
    Β Β Β Β WindowUnFullscreen: string;
    Β Β Β Β WindowUnMaximise: string;
    Β Β Β Β WindowUnMinimise: string;
    Β Β Β Β WindowZoom: string;
    Β Β Β Β WindowZoomIn: string;
    Β Β Β Β WindowZoomOut: string;
    Β Β Β Β WindowZoomReset: string;
    }
    • ApplicationOpenedWithFile: string
    • ApplicationStarted: string
    • ThemeChanged: string
    • WindowClosing: string
    • WindowDPIChanged: string
    • WindowDidMove: string
    • WindowDidResize: string
    • WindowFilesDropped: string
    • WindowFocus: string
    • WindowFullscreen: string
    • WindowHide: string
    • WindowLostFocus: string
    • WindowMaximise: string
    • WindowMinimise: string
    • WindowRestore: string
    • WindowRuntimeReady: string
    • WindowShow: string
    • WindowUnFullscreen: string
    • WindowUnMaximise: string
    • WindowUnMinimise: string
    • WindowZoom: string
    • WindowZoomIn: string
    • WindowZoomOut: string
    • WindowZoomReset: string
  • Linux: {
    Β Β Β Β ApplicationStartup: string;
    Β Β Β Β SystemThemeChanged: string;
    Β Β Β Β WindowDeleteEvent: string;
    Β Β Β Β WindowDidMove: string;
    Β Β Β Β WindowDidResize: string;
    Β Β Β Β WindowFocusIn: string;
    Β Β Β Β WindowFocusOut: string;
    Β Β Β Β WindowLoadChanged: string;
    }
    • ApplicationStartup: string
    • SystemThemeChanged: string
    • WindowDeleteEvent: string
    • WindowDidMove: string
    • WindowDidResize: string
    • WindowFocusIn: string
    • WindowFocusOut: string
    • WindowLoadChanged: string
  • Mac: {
    Β Β Β Β ApplicationDidBecomeActive: string;
    Β Β Β Β ApplicationDidChangeBackingProperties: string;
    Β Β Β Β ApplicationDidChangeEffectiveAppearance: string;
    Β Β Β Β ApplicationDidChangeIcon: string;
    Β Β Β Β ApplicationDidChangeOcclusionState: string;
    Β Β Β Β ApplicationDidChangeScreenParameters: string;
    Β Β Β Β ApplicationDidChangeStatusBarFrame: string;
    Β Β Β Β ApplicationDidChangeStatusBarOrientation: string;
    Β Β Β Β ApplicationDidChangeTheme: string;
    Β Β Β Β ApplicationDidFinishLaunching: string;
    Β Β Β Β ApplicationDidHide: string;
    Β Β Β Β ApplicationDidResignActive: string;
    Β Β Β Β ApplicationDidUnhide: string;
    Β Β Β Β ApplicationDidUpdate: string;
    Β Β Β Β ApplicationShouldHandleReopen: string;
    Β Β Β Β ApplicationWillBecomeActive: string;
    Β Β Β Β ApplicationWillFinishLaunching: string;
    Β Β Β Β ApplicationWillHide: string;
    Β Β Β Β ApplicationWillResignActive: string;
    Β Β Β Β ApplicationWillTerminate: string;
    Β Β Β Β ApplicationWillUnhide: string;
    Β Β Β Β ApplicationWillUpdate: string;
    Β Β Β Β MenuDidAddItem: string;
    Β Β Β Β MenuDidBeginTracking: string;
    Β Β Β Β MenuDidClose: string;
    Β Β Β Β MenuDidDisplayItem: string;
    Β Β Β Β MenuDidEndTracking: string;
    Β Β Β Β MenuDidHighlightItem: string;
    Β Β Β Β MenuDidOpen: string;
    Β Β Β Β MenuDidPopUp: string;
    Β Β Β Β MenuDidRemoveItem: string;
    Β Β Β Β MenuDidSendAction: string;
    Β Β Β Β MenuDidSendActionToItem: string;
    Β Β Β Β MenuDidUpdate: string;
    Β Β Β Β MenuWillAddItem: string;
    Β Β Β Β MenuWillBeginTracking: string;
    Β Β Β Β MenuWillDisplayItem: string;
    Β Β Β Β MenuWillEndTracking: string;
    Β Β Β Β MenuWillHighlightItem: string;
    Β Β Β Β MenuWillOpen: string;
    Β Β Β Β MenuWillPopUp: string;
    Β Β Β Β MenuWillRemoveItem: string;
    Β Β Β Β MenuWillSendAction: string;
    Β Β Β Β MenuWillSendActionToItem: string;
    Β Β Β Β MenuWillUpdate: string;
    Β Β Β Β WebViewDidCommitNavigation: string;
    Β Β Β Β WebViewDidFinishNavigation: string;
    Β Β Β Β WebViewDidReceiveServerRedirectForProvisionalNavigation: string;
    Β Β Β Β WebViewDidStartProvisionalNavigation: string;
    Β Β Β Β WindowDidBecomeKey: string;
    Β Β Β Β WindowDidBecomeMain: string;
    Β Β Β Β WindowDidBeginSheet: string;
    Β Β Β Β WindowDidChangeAlpha: string;
    Β Β Β Β WindowDidChangeBackingLocation: string;
    Β Β Β Β WindowDidChangeBackingProperties: string;
    Β Β Β Β WindowDidChangeCollectionBehavior: string;
    Β Β Β Β WindowDidChangeEffectiveAppearance: string;
    Β Β Β Β WindowDidChangeOcclusionState: string;
    Β Β Β Β WindowDidChangeOrderingMode: string;
    Β Β Β Β WindowDidChangeScreen: string;
    Β Β Β Β WindowDidChangeScreenParameters: string;
    Β Β Β Β WindowDidChangeScreenProfile: string;
    Β Β Β Β WindowDidChangeScreenSpace: string;
    Β Β Β Β WindowDidChangeScreenSpaceProperties: string;
    Β Β Β Β WindowDidChangeSharingType: string;
    Β Β Β Β WindowDidChangeSpace: string;
    Β Β Β Β WindowDidChangeSpaceOrderingMode: string;
    Β Β Β Β WindowDidChangeTitle: string;
    Β Β Β Β WindowDidChangeToolbar: string;
    Β Β Β Β WindowDidDeminiaturize: string;
    Β Β Β Β WindowDidEndSheet: string;
    Β Β Β Β WindowDidEnterFullScreen: string;
    Β Β Β Β WindowDidEnterVersionBrowser: string;
    Β Β Β Β WindowDidExitFullScreen: string;
    Β Β Β Β WindowDidExitVersionBrowser: string;
    Β Β Β Β WindowDidExpose: string;
    Β Β Β Β WindowDidFocus: string;
    Β Β Β Β WindowDidMiniaturize: string;
    Β Β Β Β WindowDidMove: string;
    Β Β Β Β WindowDidOrderOffScreen: string;
    Β Β Β Β WindowDidOrderOnScreen: string;
    Β Β Β Β WindowDidResignKey: string;
    Β Β Β Β WindowDidResignMain: string;
    Β Β Β Β WindowDidResize: string;
    Β Β Β Β WindowDidUpdate: string;
    Β Β Β Β WindowDidUpdateAlpha: string;
    Β Β Β Β WindowDidUpdateCollectionBehavior: string;
    Β Β Β Β WindowDidUpdateCollectionProperties: string;
    Β Β Β Β WindowDidUpdateShadow: string;
    Β Β Β Β WindowDidUpdateTitle: string;
    Β Β Β Β WindowDidUpdateToolbar: string;
    Β Β Β Β WindowDidZoom: string;
    Β Β Β Β WindowFileDraggingEntered: string;
    Β Β Β Β WindowFileDraggingExited: string;
    Β Β Β Β WindowFileDraggingPerformed: string;
    Β Β Β Β WindowHide: string;
    Β Β Β Β WindowMaximise: string;
    Β Β Β Β WindowMinimise: string;
    Β Β Β Β WindowShouldClose: string;
    Β Β Β Β WindowShow: string;
    Β Β Β Β WindowUnMaximise: string;
    Β Β Β Β WindowUnMinimise: string;
    Β Β Β Β WindowWillBecomeKey: string;
    Β Β Β Β WindowWillBecomeMain: string;
    Β Β Β Β WindowWillBeginSheet: string;
    Β Β Β Β WindowWillChangeOrderingMode: string;
    Β Β Β Β WindowWillClose: string;
    Β Β Β Β WindowWillDeminiaturize: string;
    Β Β Β Β WindowWillEnterFullScreen: string;
    Β Β Β Β WindowWillEnterVersionBrowser: string;
    Β Β Β Β WindowWillExitFullScreen: string;
    Β Β Β Β WindowWillExitVersionBrowser: string;
    Β Β Β Β WindowWillFocus: string;
    Β Β Β Β WindowWillMiniaturize: string;
    Β Β Β Β WindowWillMove: string;
    Β Β Β Β WindowWillOrderOffScreen: string;
    Β Β Β Β WindowWillOrderOnScreen: string;
    Β Β Β Β WindowWillResignMain: string;
    Β Β Β Β WindowWillResize: string;
    Β Β Β Β WindowWillUnfocus: string;
    Β Β Β Β WindowWillUpdate: string;
    Β Β Β Β WindowWillUpdateAlpha: string;
    Β Β Β Β WindowWillUpdateCollectionBehavior: string;
    Β Β Β Β WindowWillUpdateCollectionProperties: string;
    Β Β Β Β WindowWillUpdateShadow: string;
    Β Β Β Β WindowWillUpdateTitle: string;
    Β Β Β Β WindowWillUpdateToolbar: string;
    Β Β Β Β WindowWillUpdateVisibility: string;
    Β Β Β Β WindowWillUseStandardFrame: string;
    Β Β Β Β WindowZoomIn: string;
    Β Β Β Β WindowZoomOut: string;
    Β Β Β Β WindowZoomReset: string;
    }
    • ApplicationDidBecomeActive: string
    • ApplicationDidChangeBackingProperties: string
    • ApplicationDidChangeEffectiveAppearance: string
    • ApplicationDidChangeIcon: string
    • ApplicationDidChangeOcclusionState: string
    • ApplicationDidChangeScreenParameters: string
    • ApplicationDidChangeStatusBarFrame: string
    • ApplicationDidChangeStatusBarOrientation: string
    • ApplicationDidChangeTheme: string
    • ApplicationDidFinishLaunching: string
    • ApplicationDidHide: string
    • ApplicationDidResignActive: string
    • ApplicationDidUnhide: string
    • ApplicationDidUpdate: string
    • ApplicationShouldHandleReopen: string
    • ApplicationWillBecomeActive: string
    • ApplicationWillFinishLaunching: string
    • ApplicationWillHide: string
    • ApplicationWillResignActive: string
    • ApplicationWillTerminate: string
    • ApplicationWillUnhide: string
    • ApplicationWillUpdate: string
    • MenuDidAddItem: string
    • MenuDidBeginTracking: string
    • MenuDidClose: string
    • MenuDidDisplayItem: string
    • MenuDidEndTracking: string
    • MenuDidHighlightItem: string
    • MenuDidOpen: string
    • MenuDidPopUp: string
    • MenuDidRemoveItem: string
    • MenuDidSendAction: string
    • MenuDidSendActionToItem: string
    • MenuDidUpdate: string
    • MenuWillAddItem: string
    • MenuWillBeginTracking: string
    • MenuWillDisplayItem: string
    • MenuWillEndTracking: string
    • MenuWillHighlightItem: string
    • MenuWillOpen: string
    • MenuWillPopUp: string
    • MenuWillRemoveItem: string
    • MenuWillSendAction: string
    • MenuWillSendActionToItem: string
    • MenuWillUpdate: string
    • WebViewDidCommitNavigation: string
    • WebViewDidFinishNavigation: string
    • WebViewDidReceiveServerRedirectForProvisionalNavigation: string
    • WebViewDidStartProvisionalNavigation: string
    • WindowDidBecomeKey: string
    • WindowDidBecomeMain: string
    • WindowDidBeginSheet: string
    • WindowDidChangeAlpha: string
    • WindowDidChangeBackingLocation: string
    • WindowDidChangeBackingProperties: string
    • WindowDidChangeCollectionBehavior: string
    • WindowDidChangeEffectiveAppearance: string
    • WindowDidChangeOcclusionState: string
    • WindowDidChangeOrderingMode: string
    • WindowDidChangeScreen: string
    • WindowDidChangeScreenParameters: string
    • WindowDidChangeScreenProfile: string
    • WindowDidChangeScreenSpace: string
    • WindowDidChangeScreenSpaceProperties: string
    • WindowDidChangeSharingType: string
    • WindowDidChangeSpace: string
    • WindowDidChangeSpaceOrderingMode: string
    • WindowDidChangeTitle: string
    • WindowDidChangeToolbar: string
    • WindowDidDeminiaturize: string
    • WindowDidEndSheet: string
    • WindowDidEnterFullScreen: string
    • WindowDidEnterVersionBrowser: string
    • WindowDidExitFullScreen: string
    • WindowDidExitVersionBrowser: string
    • WindowDidExpose: string
    • WindowDidFocus: string
    • WindowDidMiniaturize: string
    • WindowDidMove: string
    • WindowDidOrderOffScreen: string
    • WindowDidOrderOnScreen: string
    • WindowDidResignKey: string
    • WindowDidResignMain: string
    • WindowDidResize: string
    • WindowDidUpdate: string
    • WindowDidUpdateAlpha: string
    • WindowDidUpdateCollectionBehavior: string
    • WindowDidUpdateCollectionProperties: string
    • WindowDidUpdateShadow: string
    • WindowDidUpdateTitle: string
    • WindowDidUpdateToolbar: string
    • WindowDidZoom: string
    • WindowFileDraggingEntered: string
    • WindowFileDraggingExited: string
    • WindowFileDraggingPerformed: string
    • WindowHide: string
    • WindowMaximise: string
    • WindowMinimise: string
    • WindowShouldClose: string
    • WindowShow: string
    • WindowUnMaximise: string
    • WindowUnMinimise: string
    • WindowWillBecomeKey: string
    • WindowWillBecomeMain: string
    • WindowWillBeginSheet: string
    • WindowWillChangeOrderingMode: string
    • WindowWillClose: string
    • WindowWillDeminiaturize: string
    • WindowWillEnterFullScreen: string
    • WindowWillEnterVersionBrowser: string
    • WindowWillExitFullScreen: string
    • WindowWillExitVersionBrowser: string
    • WindowWillFocus: string
    • WindowWillMiniaturize: string
    • WindowWillMove: string
    • WindowWillOrderOffScreen: string
    • WindowWillOrderOnScreen: string
    • WindowWillResignMain: string
    • WindowWillResize: string
    • WindowWillUnfocus: string
    • WindowWillUpdate: string
    • WindowWillUpdateAlpha: string
    • WindowWillUpdateCollectionBehavior: string
    • WindowWillUpdateCollectionProperties: string
    • WindowWillUpdateShadow: string
    • WindowWillUpdateTitle: string
    • WindowWillUpdateToolbar: string
    • WindowWillUpdateVisibility: string
    • WindowWillUseStandardFrame: string
    • WindowZoomIn: string
    • WindowZoomOut: string
    • WindowZoomReset: string
  • Windows: {
    Β Β Β Β APMPowerSettingChange: string;
    Β Β Β Β APMPowerStatusChange: string;
    Β Β Β Β APMResumeAutomatic: string;
    Β Β Β Β APMResumeSuspend: string;
    Β Β Β Β APMSuspend: string;
    Β Β Β Β ApplicationStarted: string;
    Β Β Β Β SystemThemeChanged: string;
    Β Β Β Β WebViewNavigationCompleted: string;
    Β Β Β Β WindowActive: string;
    Β Β Β Β WindowBackgroundErase: string;
    Β Β Β Β WindowClickActive: string;
    Β Β Β Β WindowClosing: string;
    Β Β Β Β WindowDPIChanged: string;
    Β Β Β Β WindowDidMove: string;
    Β Β Β Β WindowDidResize: string;
    Β Β Β Β WindowDragDrop: string;
    Β Β Β Β WindowDragEnter: string;
    Β Β Β Β WindowDragLeave: string;
    Β Β Β Β WindowDragOver: string;
    Β Β Β Β WindowEndMove: string;
    Β Β Β Β WindowEndResize: string;
    Β Β Β Β WindowFullscreen: string;
    Β Β Β Β WindowHide: string;
    Β Β Β Β WindowInactive: string;
    Β Β Β Β WindowKeyDown: string;
    Β Β Β Β WindowKeyUp: string;
    Β Β Β Β WindowKillFocus: string;
    Β Β Β Β WindowMaximise: string;
    Β Β Β Β WindowMinimise: string;
    Β Β Β Β WindowNonClientHit: string;
    Β Β Β Β WindowNonClientMouseDown: string;
    Β Β Β Β WindowNonClientMouseLeave: string;
    Β Β Β Β WindowNonClientMouseMove: string;
    Β Β Β Β WindowNonClientMouseUp: string;
    Β Β Β Β WindowPaint: string;
    Β Β Β Β WindowRestore: string;
    Β Β Β Β WindowSetFocus: string;
    Β Β Β Β WindowShow: string;
    Β Β Β Β WindowStartMove: string;
    Β Β Β Β WindowStartResize: string;
    Β Β Β Β WindowUnFullscreen: string;
    Β Β Β Β WindowUnMaximise: string;
    Β Β Β Β WindowUnMinimise: string;
    Β Β Β Β WindowZOrderChanged: string;
    }
    • APMPowerSettingChange: string
    • APMPowerStatusChange: string
    • APMResumeAutomatic: string
    • APMResumeSuspend: string
    • APMSuspend: string
    • ApplicationStarted: string
    • SystemThemeChanged: string
    • WebViewNavigationCompleted: string
    • WindowActive: string
    • WindowBackgroundErase: string
    • WindowClickActive: string
    • WindowClosing: string
    • WindowDPIChanged: string
    • WindowDidMove: string
    • WindowDidResize: string
    • WindowDragDrop: string
    • WindowDragEnter: string
    • WindowDragLeave: string
    • WindowDragOver: string
    • WindowEndMove: string
    • WindowEndResize: string
    • WindowFullscreen: string
    • WindowHide: string
    • WindowInactive: string
    • WindowKeyDown: string
    • WindowKeyUp: string
    • WindowKillFocus: string
    • WindowMaximise: string
    • WindowMinimise: string
    • WindowNonClientHit: string
    • WindowNonClientMouseDown: string
    • WindowNonClientMouseLeave: string
    • WindowNonClientMouseMove: string
    • WindowNonClientMouseUp: string
    • WindowPaint: string
    • WindowRestore: string
    • WindowSetFocus: string
    • WindowShow: string
    • WindowStartMove: string
    • WindowStartResize: string
    • WindowUnFullscreen: string
    • WindowUnMaximise: string
    • WindowUnMinimise: string
    • WindowZOrderChanged: string

Generated using TypeDoc

\ No newline at end of file +Types | @wailsio/runtime

Variable TypesConst

Types: Readonly<
Β Β Β Β {
Β Β Β Β Β Β Β Β Common: Readonly<
Β Β Β Β Β Β Β Β Β Β Β Β {
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationLaunchedWithUrl: "common:ApplicationLaunchedWithUrl";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationOpenedWithFile: "common:ApplicationOpenedWithFile";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationStarted: "common:ApplicationStarted";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ThemeChanged: "common:ThemeChanged";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowClosing: "common:WindowClosing";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidMove: "common:WindowDidMove";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidResize: "common:WindowDidResize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDPIChanged: "common:WindowDPIChanged";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowFilesDropped: "common:WindowFilesDropped";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowFocus: "common:WindowFocus";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowFullscreen: "common:WindowFullscreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowHide: "common:WindowHide";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowLostFocus: "common:WindowLostFocus";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowMaximise: "common:WindowMaximise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowMinimise: "common:WindowMinimise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowRestore: "common:WindowRestore";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowRuntimeReady: "common:WindowRuntimeReady";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowShow: "common:WindowShow";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowToggleFrameless: "common:WindowToggleFrameless";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowUnFullscreen: "common:WindowUnFullscreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowUnMaximise: "common:WindowUnMaximise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowUnMinimise: "common:WindowUnMinimise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowZoom: "common:WindowZoom";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowZoomIn: "common:WindowZoomIn";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowZoomOut: "common:WindowZoomOut";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowZoomReset: "common:WindowZoomReset";
Β Β Β Β Β Β Β Β Β Β Β Β },
Β Β Β Β Β Β Β Β >;
Β Β Β Β Β Β Β Β Linux: Readonly<
Β Β Β Β Β Β Β Β Β Β Β Β {
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationStartup: "linux:ApplicationStartup";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β SystemThemeChanged: "linux:SystemThemeChanged";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDeleteEvent: "linux:WindowDeleteEvent";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidMove: "linux:WindowDidMove";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidResize: "linux:WindowDidResize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowFocusIn: "linux:WindowFocusIn";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowFocusOut: "linux:WindowFocusOut";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowLoadChanged: "linux:WindowLoadChanged";
Β Β Β Β Β Β Β Β Β Β Β Β },
Β Β Β Β Β Β Β Β >;
Β Β Β Β Β Β Β Β Mac: Readonly<
Β Β Β Β Β Β Β Β Β Β Β Β {
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidBecomeActive: "mac:ApplicationDidBecomeActive";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidChangeBackingProperties: "mac:ApplicationDidChangeBackingProperties";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidChangeEffectiveAppearance: "mac:ApplicationDidChangeEffectiveAppearance";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidChangeIcon: "mac:ApplicationDidChangeIcon";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidChangeOcclusionState: "mac:ApplicationDidChangeOcclusionState";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidChangeScreenParameters: "mac:ApplicationDidChangeScreenParameters";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidChangeStatusBarFrame: "mac:ApplicationDidChangeStatusBarFrame";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidChangeStatusBarOrientation: "mac:ApplicationDidChangeStatusBarOrientation";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidChangeTheme: "mac:ApplicationDidChangeTheme";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidFinishLaunching: "mac:ApplicationDidFinishLaunching";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidHide: "mac:ApplicationDidHide";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidResignActive: "mac:ApplicationDidResignActive";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidUnhide: "mac:ApplicationDidUnhide";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationDidUpdate: "mac:ApplicationDidUpdate";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationShouldHandleReopen: "mac:ApplicationShouldHandleReopen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationWillBecomeActive: "mac:ApplicationWillBecomeActive";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationWillFinishLaunching: "mac:ApplicationWillFinishLaunching";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationWillHide: "mac:ApplicationWillHide";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationWillResignActive: "mac:ApplicationWillResignActive";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationWillTerminate: "mac:ApplicationWillTerminate";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationWillUnhide: "mac:ApplicationWillUnhide";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationWillUpdate: "mac:ApplicationWillUpdate";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidAddItem: "mac:MenuDidAddItem";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidBeginTracking: "mac:MenuDidBeginTracking";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidClose: "mac:MenuDidClose";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidDisplayItem: "mac:MenuDidDisplayItem";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidEndTracking: "mac:MenuDidEndTracking";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidHighlightItem: "mac:MenuDidHighlightItem";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidOpen: "mac:MenuDidOpen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidPopUp: "mac:MenuDidPopUp";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidRemoveItem: "mac:MenuDidRemoveItem";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidSendAction: "mac:MenuDidSendAction";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidSendActionToItem: "mac:MenuDidSendActionToItem";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuDidUpdate: "mac:MenuDidUpdate";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillAddItem: "mac:MenuWillAddItem";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillBeginTracking: "mac:MenuWillBeginTracking";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillDisplayItem: "mac:MenuWillDisplayItem";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillEndTracking: "mac:MenuWillEndTracking";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillHighlightItem: "mac:MenuWillHighlightItem";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillOpen: "mac:MenuWillOpen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillPopUp: "mac:MenuWillPopUp";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillRemoveItem: "mac:MenuWillRemoveItem";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillSendAction: "mac:MenuWillSendAction";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillSendActionToItem: "mac:MenuWillSendActionToItem";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β MenuWillUpdate: "mac:MenuWillUpdate";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WebViewDidCommitNavigation: "mac:WebViewDidCommitNavigation";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WebViewDidFinishNavigation: "mac:WebViewDidFinishNavigation";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WebViewDidReceiveServerRedirectForProvisionalNavigation: "mac:WebViewDidReceiveServerRedirectForProvisionalNavigation";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WebViewDidStartProvisionalNavigation: "mac:WebViewDidStartProvisionalNavigation";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidBecomeKey: "mac:WindowDidBecomeKey";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidBecomeMain: "mac:WindowDidBecomeMain";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidBeginSheet: "mac:WindowDidBeginSheet";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeAlpha: "mac:WindowDidChangeAlpha";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeBackingLocation: "mac:WindowDidChangeBackingLocation";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeBackingProperties: "mac:WindowDidChangeBackingProperties";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeCollectionBehavior: "mac:WindowDidChangeCollectionBehavior";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeEffectiveAppearance: "mac:WindowDidChangeEffectiveAppearance";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeOcclusionState: "mac:WindowDidChangeOcclusionState";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeOrderingMode: "mac:WindowDidChangeOrderingMode";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeScreen: "mac:WindowDidChangeScreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeScreenParameters: "mac:WindowDidChangeScreenParameters";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeScreenProfile: "mac:WindowDidChangeScreenProfile";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeScreenSpace: "mac:WindowDidChangeScreenSpace";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeScreenSpaceProperties: "mac:WindowDidChangeScreenSpaceProperties";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeSharingType: "mac:WindowDidChangeSharingType";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeSpace: "mac:WindowDidChangeSpace";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeSpaceOrderingMode: "mac:WindowDidChangeSpaceOrderingMode";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeTitle: "mac:WindowDidChangeTitle";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidChangeToolbar: "mac:WindowDidChangeToolbar";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidDeminiaturize: "mac:WindowDidDeminiaturize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidEndSheet: "mac:WindowDidEndSheet";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidEnterFullScreen: "mac:WindowDidEnterFullScreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidEnterVersionBrowser: "mac:WindowDidEnterVersionBrowser";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidExitFullScreen: "mac:WindowDidExitFullScreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidExitVersionBrowser: "mac:WindowDidExitVersionBrowser";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidExpose: "mac:WindowDidExpose";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidFocus: "mac:WindowDidFocus";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidMiniaturize: "mac:WindowDidMiniaturize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidMove: "mac:WindowDidMove";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidOrderOffScreen: "mac:WindowDidOrderOffScreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidOrderOnScreen: "mac:WindowDidOrderOnScreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidResignKey: "mac:WindowDidResignKey";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidResignMain: "mac:WindowDidResignMain";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidResize: "mac:WindowDidResize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidUpdate: "mac:WindowDidUpdate";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidUpdateAlpha: "mac:WindowDidUpdateAlpha";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidUpdateCollectionBehavior: "mac:WindowDidUpdateCollectionBehavior";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidUpdateCollectionProperties: "mac:WindowDidUpdateCollectionProperties";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidUpdateShadow: "mac:WindowDidUpdateShadow";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidUpdateTitle: "mac:WindowDidUpdateTitle";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidUpdateToolbar: "mac:WindowDidUpdateToolbar";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidZoom: "mac:WindowDidZoom";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowFileDraggingEntered: "mac:WindowFileDraggingEntered";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowFileDraggingExited: "mac:WindowFileDraggingExited";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowFileDraggingPerformed: "mac:WindowFileDraggingPerformed";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowHide: "mac:WindowHide";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowMaximise: "mac:WindowMaximise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowMinimise: "mac:WindowMinimise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowShouldClose: "mac:WindowShouldClose";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowShow: "mac:WindowShow";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowUnMaximise: "mac:WindowUnMaximise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowUnMinimise: "mac:WindowUnMinimise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillBecomeKey: "mac:WindowWillBecomeKey";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillBecomeMain: "mac:WindowWillBecomeMain";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillBeginSheet: "mac:WindowWillBeginSheet";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillChangeOrderingMode: "mac:WindowWillChangeOrderingMode";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillClose: "mac:WindowWillClose";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillDeminiaturize: "mac:WindowWillDeminiaturize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillEnterFullScreen: "mac:WindowWillEnterFullScreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillEnterVersionBrowser: "mac:WindowWillEnterVersionBrowser";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillExitFullScreen: "mac:WindowWillExitFullScreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillExitVersionBrowser: "mac:WindowWillExitVersionBrowser";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillFocus: "mac:WindowWillFocus";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillMiniaturize: "mac:WindowWillMiniaturize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillMove: "mac:WindowWillMove";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillOrderOffScreen: "mac:WindowWillOrderOffScreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillOrderOnScreen: "mac:WindowWillOrderOnScreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillResignMain: "mac:WindowWillResignMain";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillResize: "mac:WindowWillResize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillUnfocus: "mac:WindowWillUnfocus";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillUpdate: "mac:WindowWillUpdate";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillUpdateAlpha: "mac:WindowWillUpdateAlpha";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillUpdateCollectionBehavior: "mac:WindowWillUpdateCollectionBehavior";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillUpdateCollectionProperties: "mac:WindowWillUpdateCollectionProperties";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillUpdateShadow: "mac:WindowWillUpdateShadow";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillUpdateTitle: "mac:WindowWillUpdateTitle";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillUpdateToolbar: "mac:WindowWillUpdateToolbar";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillUpdateVisibility: "mac:WindowWillUpdateVisibility";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowWillUseStandardFrame: "mac:WindowWillUseStandardFrame";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowZoomIn: "mac:WindowZoomIn";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowZoomOut: "mac:WindowZoomOut";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowZoomReset: "mac:WindowZoomReset";
Β Β Β Β Β Β Β Β Β Β Β Β },
Β Β Β Β Β Β Β Β >;
Β Β Β Β Β Β Β Β Windows: Readonly<
Β Β Β Β Β Β Β Β Β Β Β Β {
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β APMPowerSettingChange: "windows:APMPowerSettingChange";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β APMPowerStatusChange: "windows:APMPowerStatusChange";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β APMResumeAutomatic: "windows:APMResumeAutomatic";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β APMResumeSuspend: "windows:APMResumeSuspend";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β APMSuspend: "windows:APMSuspend";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β ApplicationStarted: "windows:ApplicationStarted";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β SystemThemeChanged: "windows:SystemThemeChanged";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WebViewNavigationCompleted: "windows:WebViewNavigationCompleted";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowActive: "windows:WindowActive";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowBackgroundErase: "windows:WindowBackgroundErase";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowClickActive: "windows:WindowClickActive";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowClosing: "windows:WindowClosing";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidMove: "windows:WindowDidMove";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDidResize: "windows:WindowDidResize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDPIChanged: "windows:WindowDPIChanged";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDragDrop: "windows:WindowDragDrop";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDragEnter: "windows:WindowDragEnter";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDragLeave: "windows:WindowDragLeave";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowDragOver: "windows:WindowDragOver";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowEndMove: "windows:WindowEndMove";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowEndResize: "windows:WindowEndResize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowFullscreen: "windows:WindowFullscreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowHide: "windows:WindowHide";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowInactive: "windows:WindowInactive";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowKeyDown: "windows:WindowKeyDown";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowKeyUp: "windows:WindowKeyUp";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowKillFocus: "windows:WindowKillFocus";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowMaximise: "windows:WindowMaximise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowMinimise: "windows:WindowMinimise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowNonClientHit: "windows:WindowNonClientHit";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowNonClientMouseDown: "windows:WindowNonClientMouseDown";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowNonClientMouseLeave: "windows:WindowNonClientMouseLeave";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowNonClientMouseMove: "windows:WindowNonClientMouseMove";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowNonClientMouseUp: "windows:WindowNonClientMouseUp";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowPaint: "windows:WindowPaint";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowRestore: "windows:WindowRestore";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowSetFocus: "windows:WindowSetFocus";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowShow: "windows:WindowShow";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowStartMove: "windows:WindowStartMove";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowStartResize: "windows:WindowStartResize";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowUnFullscreen: "windows:WindowUnFullscreen";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowUnMaximise: "windows:WindowUnMaximise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowUnMinimise: "windows:WindowUnMinimise";
Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β WindowZOrderChanged: "windows:WindowZOrderChanged";
Β Β Β Β Β Β Β Β Β Β Β Β },
Β Β Β Β Β Β Β Β >;
Β Β Β Β },
> = ...
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/Window.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/Window.html index f638162a6..e83218ac7 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/Window.html +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/Window.html @@ -1,2 +1,2 @@ -Window | @wailsio/runtime

Variable WindowConst

Window: Window = ...

The window within which the script is running.

-

Generated using TypeDoc

\ No newline at end of file +Window | @wailsio/runtime

Variable WindowConst

Window: Window = ...

The window within which the script is running.

+
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.Blob-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.Blob-1.html new file mode 100644 index 000000000..7a6dca0a7 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.Blob-1.html @@ -0,0 +1 @@ +Blob | @wailsio/runtime
Blob: {
Β Β Β Β prototype: Blob;
Β Β Β Β new (blobParts?: BlobPart[], options?: BlobPropertyBag): Blob;
}

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.Event-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.Event-1.html new file mode 100644 index 000000000..10a91c579 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.Event-1.html @@ -0,0 +1 @@ +Event | @wailsio/runtime
Event: {
Β Β Β Β AT_TARGET: 2;
Β Β Β Β BUBBLING_PHASE: 3;
Β Β Β Β CAPTURING_PHASE: 1;
Β Β Β Β NONE: 0;
Β Β Β Β prototype: Event;
Β Β Β Β new (type: string, eventInitDict?: EventInit): Event;
}

Type declaration

  • ReadonlyAT_TARGET: 2
  • ReadonlyBUBBLING_PHASE: 3
  • ReadonlyCAPTURING_PHASE: 1
  • ReadonlyNONE: 0
  • prototype: Event
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.EventTarget-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.EventTarget-1.html new file mode 100644 index 000000000..b260ea11e --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.EventTarget-1.html @@ -0,0 +1 @@ +EventTarget | @wailsio/runtime
EventTarget: { prototype: EventTarget; new (): EventTarget }

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.MediaSource-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.MediaSource-1.html new file mode 100644 index 000000000..ff20149ac --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.MediaSource-1.html @@ -0,0 +1,3 @@ +MediaSource | @wailsio/runtime
MediaSource: {
Β Β Β Β canConstructInDedicatedWorker: boolean;
Β Β Β Β prototype: MediaSource;
Β Β Β Β isTypeSupported(type: string): boolean;
Β Β Β Β new (): MediaSource;
}

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableByteStreamController-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableByteStreamController-1.html new file mode 100644 index 000000000..03abf22d0 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableByteStreamController-1.html @@ -0,0 +1 @@ +ReadableByteStreamController | @wailsio/runtime

Variable ReadableByteStreamController

ReadableByteStreamController: {
Β Β Β Β prototype: ReadableByteStreamController;
Β Β Β Β new (): ReadableByteStreamController;
}
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStream-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStream-1.html new file mode 100644 index 000000000..ff895225a --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStream-1.html @@ -0,0 +1 @@ +ReadableStream | @wailsio/runtime
ReadableStream: {
Β Β Β Β prototype: ReadableStream;
Β Β Β Β new (
Β Β Β Β Β Β Β Β underlyingSource: UnderlyingByteSource,
Β Β Β Β Β Β Β Β strategy?: { highWaterMark?: number },
Β Β Β Β ): ReadableStream<Uint8Array<ArrayBufferLike>>;
Β Β Β Β new <R = any>(
Β Β Β Β Β Β Β Β underlyingSource: UnderlyingDefaultSource<R>,
Β Β Β Β Β Β Β Β strategy?: QueuingStrategy<R>,
Β Β Β Β ): ReadableStream<R>;
Β Β Β Β new <R = any>(
Β Β Β Β Β Β Β Β underlyingSource?: UnderlyingSource<R>,
Β Β Β Β Β Β Β Β strategy?: QueuingStrategy<R>,
Β Β Β Β ): ReadableStream<R>;
}

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamBYOBReader-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamBYOBReader-1.html new file mode 100644 index 000000000..97d078bc2 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamBYOBReader-1.html @@ -0,0 +1 @@ +ReadableStreamBYOBReader | @wailsio/runtime
ReadableStreamBYOBReader: {
Β Β Β Β prototype: ReadableStreamBYOBReader;
Β Β Β Β new (
Β Β Β Β Β Β Β Β stream: ReadableStream<Uint8Array<ArrayBufferLike>>,
Β Β Β Β ): ReadableStreamBYOBReader;
}
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamBYOBRequest-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamBYOBRequest-1.html new file mode 100644 index 000000000..bcda606c6 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamBYOBRequest-1.html @@ -0,0 +1 @@ +ReadableStreamBYOBRequest | @wailsio/runtime

Variable ReadableStreamBYOBRequest

ReadableStreamBYOBRequest: {
Β Β Β Β prototype: ReadableStreamBYOBRequest;
Β Β Β Β new (): ReadableStreamBYOBRequest;
}
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamDefaultController-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamDefaultController-1.html new file mode 100644 index 000000000..b96ea63f8 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamDefaultController-1.html @@ -0,0 +1 @@ +ReadableStreamDefaultController | @wailsio/runtime

Variable ReadableStreamDefaultController

ReadableStreamDefaultController: {
Β Β Β Β prototype: ReadableStreamDefaultController;
Β Β Β Β new (): ReadableStreamDefaultController;
}
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamDefaultReader-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamDefaultReader-1.html new file mode 100644 index 000000000..a7d746f8d --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.ReadableStreamDefaultReader-1.html @@ -0,0 +1 @@ +ReadableStreamDefaultReader | @wailsio/runtime

Variable ReadableStreamDefaultReader

ReadableStreamDefaultReader: {
Β Β Β Β prototype: ReadableStreamDefaultReader;
Β Β Β Β new <R = any>(stream: ReadableStream<R>): ReadableStreamDefaultReader<R>;
}

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.SourceBuffer-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.SourceBuffer-1.html new file mode 100644 index 000000000..ca7e84af9 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.SourceBuffer-1.html @@ -0,0 +1 @@ +SourceBuffer | @wailsio/runtime
SourceBuffer: { prototype: SourceBuffer; new (): SourceBuffer }

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.TimeRanges-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.TimeRanges-1.html new file mode 100644 index 000000000..9d9e6fa1b --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.TimeRanges-1.html @@ -0,0 +1 @@ +TimeRanges | @wailsio/runtime
TimeRanges: { prototype: TimeRanges; new (): TimeRanges }

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.URL-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.URL-1.html new file mode 100644 index 000000000..a0f9adf7d --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.URL-1.html @@ -0,0 +1,5 @@ +URL | @wailsio/runtime
URL: {
Β Β Β Β prototype: URL;
Β Β Β Β canParse(url: string | URL, base?: string | URL): boolean;
Β Β Β Β createObjectURL(obj: Blob | MediaSource): string;
Β Β Β Β parse(url: string | URL, base?: string | URL): null | URL;
Β Β Β Β revokeObjectURL(url: string): void;
Β Β Β Β new (url: string | URL, base?: string | URL): URL;
}

Type declaration

    • new (url: string | URL, base?: string | URL): URL
    • Parameters

      • url: string | URL
      • Optionalbase: string | URL

      Returns URL

  • prototype: URL
  • canParse:function
    • Parameters

      • url: string | URL
      • Optionalbase: string | URL

      Returns boolean

  • createObjectURL:function
  • parse:function
    • Parameters

      • url: string | URL
      • Optionalbase: string | URL

      Returns null | URL

  • revokeObjectURL:function
    • Parameters

      • url: string

      Returns void

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.WritableStream-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.WritableStream-1.html new file mode 100644 index 000000000..e31a06f97 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.WritableStream-1.html @@ -0,0 +1 @@ +WritableStream | @wailsio/runtime
WritableStream: {
Β Β Β Β prototype: WritableStream;
Β Β Β Β new <W = any>(
Β Β Β Β Β Β Β Β underlyingSink?: UnderlyingSink<W>,
Β Β Β Β Β Β Β Β strategy?: QueuingStrategy<W>,
Β Β Β Β ): WritableStream<W>;
}

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.WritableStreamDefaultController-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.WritableStreamDefaultController-1.html new file mode 100644 index 000000000..98ec3ec27 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.WritableStreamDefaultController-1.html @@ -0,0 +1 @@ +WritableStreamDefaultController | @wailsio/runtime

Variable WritableStreamDefaultController

WritableStreamDefaultController: {
Β Β Β Β prototype: WritableStreamDefaultController;
Β Β Β Β new (): WritableStreamDefaultController;
}
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.WritableStreamDefaultWriter-1.html b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.WritableStreamDefaultWriter-1.html new file mode 100644 index 000000000..286db85ef --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/docs/variables/_internal_.WritableStreamDefaultWriter-1.html @@ -0,0 +1 @@ +WritableStreamDefaultWriter | @wailsio/runtime

Variable WritableStreamDefaultWriter

WritableStreamDefaultWriter: {
Β Β Β Β prototype: WritableStreamDefaultWriter;
Β Β Β Β new <W = any>(stream: WritableStream<W>): WritableStreamDefaultWriter<W>;
}

Type declaration

diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/package-lock.json b/v3/internal/runtime/desktop/@wailsio/runtime/package-lock.json index 2ba87daf2..2c409024a 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/package-lock.json +++ b/v3/internal/runtime/desktop/@wailsio/runtime/package-lock.json @@ -1,18 +1,460 @@ { "name": "@wailsio/runtime", - "version": "3.0.0-alpha.39", + "version": "3.0.0-alpha.68", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@wailsio/runtime", - "version": "3.0.0-alpha.39", + "version": "3.0.0-alpha.68", "license": "MIT", "devDependencies": { + "happy-dom": "^17.1.1", + "promises-aplus-tests": "2.1.2", "rimraf": "^5.0.5", - "typedoc": "^0.25.7", - "typedoc-plugin-markdown": "^3.17.1", - "typescript": "^5.3.3" + "typedoc": "^0.27.7", + "typedoc-plugin-markdown": "^4.4.2", + "typedoc-plugin-mdn-links": "^4.0.13", + "typedoc-plugin-missing-exports": "^3.1.0", + "typescript": "^5.7.3", + "vitest": "^3.0.6" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz", + "integrity": "sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^1.27.2", + "@shikijs/types": "^1.27.2", + "@shikijs/vscode-textmate": "^10.0.1" } }, "node_modules/@isaacs/cliui": { @@ -20,6 +462,7 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -32,21 +475,521 @@ "node": ">=12" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" } }, - "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", + "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "cpu": [ + "arm" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", + "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", + "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", + "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", + "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", + "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", + "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", + "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", + "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", + "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", + "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", + "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", + "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", + "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", + "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", + "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", + "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", + "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/types": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", + "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.0.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -54,44 +997,292 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/ansi-sequence-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", - "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", - "dev": true - }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -103,13 +1294,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -119,25 +1312,223 @@ "node": ">= 8" } }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, + "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -147,120 +1538,71 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" + "node": ">= 6" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", - "dev": true - }, - "node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "dev": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -271,78 +1613,680 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/happy-dom": { + "version": "17.1.8", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.1.8.tgz", + "integrity": "sha512-Yxbq/FG79z1rhAf/iB6YM8wO2JB/JDQBy99RiLSs+2siEAi5J05x9eW1nnASHZJbpldjJE2KuFLsLZ+AzX/IxA==", "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "node_modules/mocha": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/promises-aplus-tests": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/promises-aplus-tests/-/promises-aplus-tests-2.1.2.tgz", + "integrity": "sha512-XiDfjQqx+rHLof8CU9xPOMLsjiXXxr3fkjE7WJjUzXttffB8K/nsnNsPTcwS4VvHliSjGVsYVqIjFeTHw53f5w==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "mocha": "^2.5.3", + "sinon": "^1.10.3", + "underscore": "~1.8.3" + }, + "bin": { + "promises-aplus-tests": "lib/cli.js" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", + "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.8", + "@rollup/rollup-android-arm64": "4.34.8", + "@rollup/rollup-darwin-arm64": "4.34.8", + "@rollup/rollup-darwin-x64": "4.34.8", + "@rollup/rollup-freebsd-arm64": "4.34.8", + "@rollup/rollup-freebsd-x64": "4.34.8", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", + "@rollup/rollup-linux-arm-musleabihf": "4.34.8", + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-arm64-musl": "4.34.8", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", + "@rollup/rollup-linux-riscv64-gnu": "4.34.8", + "@rollup/rollup-linux-s390x-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-musl": "4.34.8", + "@rollup/rollup-win32-arm64-msvc": "4.34.8", + "@rollup/rollup-win32-ia32-msvc": "4.34.8", + "@rollup/rollup-win32-x64-msvc": "4.34.8", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -355,27 +2299,24 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/shiki": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", - "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "dependencies": { - "ansi-sequence-parser": "^1.1.0", - "jsonc-parser": "^3.2.0", - "vscode-oniguruma": "^1.7.0", - "vscode-textmate": "^8.0.0" - } + "license": "ISC" }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -383,20 +2324,78 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -415,6 +2414,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -429,6 +2429,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -437,13 +2438,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -456,6 +2459,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -472,6 +2476,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -484,48 +2489,185 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/typedoc": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.12.tgz", - "integrity": "sha512-F+qhkK2VoTweDXd1c42GS/By2DvI2uDF4/EpG424dTexSHdtCH52C6IcAvMA6jR3DzAWZjHpUOW+E02kyPNUNw==", + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typedoc": { + "version": "0.27.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.9.tgz", + "integrity": "sha512-/z585740YHURLl9DN2jCWe6OW7zKYm6VoQ93H0sxZ1cwHQEQrUn5BJrEnkWhfzUdyO+BLGjnKUZ9iz9hKloFDw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^1.24.0", "lunr": "^2.3.9", - "marked": "^4.3.0", - "minimatch": "^9.0.3", - "shiki": "^0.14.7" + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.6.1" }, "bin": { "typedoc": "bin/typedoc" }, "engines": { - "node": ">= 16" + "node": ">= 18" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" } }, "node_modules/typedoc-plugin-markdown": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.17.1.tgz", - "integrity": "sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.4.2.tgz", + "integrity": "sha512-kJVkU2Wd+AXQpyL6DlYXXRrfNrHrEIUgiABWH8Z+2Lz5Sq6an4dQ/hfvP75bbokjNDUskOdFlEEm/0fSVyC7eg==", "dev": true, - "dependencies": { - "handlebars": "^4.7.7" + "license": "MIT", + "engines": { + "node": ">= 18" }, "peerDependencies": { - "typedoc": ">=0.24.0" + "typedoc": "0.27.x" + } + }, + "node_modules/typedoc-plugin-mdn-links": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-4.0.14.tgz", + "integrity": "sha512-IDILzJr4OzNb5uAWWRMZYny80Q6jUQerAwskdYbNMwaHMRwsB3+y8oqYYE7/PyH+kJVvJnCC4G2wnAQ3CLdgqA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typedoc": "0.26.x || 0.27.x" + } + }, + "node_modules/typedoc-plugin-missing-exports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-3.1.0.tgz", + "integrity": "sha512-Sogbaj+qDa21NjB3SlIw4JXSwmcl/WOjwiPNaVEcPhpNG/MiRTtpwV81cT7h1cbu9StpONFPbddYWR0KV/fTWA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typedoc": "0.26.x || 0.27.x" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -534,36 +2676,211 @@ "node": ">=14.17" } }, - "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true, - "optional": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz", + "integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.5.2", + "rollup": "^4.30.1" + }, "bin": { - "uglifyjs": "bin/uglifyjs" + "vite": "bin/vite.js" }, "engines": { - "node": ">=0.8.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/vscode-oniguruma": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", - "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", - "dev": true + "node_modules/vite-node": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/vscode-textmate": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", - "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", - "dev": true + "node_modules/vitest": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.7", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -574,17 +2891,36 @@ "node": ">= 8" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -603,6 +2939,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -620,36 +2957,24 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -664,12 +2989,152 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/package.json b/v3/internal/runtime/desktop/@wailsio/runtime/package.json index 1195a58f3..578e1609c 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/package.json +++ b/v3/internal/runtime/desktop/@wailsio/runtime/package.json @@ -1,38 +1,62 @@ { "name": "@wailsio/runtime", "type": "module", - "version": "3.0.0-alpha.55", + "version": "3.0.0-alpha.68", "description": "Wails Runtime", "types": "types/index.d.ts", "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./src/index.js", - "import": "./src/index.js" - } + "types": "./types/index.d.ts", + "import": "./dist/index.js" }, "repository": { "type": "git", - "url": "git+https://github.com/wailsapp/wails.git" - }, - "scripts": { - "prebuild:types": "rimraf ./types", - "build:types": "npx -p typescript tsc src/index.js --declaration --allowJs --emitDeclarationOnly --outDir types", - "postbuild:types": "task generate:events", - "build:docs": "npx typedoc ./src/index.js", - "build:docs:md": "npx typedoc ./src/index.js" + "url": "git+https://github.com/wailsapp/wails.git", + "directory": "v3/internal/runtime/desktop/@wailsio/runtime" }, "author": "The Wails Team", "license": "MIT", + "homepage": "https://v3.wails.io", "bugs": { "url": "https://github.com/wailsapp/wails/issues" }, - "homepage": "https://wails.io", - "private": false, + "files": [ + "./dist", + "./types" + ], + "sideEffects": [ + "./dist/index.js", + "./dist/contextmenu.js", + "./dist/drag.js" + ], + "scripts": { + "check": "npx tsc --noEmit", + "test": "npx vitest run", + "clean": "npx rimraf ./dist ./docs ./types ./tsconfig.tsbuildinfo", + "generate:events": "task generate:events", + "generate": "npm run generate:events", + "prebuild": "npm run clean && npm run generate", + "build:code": "npx tsc", + "build:docs": "npx typedoc --gitRevision v3-alpha --plugin typedoc-plugin-mdn-links --plugin typedoc-plugin-missing-exports ./src/index.ts", + "build:docs:md": "npx typedoc --gitRevision v3-alpha --plugin typedoc-plugin-markdown --plugin typedoc-plugin-mdn-links --plugin typedoc-plugin-missing-exports ./src/index.ts", + "build": "npm run build:code & npm run build:docs & wait", + "prepack": "npm run build" + }, "devDependencies": { + "happy-dom": "^17.1.1", + "promises-aplus-tests": "2.1.2", "rimraf": "^5.0.5", - "typedoc": "^0.25.7", - "typedoc-plugin-markdown": "^3.17.1", - "typescript": "^5.3.3" + "typedoc": "^0.27.7", + "typedoc-plugin-markdown": "^4.4.2", + "typedoc-plugin-mdn-links": "^4.0.13", + "typedoc-plugin-missing-exports": "^3.1.0", + "typescript": "^5.7.3", + "vitest": "^3.0.6" + }, + "overrides": { + "promises-aplus-tests": { + "mocha": "^11.1.0", + "sinon": "^19.0.2", + "underscore": "^1.13.7" + } } } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/application.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/application.ts similarity index 61% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/application.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/application.ts index 8f650e287..57a41ac9e 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/application.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/application.ts @@ -8,10 +8,8 @@ The electron alternative for Go (c) Lea Anthony 2019-present */ -/* jshint esversion: 9 */ - -import { newRuntimeCallerWithID, objectNames } from "./runtime"; -const call = newRuntimeCallerWithID(objectNames.Application, ''); +import { newRuntimeCaller, objectNames } from "./runtime.js"; +const call = newRuntimeCaller(objectNames.Application); const HideMethod = 0; const ShowMethod = 1; @@ -19,28 +17,21 @@ const QuitMethod = 2; /** * Hides a certain method by calling the HideMethod function. - * - * @return {Promise} - * */ -export function Hide() { +export function Hide(): Promise { return call(HideMethod); } /** * Calls the ShowMethod and returns the result. - * - * @return {Promise} */ -export function Show() { +export function Show(): Promise { return call(ShowMethod); } /** * Calls the QuitMethod to terminate the program. - * - * @return {Promise} */ -export function Quit() { +export function Quit(): Promise { return call(QuitMethod); } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.js deleted file mode 100644 index 64d80c986..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ -import {newRuntimeCallerWithID, objectNames} from "./runtime"; - -const call = newRuntimeCallerWithID(objectNames.Browser, ''); -const BrowserOpenURL = 0; - -/** - * Open a browser window to the given URL - * @param {string} url - The URL to open - * @returns {Promise} - */ -export function OpenURL(url) { - return call(BrowserOpenURL, {url}); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.ts new file mode 100644 index 000000000..465310d3d --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.ts @@ -0,0 +1,24 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { newRuntimeCaller, objectNames } from "./runtime.js"; + +const call = newRuntimeCaller(objectNames.Browser); + +const BrowserOpenURL = 0; + +/** + * Open a browser window to the given URL. + * + * @param url - The URL to open + */ +export function OpenURL(url: string | URL): Promise { + return call(BrowserOpenURL, {url: url.toString()}); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/callable.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/callable.ts new file mode 100644 index 000000000..e8e2e4087 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/callable.ts @@ -0,0 +1,125 @@ +// Source: https://github.com/inspect-js/is-callable + +// The MIT License (MIT) +// +// Copyright (c) 2015 Jordan Harband +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +var fnToStr = Function.prototype.toString; +var reflectApply: typeof Reflect.apply | false | null = typeof Reflect === 'object' && Reflect !== null && Reflect.apply; +var badArrayLike: any; +var isCallableMarker: any; +if (typeof reflectApply === 'function' && typeof Object.defineProperty === 'function') { + try { + badArrayLike = Object.defineProperty({}, 'length', { + get: function () { + throw isCallableMarker; + } + }); + isCallableMarker = {}; + // eslint-disable-next-line no-throw-literal + reflectApply(function () { throw 42; }, null, badArrayLike); + } catch (_) { + if (_ !== isCallableMarker) { + reflectApply = null; + } + } +} else { + reflectApply = null; +} + +var constructorRegex = /^\s*class\b/; +var isES6ClassFn = function isES6ClassFunction(value: any): boolean { + try { + var fnStr = fnToStr.call(value); + return constructorRegex.test(fnStr); + } catch (e) { + return false; // not a function + } +}; + +var tryFunctionObject = function tryFunctionToStr(value: any): boolean { + try { + if (isES6ClassFn(value)) { return false; } + fnToStr.call(value); + return true; + } catch (e) { + return false; + } +}; +var toStr = Object.prototype.toString; +var objectClass = '[object Object]'; +var fnClass = '[object Function]'; +var genClass = '[object GeneratorFunction]'; +var ddaClass = '[object HTMLAllCollection]'; // IE 11 +var ddaClass2 = '[object HTML document.all class]'; +var ddaClass3 = '[object HTMLCollection]'; // IE 9-10 +var hasToStringTag = typeof Symbol === 'function' && !!Symbol.toStringTag; // better: use `has-tostringtag` + +var isIE68 = !(0 in [,]); // eslint-disable-line no-sparse-arrays, comma-spacing + +var isDDA: (value: any) => boolean = function isDocumentDotAll() { return false; }; +if (typeof document === 'object') { + // Firefox 3 canonicalizes DDA to undefined when it's not accessed directly + var all = document.all; + if (toStr.call(all) === toStr.call(document.all)) { + isDDA = function isDocumentDotAll(value) { + /* globals document: false */ + // in IE 6-8, typeof document.all is "object" and it's truthy + if ((isIE68 || !value) && (typeof value === 'undefined' || typeof value === 'object')) { + try { + var str = toStr.call(value); + return ( + str === ddaClass + || str === ddaClass2 + || str === ddaClass3 // opera 12.16 + || str === objectClass // IE 6-8 + ) && value('') == null; // eslint-disable-line eqeqeq + } catch (e) { /**/ } + } + return false; + }; + } +} + +function isCallableRefApply(value: T | unknown): value is (...args: any[]) => any { + if (isDDA(value)) { return true; } + if (!value) { return false; } + if (typeof value !== 'function' && typeof value !== 'object') { return false; } + try { + (reflectApply as any)(value, null, badArrayLike); + } catch (e) { + if (e !== isCallableMarker) { return false; } + } + return !isES6ClassFn(value) && tryFunctionObject(value); +} + +function isCallableNoRefApply(value: T | unknown): value is (...args: any[]) => any { + if (isDDA(value)) { return true; } + if (!value) { return false; } + if (typeof value !== 'function' && typeof value !== 'object') { return false; } + if (hasToStringTag) { return tryFunctionObject(value); } + if (isES6ClassFn(value)) { return false; } + var strClass = toStr.call(value); + if (strClass !== fnClass && strClass !== genClass && !(/^\[object HTML/).test(strClass)) { return false; } + return tryFunctionObject(value); +}; + +export default reflectApply ? isCallableRefApply : isCallableNoRefApply; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.js deleted file mode 100644 index b9edcf2ac..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.js +++ /dev/null @@ -1,174 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ -import { newRuntimeCallerWithID, objectNames } from "./runtime"; -import { nanoid } from './nanoid.js'; - -// Setup -window._wails = window._wails || {}; -window._wails.callResultHandler = resultHandler; -window._wails.callErrorHandler = errorHandler; - - -const CallBinding = 0; -const call = newRuntimeCallerWithID(objectNames.Call, ''); -const cancelCall = newRuntimeCallerWithID(objectNames.CancelCall, ''); -let callResponses = new Map(); - -/** - * Generates a unique ID using the nanoid library. - * - * @return {string} - A unique ID that does not exist in the callResponses set. - */ -function generateID() { - let result; - do { - result = nanoid(); - } while (callResponses.has(result)); - return result; -} - -/** - * Handles the result of a call request. - * - * @param {string} id - The id of the request to handle the result for. - * @param {string} data - The result data of the request. - * @param {boolean} isJSON - Indicates whether the data is JSON or not. - * - * @return {undefined} - This method does not return any value. - */ -function resultHandler(id, data, isJSON) { - const promiseHandler = getAndDeleteResponse(id); - if (promiseHandler) { - promiseHandler.resolve(isJSON ? JSON.parse(data) : data); - } -} - -/** - * Handles the error from a call request. - * - * @param {string} id - The id of the promise handler. - * @param {string} message - The error message to reject the promise handler with. - * - * @return {void} - */ -function errorHandler(id, message) { - const promiseHandler = getAndDeleteResponse(id); - if (promiseHandler) { - promiseHandler.reject(message); - } -} - -/** - * Retrieves and removes the response associated with the given ID from the callResponses map. - * - * @param {any} id - The ID of the response to be retrieved and removed. - * - * @returns {any} The response object associated with the given ID. - */ -function getAndDeleteResponse(id) { - const response = callResponses.get(id); - callResponses.delete(id); - return response; -} - -/** - * Executes a call using the provided type and options. - * - * @param {string|number} type - The type of call to execute. - * @param {Object} [options={}] - Additional options for the call. - * @return {Promise} - A promise that will be resolved or rejected based on the result of the call. It also has a cancel method to cancel a long running request. - */ -function callBinding(type, options = {}) { - const id = generateID(); - const doCancel = () => { return cancelCall(type, {"call-id": id}) }; - let queuedCancel = false, callRunning = false; - let p = new Promise((resolve, reject) => { - options["call-id"] = id; - callResponses.set(id, { resolve, reject }); - call(type, options). - then((_) => { - callRunning = true; - if (queuedCancel) { - return doCancel(); - } - }). - catch((error) => { - reject(error); - callResponses.delete(id); - }); - }); - p.cancel = () => { - if (callRunning) { - return doCancel(); - } else { - queuedCancel = true; - } - }; - - return p; -} - -/** - * Call method. - * - * @param {Object} options - The options for the method. - * @returns {Object} - The result of the call. - */ -export function Call(options) { - return callBinding(CallBinding, options); -} - -/** - * Executes a method by name. - * - * @param {string} methodName - The name of the method in the format 'package.struct.method'. - * @param {...*} args - The arguments to pass to the method. - * @throws {Error} If the name is not a string or is not in the correct format. - * @returns {*} The result of the method execution. - */ -export function ByName(methodName, ...args) { - return callBinding(CallBinding, { - methodName, - args - }); -} - -/** - * Calls a method by its ID with the specified arguments. - * - * @param {number} methodID - The ID of the method to call. - * @param {...*} args - The arguments to pass to the method. - * @return {*} - The result of the method call. - */ -export function ByID(methodID, ...args) { - return callBinding(CallBinding, { - methodID, - args - }); -} - -/** - * Calls a method on a plugin. - * - * @param {string} pluginName - The name of the plugin. - * @param {string} methodName - The name of the method to call. - * @param {...*} args - The arguments to pass to the method. - * @returns {*} - The result of the method call. - */ -export function Plugin(pluginName, methodName, ...args) { - return callBinding(CallBinding, { - packageName: "wails-plugins", - structName: pluginName, - methodName, - args - }); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.ts new file mode 100644 index 000000000..11d063323 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.ts @@ -0,0 +1,233 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { CancellablePromise, type CancellablePromiseWithResolvers } from "./cancellable.js"; +import { newRuntimeCaller, objectNames } from "./runtime.js"; +import { nanoid } from "./nanoid.js"; + +// Setup +window._wails = window._wails || {}; +window._wails.callResultHandler = resultHandler; +window._wails.callErrorHandler = errorHandler; + +type PromiseResolvers = Omit, "promise" | "oncancelled"> + +const call = newRuntimeCaller(objectNames.Call); +const cancelCall = newRuntimeCaller(objectNames.CancelCall); +const callResponses = new Map(); + +const CallBinding = 0; +const CancelMethod = 0 + +/** + * Holds all required information for a binding call. + * May provide either a method ID or a method name, but not both. + */ +export type CallOptions = { + /** The numeric ID of the bound method to call. */ + methodID: number; + /** The fully qualified name of the bound method to call. */ + methodName?: never; + /** Arguments to be passed into the bound method. */ + args: any[]; +} | { + /** The numeric ID of the bound method to call. */ + methodID?: never; + /** The fully qualified name of the bound method to call. */ + methodName: string; + /** Arguments to be passed into the bound method. */ + args: any[]; +}; + +/** + * Exception class that will be thrown in case the bound method returns an error. + * The value of the {@link RuntimeError#name} property is "RuntimeError". + */ +export class RuntimeError extends Error { + /** + * Constructs a new RuntimeError instance. + * @param message - The error message. + * @param options - Options to be forwarded to the Error constructor. + */ + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + this.name = "RuntimeError"; + } +} + +/** + * Handles the result of a call request. + * + * @param id - The id of the request to handle the result for. + * @param data - The result data of the request. + * @param isJSON - Indicates whether the data is JSON or not. + */ +function resultHandler(id: string, data: string, isJSON: boolean): void { + const resolvers = getAndDeleteResponse(id); + if (!resolvers) { + return; + } + + if (!data) { + resolvers.resolve(undefined); + } else if (!isJSON) { + resolvers.resolve(data); + } else { + try { + resolvers.resolve(JSON.parse(data)); + } catch (err: any) { + resolvers.reject(new TypeError("could not parse result: " + err.message, { cause: err })); + } + } +} + +/** + * Handles the error from a call request. + * + * @param id - The id of the promise handler. + * @param data - The error data to reject the promise handler with. + * @param isJSON - Indicates whether the data is JSON or not. + */ +function errorHandler(id: string, data: string, isJSON: boolean): void { + const resolvers = getAndDeleteResponse(id); + if (!resolvers) { + return; + } + + if (!isJSON) { + resolvers.reject(new Error(data)); + } else { + let error: any; + try { + error = JSON.parse(data); + } catch (err: any) { + resolvers.reject(new TypeError("could not parse error: " + err.message, { cause: err })); + return; + } + + let options: ErrorOptions = {}; + if (error.cause) { + options.cause = error.cause; + } + + let exception; + switch (error.kind) { + case "ReferenceError": + exception = new ReferenceError(error.message, options); + break; + case "TypeError": + exception = new TypeError(error.message, options); + break; + case "RuntimeError": + exception = new RuntimeError(error.message, options); + break; + default: + exception = new Error(error.message, options); + break; + } + + resolvers.reject(exception); + } +} + +/** + * Retrieves and removes the response associated with the given ID from the callResponses map. + * + * @param id - The ID of the response to be retrieved and removed. + * @returns The response object associated with the given ID, if any. + */ +function getAndDeleteResponse(id: string): PromiseResolvers | undefined { + const response = callResponses.get(id); + callResponses.delete(id); + return response; +} + +/** + * Generates a unique ID using the nanoid library. + * + * @returns A unique ID that does not exist in the callResponses set. + */ +function generateID(): string { + let result; + do { + result = nanoid(); + } while (callResponses.has(result)); + return result; +} + +/** + * Call a bound method according to the given call options. + * + * In case of failure, the returned promise will reject with an exception + * among ReferenceError (unknown method), TypeError (wrong argument count or type), + * {@link RuntimeError} (method returned an error), or other (network or internal errors). + * The exception might have a "cause" field with the value returned + * by the application- or service-level error marshaling functions. + * + * @param options - A method call descriptor. + * @returns The result of the call. + */ +export function Call(options: CallOptions): CancellablePromise { + const id = generateID(); + + const result = CancellablePromise.withResolvers(); + callResponses.set(id, { resolve: result.resolve, reject: result.reject }); + + const request = call(CallBinding, Object.assign({ "call-id": id }, options)); + let running = false; + + request.then(() => { + running = true; + }, (err) => { + callResponses.delete(id); + result.reject(err); + }); + + const cancel = () => { + callResponses.delete(id); + return cancelCall(CancelMethod, {"call-id": id}).catch((err) => { + console.error("Error while requesting binding call cancellation:", err); + }); + }; + + result.oncancelled = () => { + if (running) { + return cancel(); + } else { + return request.then(cancel); + } + }; + + return result.promise; +} + +/** + * Calls a bound method by name with the specified arguments. + * See {@link Call} for details. + * + * @param methodName - The name of the method in the format 'package.struct.method'. + * @param args - The arguments to pass to the method. + * @returns The result of the method call. + */ +export function ByName(methodName: string, ...args: any[]): CancellablePromise { + return Call({ methodName, args }); +} + +/** + * Calls a method by its numeric ID with the specified arguments. + * See {@link Call} for details. + * + * @param methodID - The ID of the method to call. + * @param args - The arguments to pass to the method. + * @return The result of the method call. + */ +export function ByID(methodID: number, ...args: any[]): CancellablePromise { + return Call({ methodID, args }); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js new file mode 100644 index 000000000..b618c1459 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js @@ -0,0 +1,431 @@ +import * as util from "node:util"; +import { describe, it, beforeEach, afterEach, assert, expect, vi } from "vitest"; +import { CancelError, CancellablePromise, CancelledRejectionError } from "./cancellable"; + +// TODO: In order of importance: +// TODO: test cancellation of subpromises the main promise resolves to. +// TODO: test cancellation of promise chains built by calling then() and friends: +// - all promises up the chain should be cancelled; +// - rejection handlers should be always executed with the CancelError of their parent promise in the chain; +// - promises returned from rejection handlers should be cancelled too; +// - if a rejection handler throws or returns a promise that ultimately rejects, +// it should be reported as an unhandled rejection, +// - unless it is a CancelError with the same reason given for cancelling the returned promise. +// TODO: test multiple calls to cancel() (second and later should have no effect). +// TODO: test static factory methods and their cancellation support. + +let expectedUnhandled = new Map(); + +process.on('unhandledRejection', function (error, promise) { + let reason = error; + if (reason instanceof CancelledRejectionError) { + promise = reason.promise; + reason = reason.cause; + } + + let reasons = expectedUnhandled.get(promise); + const callbacks = reasons?.get(reason); + if (callbacks) { + for (const cb of callbacks) { + try { + cb(reason, promise); + } catch (e) { + console.error("Exception in unhandled rejection callback.", e); + } + } + + reasons.delete(reason); + if (reasons.size === 0) { + expectedUnhandled.delete(promise); + } + return; + } + + console.log(util.format("Unhandled rejection.\nReason: %o\nPromise: %o", reason, promise)); + throw error; +}); + +function ignoreUnhandled(reason, promise) { + expectUnhandled(reason, promise, null); +} + +function expectUnhandled(reason, promise, cb) { + let reasons = expectedUnhandled.get(promise); + if (!reasons) { + reasons = new Map(); + expectedUnhandled.set(promise, reasons); + } + let callbacks = reasons.get(reason); + if (!callbacks) { + callbacks = []; + reasons.set(reason, callbacks); + } + if (cb) { + callbacks.push(cb); + } +} + +afterEach(() => { + vi.resetAllMocks(); + vi.restoreAllMocks(); +}); + +const dummyValue = { value: "value" }; +const dummyCause = { dummy: "dummy" }; +const dummyError = new Error("dummy"); +const oncancelled = vi.fn().mockName("oncancelled"); +const sentinel = vi.fn().mockName("sentinel"); +const unhandled = vi.fn().mockName("unhandled"); + +const resolutionPatterns = [ + ["forever", "pending", (test, value, { cls = CancellablePromise, cb = oncancelled } = {}) => test( + new cls(() => {}, cb) + )], + ["already", "fulfilled", (test, value, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + prw.resolve(value ?? dummyValue); + return test(prw.promise); + }], + ["immediately", "fulfilled", (test, value, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + const tp = test(prw.promise); + prw.resolve(value ?? dummyValue); + return tp; + }], + ["eventually", "fulfilled", async (test, value, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + const tp = test(prw.promise); + await new Promise((resolve) => { + setTimeout(() => { + prw.resolve(value ?? dummyValue); + resolve(); + }, 50); + }); + return tp; + }], + ["already", "rejected", (test, reason, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + prw.reject(reason ?? dummyError); + return test(prw.promise); + }], + ["immediately", "rejected", (test, reason, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + const tp = test(prw.promise); + prw.reject(reason ?? dummyError); + return tp; + }], + ["eventually", "rejected", async (test, reason, { cls = CancellablePromise, cb = oncancelled } = {}) => { + const prw = cls.withResolvers(); + prw.oncancelled = cb; + const tp = test(prw.promise); + await new Promise((resolve) => { + setTimeout(() => { + prw.reject(reason ?? dummyError); + resolve(); + }, 50); + }); + return tp; + }], +]; + +describe("CancellablePromise.cancel", ()=> { + it("should suppress its own unhandled cancellation error", async () => { + const p = new CancellablePromise(() => {}); + p.cancel(); + + process.on('unhandledRejection', sentinel); + await new Promise((resolve) => setTimeout(resolve, 100)); + process.off('unhandledRejection', sentinel); + + expect(sentinel).not.toHaveBeenCalled(); + }); + + it.for([ + ["rejections", dummyError], + ["cancellation errors", new CancelError("dummy", { cause: dummyCause })], + ])("should not suppress arbitrary unhandled %s", async ([kind, err]) => { + const p = new CancellablePromise(() => { throw err; }); + p.cancel(); + + await new Promise((resolve) => { + expectUnhandled(err, p, unhandled); + expectUnhandled(err, p, resolve); + }); + + expect(unhandled).toHaveBeenCalledExactlyOnceWith(err, p); + }); + + describe.for(resolutionPatterns)("when applied to %s %s promises", ([time, state, test]) => { + if (time === "already") { + it("should have no effect", () => test(async (promise) => { + promise.then(sentinel, sentinel); + + let reason; + try { + promise.cancel(); + await promise; + assert(state === "fulfilled", "Promise fulfilled unexpectedly"); + } catch (err) { + reason = err; + assert(state === "rejected", "Promise rejected unexpectedly"); + } + + expect(sentinel).toHaveBeenCalled(); + expect(oncancelled).not.toHaveBeenCalled(); + expect(reason).not.toBeInstanceOf(CancelError); + })); + } else { + if (state === "rejected") { + it("should report late rejections as unhandled", () => test(async (promise) => { + promise.cancel(); + + await new Promise((resolve) => { + expectUnhandled(dummyError, promise, unhandled); + expectUnhandled(dummyError, promise, resolve); + }); + + expect(unhandled).toHaveBeenCalledExactlyOnceWith(dummyError, promise); + })); + } + + it("should reject with a CancelError", () => test(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + let reason; + try { + promise.cancel(); + await promise; + } catch (err) { + reason = err; + } + + expect(reason).toBeInstanceOf(CancelError); + })); + + it("should call the oncancelled callback synchronously", () => test(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + try { + promise.cancel(); + sentinel(); + await promise; + } catch {} + + expect(oncancelled).toHaveBeenCalledBefore(sentinel); + })); + + it("should propagate the given cause", () => test(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + let reason; + try { + promise.cancel(dummyCause); + await promise; + } catch (err) { + reason = err; + } + + expect(reason).toBeInstanceOf(CancelError); + expect(reason).toHaveProperty('cause', dummyCause); + expect(oncancelled).toHaveBeenCalledWith(reason.cause); + })); + } + }); +}); + +const onabort = vi.fn().mockName("abort"); + +const abortPatterns = [ + ["never", "standalone", (test) => { + const signal = new AbortSignal(); + signal.addEventListener('abort', onabort, { capture: true }); + return test(signal); + }], + ["already", "standalone", (test) => { + const signal = AbortSignal.abort(dummyCause); + onabort(); + return test(signal); + }], + ["eventually", "standalone", (test) => { + const signal = AbortSignal.timeout(25); + signal.addEventListener('abort', onabort, { capture: true }); + return test(signal); + }], + ["never", "controller-bound", (test) => { + const signal = new AbortController().signal; + signal.addEventListener('abort', onabort, { capture: true }); + return test(signal); + }], + ["already", " controller-bound", (test) => { + const ctrl = new AbortController(); + ctrl.signal.addEventListener('abort', onabort, { capture: true }); + ctrl.abort(dummyCause); + return test(ctrl.signal); + }], + ["immediately", "controller-bound", (test) => { + const ctrl = new AbortController(); + ctrl.signal.addEventListener('abort', onabort, { capture: true }); + const tp = test(ctrl.signal); + ctrl.abort(dummyCause); + return tp; + }], + ["eventually", "controller-bound", (test) => { + const ctrl = new AbortController(); + ctrl.signal.addEventListener('abort', onabort, { capture: true }); + const tp = test(ctrl.signal); + setTimeout(() => ctrl.abort(dummyCause), 25); + return tp; + }] +]; + +describe("CancellablePromise.cancelOn", ()=> { + it("should return the target promise for chaining", () => { + const p = new CancellablePromise(() => {}); + expect(p.cancelOn(AbortSignal.abort())).toBe(p); + }); + + function tests(abortTime, mode, testSignal, resolveTime, state, testPromise) { + if (abortTime !== "never") { + it(`should call CancellablePromise.cancel ${abortTime === "already" ? "immediately" : "on abort"} with the abort reason as cause`, () => testSignal((signal) => testPromise(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + const cancelSpy = vi.spyOn(promise, 'cancel'); + + promise.catch(() => {}); + promise.cancelOn(signal); + + if (signal.aborted) { + sentinel(); + } else { + await new Promise((resolve) => { + signal.onabort = () => { + sentinel(); + resolve(); + }; + }); + } + + expect(cancelSpy).toHaveBeenCalledAfter(onabort); + expect(cancelSpy).toHaveBeenCalledBefore(sentinel); + expect(cancelSpy).toHaveBeenCalledExactlyOnceWith(signal.reason); + }))); + } + + if ( + resolveTime === "already" + || abortTime === "never" + || ( + ["immediately", "eventually"].includes(abortTime) + && ["already", "immediately"].includes(resolveTime) + ) + ) { + it("should have no effect", () => testSignal((signal) => testPromise(async (promise) => { + promise.then(sentinel, sentinel); + + let reason; + try { + if (resolveTime !== "forever") { + await promise.cancelOn(signal); + assert(state === "fulfilled", "Promise fulfilled unexpectedly"); + } else { + await Promise.race([promise, new Promise((resolve) => setTimeout(resolve, 100))]).then(sentinel); + } + } catch (err) { + reason = err; + assert(state === "rejected", "Promise rejected unexpectedly"); + } + + if (abortTime !== "never" && !signal.aborted) { + // Wait for the AbortSignal to have actually aborted. + await new Promise((resolve) => signal.onabort = resolve); + } + + expect(sentinel).toHaveBeenCalled(); + expect(oncancelled).not.toHaveBeenCalled(); + expect(reason).not.toBeInstanceOf(CancelError); + }))); + } else { + if (state === "rejected") { + it("should report late rejections as unhandled", () => testSignal((signal) => testPromise(async (promise) => { + promise.cancelOn(signal); + + await new Promise((resolve) => { + expectUnhandled(dummyError, promise, unhandled); + expectUnhandled(dummyError, promise, resolve); + }); + + expect(unhandled).toHaveBeenCalledExactlyOnceWith(dummyError, promise); + }))); + } + + it("should reject with a CancelError", () => testSignal((signal) => testPromise(async (promise)=> { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + let reason; + try { + await promise.cancelOn(signal); + } catch (err) { + reason = err; + } + + expect(reason).toBeInstanceOf(CancelError); + }))); + + it(`should call the oncancelled callback ${abortTime === "already" ? "" : "a"}synchronously`, () => testSignal((signal) => testPromise(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + try { + promise.cancelOn(signal); + sentinel(); + await promise; + } catch {} + + expect(oncancelled).toHaveBeenCalledAfter(onabort); + if (abortTime === "already") { + expect(oncancelled).toHaveBeenCalledBefore(sentinel); + } else { + expect(oncancelled).toHaveBeenCalledAfter(sentinel); + } + }))); + + it("should propagate the abort reason as cause", () => testSignal((signal) => testPromise(async (promise) => { + // Ignore the unhandled rejection from the test promise. + if (state === "rejected") { ignoreUnhandled(dummyError, promise); } + + let reason; + try { + await promise.cancelOn(signal); + } catch (err) { + reason = err; + } + + expect(reason).toBeInstanceOf(CancelError); + expect(reason).toHaveProperty('cause', signal.reason); + expect(oncancelled).toHaveBeenCalledWith(signal.reason); + }))); + } + } + + describe.for(abortPatterns)("when called with %s aborted %s signals", ([abortTime, mode, testSignal]) => { + describe.for(resolutionPatterns)("when applied to %s %s promises", ([resolveTime, state, testPromise]) => { + tests(abortTime, mode, testSignal, resolveTime, state, testPromise); + }); + }); + + describe.for(resolutionPatterns)("when applied to %s %s promises", ([resolveTime, state, testPromise]) => { + describe.for(abortPatterns)("when called with %s aborted %s signals", ([abortTime, mode, testSignal]) => { + tests(abortTime, mode, testSignal, resolveTime, state, testPromise); + }); + }); +}); \ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts new file mode 100644 index 000000000..5a839e211 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts @@ -0,0 +1,934 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import isCallable from "./callable.js"; + +/** + * Exception class that will be used as rejection reason + * in case a {@link CancellablePromise} is cancelled successfully. + * + * The value of the {@link name} property is the string `"CancelError"`. + * The value of the {@link cause} property is the cause passed to the cancel method, if any. + */ +export class CancelError extends Error { + /** + * Constructs a new `CancelError` instance. + * @param message - The error message. + * @param options - Options to be forwarded to the Error constructor. + */ + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + this.name = "CancelError"; + } +} + +/** + * Exception class that will be reported as an unhandled rejection + * in case a {@link CancellablePromise} rejects after being cancelled, + * or when the `oncancelled` callback throws or rejects. + * + * The value of the {@link name} property is the string `"CancelledRejectionError"`. + * The value of the {@link cause} property is the reason the promise rejected with. + * + * Because the original promise was cancelled, + * a wrapper promise will be passed to the unhandled rejection listener instead. + * The {@link promise} property holds a reference to the original promise. + */ +export class CancelledRejectionError extends Error { + /** + * Holds a reference to the promise that was cancelled and then rejected. + */ + promise: CancellablePromise; + + /** + * Constructs a new `CancelledRejectionError` instance. + * @param promise - The promise that caused the error originally. + * @param reason - The rejection reason. + * @param info - An optional informative message specifying the circumstances in which the error was thrown. + * Defaults to the string `"Unhandled rejection in cancelled promise."`. + */ + constructor(promise: CancellablePromise, reason?: any, info?: string) { + super((info ?? "Unhandled rejection in cancelled promise.") + " Reason: " + errorMessage(reason), { cause: reason }); + this.promise = promise; + this.name = "CancelledRejectionError"; + } +} + +type CancellablePromiseResolver = (value: T | PromiseLike | CancellablePromiseLike) => void; +type CancellablePromiseRejector = (reason?: any) => void; +type CancellablePromiseCanceller = (cause?: any) => void | PromiseLike; +type CancellablePromiseExecutor = (resolve: CancellablePromiseResolver, reject: CancellablePromiseRejector) => void; + +export interface CancellablePromiseLike { + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike | CancellablePromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike | CancellablePromiseLike) | undefined | null): CancellablePromiseLike; + cancel(cause?: any): void | PromiseLike; +} + +/** + * Wraps a cancellable promise along with its resolution methods. + * The `oncancelled` field will be null initially but may be set to provide a custom cancellation function. + */ +export interface CancellablePromiseWithResolvers { + promise: CancellablePromise; + resolve: CancellablePromiseResolver; + reject: CancellablePromiseRejector; + oncancelled: CancellablePromiseCanceller | null; +} + +interface CancellablePromiseState { + readonly root: CancellablePromiseState; + resolving: boolean; + settled: boolean; + reason?: CancelError; +} + +// Private field names. +const barrierSym = Symbol("barrier"); +const cancelImplSym = Symbol("cancelImpl"); +const species = Symbol.species ?? Symbol("speciesPolyfill"); + +/** + * A promise with an attached method for cancelling long-running operations (see {@link CancellablePromise#cancel}). + * Cancellation can optionally be bound to an {@link AbortSignal} + * for better composability (see {@link CancellablePromise#cancelOn}). + * + * Cancelling a pending promise will result in an immediate rejection + * with an instance of {@link CancelError} as reason, + * but whoever started the promise will be responsible + * for actually aborting the underlying operation. + * To this purpose, the constructor and all chaining methods + * accept optional cancellation callbacks. + * + * If a `CancellablePromise` still resolves after having been cancelled, + * the result will be discarded. If it rejects, the reason + * will be reported as an unhandled rejection, + * wrapped in a {@link CancelledRejectionError} instance. + * To facilitate the handling of cancellation requests, + * cancelled `CancellablePromise`s will _not_ report unhandled `CancelError`s + * whose `cause` field is the same as the one with which the current promise was cancelled. + * + * All usual promise methods are defined and return a `CancellablePromise` + * whose cancel method will cancel the parent operation as well, propagating the cancellation reason + * upwards through promise chains. + * Conversely, cancelling a promise will not automatically cancel dependent promises downstream: + * ```ts + * let root = new CancellablePromise((resolve, reject) => { ... }); + * let child1 = root.then(() => { ... }); + * let child2 = child1.then(() => { ... }); + * let child3 = root.catch(() => { ... }); + * child1.cancel(); // Cancels child1 and root, but not child2 or child3 + * ``` + * Cancelling a promise that has already settled is safe and has no consequence. + * + * The `cancel` method returns a promise that _always fulfills_ + * after the whole chain has processed the cancel request + * and all attached callbacks up to that moment have run. + * + * All ES2024 promise methods (static and instance) are defined on CancellablePromise, + * but actual availability may vary with OS/webview version. + * + * In line with the proposal at https://github.com/tc39/proposal-rm-builtin-subclassing, + * `CancellablePromise` does not support transparent subclassing. + * Extenders should take care to provide their own method implementations. + * This might be reconsidered in case the proposal is retired. + * + * CancellablePromise is a wrapper around the DOM Promise object + * and is compliant with the [Promises/A+ specification](https://promisesaplus.com/) + * (it passes the [compliance suite](https://github.com/promises-aplus/promises-tests)) + * if so is the underlying implementation. + */ +export class CancellablePromise extends Promise implements PromiseLike, CancellablePromiseLike { + // Private fields. + /** @internal */ + private [barrierSym]!: Partial> | null; + /** @internal */ + private readonly [cancelImplSym]!: (reason: CancelError) => void | PromiseLike; + + /** + * Creates a new `CancellablePromise`. + * + * @param executor - A callback used to initialize the promise. This callback is passed two arguments: + * a `resolve` callback used to resolve the promise with a value + * or the result of another promise (possibly cancellable), + * and a `reject` callback used to reject the promise with a provided reason or error. + * If the value provided to the `resolve` callback is a thenable _and_ cancellable object + * (it has a `then` _and_ a `cancel` method), + * cancellation requests will be forwarded to that object and the oncancelled will not be invoked anymore. + * If any one of the two callbacks is called _after_ the promise has been cancelled, + * the provided values will be cancelled and resolved as usual, + * but their results will be discarded. + * However, if the resolution process ultimately ends up in a rejection + * that is not due to cancellation, the rejection reason + * will be wrapped in a {@link CancelledRejectionError} + * and bubbled up as an unhandled rejection. + * @param oncancelled - It is the caller's responsibility to ensure that any operation + * started by the executor is properly halted upon cancellation. + * This optional callback can be used to that purpose. + * It will be called _synchronously_ with a cancellation cause + * when cancellation is requested, _after_ the promise has already rejected + * with a {@link CancelError}, but _before_ + * any {@link then}/{@link catch}/{@link finally} callback runs. + * If the callback returns a thenable, the promise returned from {@link cancel} + * will only fulfill after the former has settled. + * Unhandled exceptions or rejections from the callback will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as unhandled rejections. + * If the `resolve` callback is called before cancellation with a cancellable promise, + * cancellation requests on this promise will be diverted to that promise, + * and the original `oncancelled` callback will be discarded. + */ + constructor(executor: CancellablePromiseExecutor, oncancelled?: CancellablePromiseCanceller) { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: any) => void; + super((res, rej) => { resolve = res; reject = rej; }); + + if ((this.constructor as any)[species] !== Promise) { + throw new TypeError("CancellablePromise does not support transparent subclassing. Please refrain from overriding the [Symbol.species] static property."); + } + + let promise: CancellablePromiseWithResolvers = { + promise: this, + resolve, + reject, + get oncancelled() { return oncancelled ?? null; }, + set oncancelled(cb) { oncancelled = cb ?? undefined; } + }; + + const state: CancellablePromiseState = { + get root() { return state; }, + resolving: false, + settled: false + }; + + // Setup cancellation system. + void Object.defineProperties(this, { + [barrierSym]: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + [cancelImplSym]: { + configurable: false, + enumerable: false, + writable: false, + value: cancellerFor(promise, state) + } + }); + + // Run the actual executor. + const rejector = rejectorFor(promise, state); + try { + executor(resolverFor(promise, state), rejector); + } catch (err) { + if (state.resolving) { + console.log("Unhandled exception in CancellablePromise executor.", err); + } else { + rejector(err); + } + } + } + + /** + * Cancels immediately the execution of the operation associated with this promise. + * The promise rejects with a {@link CancelError} instance as reason, + * with the {@link CancelError#cause} property set to the given argument, if any. + * + * Has no effect if called after the promise has already settled; + * repeated calls in particular are safe, but only the first one + * will set the cancellation cause. + * + * The `CancelError` exception _need not_ be handled explicitly _on the promises that are being cancelled:_ + * cancelling a promise with no attached rejection handler does not trigger an unhandled rejection event. + * Therefore, the following idioms are all equally correct: + * ```ts + * new CancellablePromise((resolve, reject) => { ... }).cancel(); + * new CancellablePromise((resolve, reject) => { ... }).then(...).cancel(); + * new CancellablePromise((resolve, reject) => { ... }).then(...).catch(...).cancel(); + * ``` + * Whenever some cancelled promise in a chain rejects with a `CancelError` + * with the same cancellation cause as itself, the error will be discarded silently. + * However, the `CancelError` _will still be delivered_ to all attached rejection handlers + * added by {@link then} and related methods: + * ```ts + * let cancellable = new CancellablePromise((resolve, reject) => { ... }); + * cancellable.then(() => { ... }).catch(console.log); + * cancellable.cancel(); // A CancelError is printed to the console. + * ``` + * If the `CancelError` is not handled downstream by the time it reaches + * a _non-cancelled_ promise, it _will_ trigger an unhandled rejection event, + * just like normal rejections would: + * ```ts + * let cancellable = new CancellablePromise((resolve, reject) => { ... }); + * let chained = cancellable.then(() => { ... }).then(() => { ... }); // No catch... + * cancellable.cancel(); // Unhandled rejection event on chained! + * ``` + * Therefore, it is important to either cancel whole promise chains from their tail, + * as shown in the correct idioms above, or take care of handling errors everywhere. + * + * @returns A cancellable promise that _fulfills_ after the cancel callback (if any) + * and all handlers attached up to the call to cancel have run. + * If the cancel callback returns a thenable, the promise returned by `cancel` + * will also wait for that thenable to settle. + * This enables callers to wait for the cancelled operation to terminate + * without being forced to handle potential errors at the call site. + * ```ts + * cancellable.cancel().then(() => { + * // Cleanup finished, it's safe to do something else. + * }, (err) => { + * // Unreachable: the promise returned from cancel will never reject. + * }); + * ``` + * Note that the returned promise will _not_ handle implicitly any rejection + * that might have occurred already in the cancelled chain. + * It will just track whether registered handlers have been executed or not. + * Therefore, unhandled rejections will never be silently handled by calling cancel. + */ + cancel(cause?: any): CancellablePromise { + return new CancellablePromise((resolve) => { + // INVARIANT: the result of this[cancelImplSym] and the barrier do not ever reject. + // Unfortunately macOS High Sierra does not support Promise.allSettled. + Promise.all([ + this[cancelImplSym](new CancelError("Promise cancelled.", { cause })), + currentBarrier(this) + ]).then(() => resolve(), () => resolve()); + }); + } + + /** + * Binds promise cancellation to the abort event of the given {@link AbortSignal}. + * If the signal has already aborted, the promise will be cancelled immediately. + * When either condition is verified, the cancellation cause will be set + * to the signal's abort reason (see {@link AbortSignal#reason}). + * + * Has no effect if called (or if the signal aborts) _after_ the promise has already settled. + * Only the first signal to abort will set the cancellation cause. + * + * For more details about the cancellation process, + * see {@link cancel} and the `CancellablePromise` constructor. + * + * This method enables `await`ing cancellable promises without having + * to store them for future cancellation, e.g.: + * ```ts + * await longRunningOperation().cancelOn(signal); + * ``` + * instead of: + * ```ts + * let promiseToBeCancelled = longRunningOperation(); + * await promiseToBeCancelled; + * ``` + * + * @returns This promise, for method chaining. + */ + cancelOn(signal: AbortSignal): CancellablePromise { + if (signal.aborted) { + void this.cancel(signal.reason) + } else { + signal.addEventListener('abort', () => void this.cancel(signal.reason), {capture: true}); + } + + return this; + } + + /** + * Attaches callbacks for the resolution and/or rejection of the `CancellablePromise`. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * When the parent promise rejects or is cancelled, the `onrejected` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A `CancellablePromise` for the completion of whichever callback is executed. + * The returned promise is hooked up to propagate cancellation requests up the chain, but not down: + * + * - if the parent promise is cancelled, the `onrejected` handler will be invoked with a `CancelError` + * and the returned promise _will resolve regularly_ with its result; + * - conversely, if the returned promise is cancelled, _the parent promise is cancelled too;_ + * the `onrejected` handler will still be invoked with the parent's `CancelError`, + * but its result will be discarded + * and the returned promise will reject with a `CancelError` as well. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If either callback returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike | CancellablePromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike | CancellablePromiseLike) | undefined | null, oncancelled?: CancellablePromiseCanceller): CancellablePromise { + if (!(this instanceof CancellablePromise)) { + throw new TypeError("CancellablePromise.prototype.then called on an invalid object."); + } + + // NOTE: TypeScript's built-in type for then is broken, + // as it allows specifying an arbitrary TResult1 != T even when onfulfilled is not a function. + // We cannot fix it if we want to CancellablePromise to implement PromiseLike. + + if (!isCallable(onfulfilled)) { onfulfilled = identity as any; } + if (!isCallable(onrejected)) { onrejected = thrower; } + + if (onfulfilled === identity && onrejected == thrower) { + // Shortcut for trivial arguments. + return new CancellablePromise((resolve) => resolve(this as any)); + } + + const barrier: Partial> = {}; + this[barrierSym] = barrier; + + return new CancellablePromise((resolve, reject) => { + void super.then( + (value) => { + if (this[barrierSym] === barrier) { this[barrierSym] = null; } + barrier.resolve?.(); + + try { + resolve(onfulfilled!(value)); + } catch (err) { + reject(err); + } + }, + (reason?) => { + if (this[barrierSym] === barrier) { this[barrierSym] = null; } + barrier.resolve?.(); + + try { + resolve(onrejected!(reason)); + } catch (err) { + reject(err); + } + } + ); + }, async (cause?) => { + //cancelled = true; + try { + return oncancelled?.(cause); + } finally { + await this.cancel(cause); + } + }); + } + + /** + * Attaches a callback for only the rejection of the Promise. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * When the parent promise rejects or is cancelled, the `onrejected` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * It is equivalent to + * ```ts + * cancellablePromise.then(undefined, onrejected, oncancelled); + * ``` + * and the same caveats apply. + * + * @returns A Promise for the completion of the callback. + * Cancellation requests on the returned promise + * will propagate up the chain to the parent promise, + * but not in the other direction. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If `onrejected` returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + * See {@link then} for more details. + */ + catch(onrejected?: ((reason: any) => (PromiseLike | TResult)) | undefined | null, oncancelled?: CancellablePromiseCanceller): CancellablePromise { + return this.then(undefined, onrejected, oncancelled); + } + + /** + * Attaches a callback that is invoked when the CancellablePromise is settled (fulfilled or rejected). The + * resolved value cannot be accessed or modified from the callback. + * The returned promise will settle in the same state as the original one + * after the provided callback has completed execution, + * unless the callback throws or returns a rejecting promise, + * in which case the returned promise will reject as well. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * Once the parent promise settles, the `onfinally` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * This method is implemented in terms of {@link then} and the same caveats apply. + * It is polyfilled, hence available in every OS/webview version. + * + * @returns A Promise for the completion of the callback. + * Cancellation requests on the returned promise + * will propagate up the chain to the parent promise, + * but not in the other direction. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If `onfinally` returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + * See {@link then} for more details. + */ + finally(onfinally?: (() => void) | undefined | null, oncancelled?: CancellablePromiseCanceller): CancellablePromise { + if (!(this instanceof CancellablePromise)) { + throw new TypeError("CancellablePromise.prototype.finally called on an invalid object."); + } + + if (!isCallable(onfinally)) { + return this.then(onfinally, onfinally, oncancelled); + } + + return this.then( + (value) => CancellablePromise.resolve(onfinally()).then(() => value), + (reason?) => CancellablePromise.resolve(onfinally()).then(() => { throw reason; }), + oncancelled, + ); + } + + /** + * We use the `[Symbol.species]` static property, if available, + * to disable the built-in automatic subclassing features from {@link Promise}. + * It is critical for performance reasons that extenders do not override this. + * Once the proposal at https://github.com/tc39/proposal-rm-builtin-subclassing + * is either accepted or retired, this implementation will have to be revised accordingly. + * + * @ignore + * @internal + */ + static get [species]() { + return Promise; + } + + /** + * Creates a CancellablePromise that is resolved with an array of results + * when all of the provided Promises resolve, or rejected when any Promise is rejected. + * + * Every one of the provided objects that is a thenable _and_ cancellable object + * will be cancelled when the returned promise is cancelled, with the same cause. + * + * @group Static Methods + */ + static all(values: Iterable>): CancellablePromise[]>; + static all(values: T): CancellablePromise<{ -readonly [P in keyof T]: Awaited; }>; + static all | ArrayLike>(values: T): CancellablePromise { + let collected = Array.from(values); + const promise = collected.length === 0 + ? CancellablePromise.resolve(collected) + : new CancellablePromise((resolve, reject) => { + void Promise.all(collected).then(resolve, reject); + }, (cause?): Promise => cancelAll(promise, collected, cause)); + return promise; + } + + /** + * Creates a CancellablePromise that is resolved with an array of results + * when all of the provided Promises resolve or reject. + * + * Every one of the provided objects that is a thenable _and_ cancellable object + * will be cancelled when the returned promise is cancelled, with the same cause. + * + * @group Static Methods + */ + static allSettled(values: Iterable>): CancellablePromise>[]>; + static allSettled(values: T): CancellablePromise<{ -readonly [P in keyof T]: PromiseSettledResult>; }>; + static allSettled | ArrayLike>(values: T): CancellablePromise { + let collected = Array.from(values); + const promise = collected.length === 0 + ? CancellablePromise.resolve(collected) + : new CancellablePromise((resolve, reject) => { + void Promise.allSettled(collected).then(resolve, reject); + }, (cause?): Promise => cancelAll(promise, collected, cause)); + return promise; + } + + /** + * The any function returns a promise that is fulfilled by the first given promise to be fulfilled, + * or rejected with an AggregateError containing an array of rejection reasons + * if all of the given promises are rejected. + * It resolves all elements of the passed iterable to promises as it runs this algorithm. + * + * Every one of the provided objects that is a thenable _and_ cancellable object + * will be cancelled when the returned promise is cancelled, with the same cause. + * + * @group Static Methods + */ + static any(values: Iterable>): CancellablePromise>; + static any(values: T): CancellablePromise>; + static any | ArrayLike>(values: T): CancellablePromise { + let collected = Array.from(values); + const promise = collected.length === 0 + ? CancellablePromise.resolve(collected) + : new CancellablePromise((resolve, reject) => { + void Promise.any(collected).then(resolve, reject); + }, (cause?): Promise => cancelAll(promise, collected, cause)); + return promise; + } + + /** + * Creates a Promise that is resolved or rejected when any of the provided Promises are resolved or rejected. + * + * Every one of the provided objects that is a thenable _and_ cancellable object + * will be cancelled when the returned promise is cancelled, with the same cause. + * + * @group Static Methods + */ + static race(values: Iterable>): CancellablePromise>; + static race(values: T): CancellablePromise>; + static race | ArrayLike>(values: T): CancellablePromise { + let collected = Array.from(values); + const promise = new CancellablePromise((resolve, reject) => { + void Promise.race(collected).then(resolve, reject); + }, (cause?): Promise => cancelAll(promise, collected, cause)); + return promise; + } + + /** + * Creates a new cancelled CancellablePromise for the provided cause. + * + * @group Static Methods + */ + static cancel(cause?: any): CancellablePromise { + const p = new CancellablePromise(() => {}); + p.cancel(cause); + return p; + } + + /** + * Creates a new CancellablePromise that cancels + * after the specified timeout, with the provided cause. + * + * If the {@link AbortSignal.timeout} factory method is available, + * it is used to base the timeout on _active_ time rather than _elapsed_ time. + * Otherwise, `timeout` falls back to {@link setTimeout}. + * + * @group Static Methods + */ + static timeout(milliseconds: number, cause?: any): CancellablePromise { + const promise = new CancellablePromise(() => {}); + if (AbortSignal && typeof AbortSignal === 'function' && AbortSignal.timeout && typeof AbortSignal.timeout === 'function') { + AbortSignal.timeout(milliseconds).addEventListener('abort', () => void promise.cancel(cause)); + } else { + setTimeout(() => void promise.cancel(cause), milliseconds); + } + return promise; + } + + /** + * Creates a new CancellablePromise that resolves after the specified timeout. + * The returned promise can be cancelled without consequences. + * + * @group Static Methods + */ + static sleep(milliseconds: number): CancellablePromise; + /** + * Creates a new CancellablePromise that resolves after + * the specified timeout, with the provided value. + * The returned promise can be cancelled without consequences. + * + * @group Static Methods + */ + static sleep(milliseconds: number, value: T): CancellablePromise; + static sleep(milliseconds: number, value?: T): CancellablePromise { + return new CancellablePromise((resolve) => { + setTimeout(() => resolve(value!), milliseconds); + }); + } + + /** + * Creates a new rejected CancellablePromise for the provided reason. + * + * @group Static Methods + */ + static reject(reason?: any): CancellablePromise { + return new CancellablePromise((_, reject) => reject(reason)); + } + + /** + * Creates a new resolved CancellablePromise. + * + * @group Static Methods + */ + static resolve(): CancellablePromise; + /** + * Creates a new resolved CancellablePromise for the provided value. + * + * @group Static Methods + */ + static resolve(value: T): CancellablePromise>; + /** + * Creates a new resolved CancellablePromise for the provided value. + * + * @group Static Methods + */ + static resolve(value: T | PromiseLike): CancellablePromise>; + static resolve(value?: T | PromiseLike): CancellablePromise> { + if (value instanceof CancellablePromise) { + // Optimise for cancellable promises. + return value; + } + return new CancellablePromise((resolve) => resolve(value)); + } + + /** + * Creates a new CancellablePromise and returns it in an object, along with its resolve and reject functions + * and a getter/setter for the cancellation callback. + * + * This method is polyfilled, hence available in every OS/webview version. + * + * @group Static Methods + */ + static withResolvers(): CancellablePromiseWithResolvers { + let result: CancellablePromiseWithResolvers = { oncancelled: null } as any; + result.promise = new CancellablePromise((resolve, reject) => { + result.resolve = resolve; + result.reject = reject; + }, (cause?: any) => { result.oncancelled?.(cause); }); + return result; + } +} + +/** + * Returns a callback that implements the cancellation algorithm for the given cancellable promise. + * The promise returned from the resulting function does not reject. + */ +function cancellerFor(promise: CancellablePromiseWithResolvers, state: CancellablePromiseState) { + let cancellationPromise: void | PromiseLike = undefined; + + return (reason: CancelError): void | PromiseLike => { + if (!state.settled) { + state.settled = true; + state.reason = reason; + promise.reject(reason); + + // Attach an error handler that ignores this specific rejection reason and nothing else. + // In theory, a sane underlying implementation at this point + // should always reject with our cancellation reason, + // hence the handler will never throw. + void Promise.prototype.then.call(promise.promise, undefined, (err) => { + if (err !== reason) { + throw err; + } + }); + } + + // If reason is not set, the promise resolved regularly, hence we must not call oncancelled. + // If oncancelled is unset, no need to go any further. + if (!state.reason || !promise.oncancelled) { return; } + + cancellationPromise = new Promise((resolve) => { + try { + resolve(promise.oncancelled!(state.reason!.cause)); + } catch (err) { + Promise.reject(new CancelledRejectionError(promise.promise, err, "Unhandled exception in oncancelled callback.")); + } + }).catch((reason?) => { + Promise.reject(new CancelledRejectionError(promise.promise, reason, "Unhandled rejection in oncancelled callback.")); + }); + + // Unset oncancelled to prevent repeated calls. + promise.oncancelled = null; + + return cancellationPromise; + } +} + +/** + * Returns a callback that implements the resolution algorithm for the given cancellable promise. + */ +function resolverFor(promise: CancellablePromiseWithResolvers, state: CancellablePromiseState): CancellablePromiseResolver { + return (value) => { + if (state.resolving) { return; } + state.resolving = true; + + if (value === promise.promise) { + if (state.settled) { return; } + state.settled = true; + promise.reject(new TypeError("A promise cannot be resolved with itself.")); + return; + } + + if (value != null && (typeof value === 'object' || typeof value === 'function')) { + let then: any; + try { + then = (value as any).then; + } catch (err) { + state.settled = true; + promise.reject(err); + return; + } + + if (isCallable(then)) { + try { + let cancel = (value as any).cancel; + if (isCallable(cancel)) { + const oncancelled = (cause?: any) => { + Reflect.apply(cancel, value, [cause]); + }; + if (state.reason) { + // If already cancelled, propagate cancellation. + // The promise returned from the canceller algorithm does not reject + // so it can be discarded safely. + void cancellerFor({ ...promise, oncancelled }, state)(state.reason); + } else { + promise.oncancelled = oncancelled; + } + } + } catch {} + + const newState: CancellablePromiseState = { + root: state.root, + resolving: false, + get settled() { return this.root.settled }, + set settled(value) { this.root.settled = value; }, + get reason() { return this.root.reason } + }; + + const rejector = rejectorFor(promise, newState); + try { + Reflect.apply(then, value, [resolverFor(promise, newState), rejector]); + } catch (err) { + rejector(err); + } + return; // IMPORTANT! + } + } + + if (state.settled) { return; } + state.settled = true; + promise.resolve(value); + }; +} + +/** + * Returns a callback that implements the rejection algorithm for the given cancellable promise. + */ +function rejectorFor(promise: CancellablePromiseWithResolvers, state: CancellablePromiseState): CancellablePromiseRejector { + return (reason?) => { + if (state.resolving) { return; } + state.resolving = true; + + if (state.settled) { + try { + if (reason instanceof CancelError && state.reason instanceof CancelError && Object.is(reason.cause, state.reason.cause)) { + // Swallow late rejections that are CancelErrors whose cancellation cause is the same as ours. + return; + } + } catch {} + + void Promise.reject(new CancelledRejectionError(promise.promise, reason)); + } else { + state.settled = true; + promise.reject(reason); + } + } +} + +/** + * Cancels all values in an array that look like cancellable thenables. + * Returns a promise that fulfills once all cancellation procedures for the given values have settled. + */ +function cancelAll(parent: CancellablePromise, values: any[], cause?: any): Promise { + const results = []; + + for (const value of values) { + let cancel: CancellablePromiseCanceller; + try { + if (!isCallable(value.then)) { continue; } + cancel = value.cancel; + if (!isCallable(cancel)) { continue; } + } catch { continue; } + + let result: void | PromiseLike; + try { + result = Reflect.apply(cancel, value, [cause]); + } catch (err) { + Promise.reject(new CancelledRejectionError(parent, err, "Unhandled exception in cancel method.")); + continue; + } + + if (!result) { continue; } + results.push( + (result instanceof Promise ? result : Promise.resolve(result)).catch((reason?) => { + Promise.reject(new CancelledRejectionError(parent, reason, "Unhandled rejection in cancel method.")); + }) + ); + } + + return Promise.all(results) as any; +} + +/** + * Returns its argument. + */ +function identity(x: T): T { + return x; +} + +/** + * Throws its argument. + */ +function thrower(reason?: any): never { + throw reason; +} + +/** + * Attempts various strategies to convert an error to a string. + */ +function errorMessage(err: any): string { + try { + if (err instanceof Error || typeof err !== 'object' || err.toString !== Object.prototype.toString) { + return "" + err; + } + } catch {} + + try { + return JSON.stringify(err); + } catch {} + + try { + return Object.prototype.toString.call(err); + } catch {} + + return ""; +} + +/** + * Gets the current barrier promise for the given cancellable promise. If necessary, initialises the barrier. + */ +function currentBarrier(promise: CancellablePromise): Promise { + let pwr: Partial> = promise[barrierSym] ?? {}; + if (!('promise' in pwr)) { + Object.assign(pwr, promiseWithResolvers()); + } + if (promise[barrierSym] == null) { + pwr.resolve!(); + promise[barrierSym] = pwr; + } + return pwr.promise!; +} + +// Polyfill Promise.withResolvers. +let promiseWithResolvers = Promise.withResolvers; +if (promiseWithResolvers && typeof promiseWithResolvers === 'function') { + promiseWithResolvers = promiseWithResolvers.bind(Promise); +} else { + promiseWithResolvers = function (): PromiseWithResolvers { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: any) => void; + const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + return { promise, resolve, reject }; + } +} \ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.js deleted file mode 100644 index 15cfc518a..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -import {newRuntimeCallerWithID, objectNames} from "./runtime"; - -const call = newRuntimeCallerWithID(objectNames.Clipboard, ''); -const ClipboardSetText = 0; -const ClipboardText = 1; - -/** - * Sets the text to the Clipboard. - * - * @param {string} text - The text to be set to the Clipboard. - * @return {Promise} - A Promise that resolves when the operation is successful. - */ -export function SetText(text) { - return call(ClipboardSetText, {text}); -} - -/** - * Get the Clipboard text - * @returns {Promise} A promise that resolves with the text from the Clipboard. - */ -export function Text() { - return call(ClipboardText); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.ts new file mode 100644 index 000000000..a6f2f1985 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.ts @@ -0,0 +1,35 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import {newRuntimeCaller, objectNames} from "./runtime.js"; + +const call = newRuntimeCaller(objectNames.Clipboard); + +const ClipboardSetText = 0; +const ClipboardText = 1; + +/** + * Sets the text to the Clipboard. + * + * @param text - The text to be set to the Clipboard. + * @return A Promise that resolves when the operation is successful. + */ +export function SetText(text: string): Promise { + return call(ClipboardSetText, {text}); +} + +/** + * Get the Clipboard text + * + * @returns A promise that resolves with the text from the Clipboard. + */ +export function Text(): Promise { + return call(ClipboardText); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.js deleted file mode 100644 index ab0749901..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -import {newRuntimeCallerWithID, objectNames} from "./runtime"; -import {IsDebug} from "./system"; - -// setup -window.addEventListener('contextmenu', contextMenuHandler); - -const call = newRuntimeCallerWithID(objectNames.ContextMenu, ''); -const ContextMenuOpen = 0; - -function openContextMenu(id, x, y, data) { - void call(ContextMenuOpen, {id, x, y, data}); -} - -function contextMenuHandler(event) { - // Check for custom context menu - let element = event.target; - let customContextMenu = window.getComputedStyle(element).getPropertyValue("--custom-contextmenu"); - customContextMenu = customContextMenu ? customContextMenu.trim() : ""; - if (customContextMenu) { - event.preventDefault(); - let customContextMenuData = window.getComputedStyle(element).getPropertyValue("--custom-contextmenu-data"); - openContextMenu(customContextMenu, event.clientX, event.clientY, customContextMenuData); - return - } - - processDefaultContextMenu(event); -} - - -/* ---default-contextmenu: auto; (default) will show the default context menu if contentEditable is true OR text has been selected OR element is input or textarea ---default-contextmenu: show; will always show the default context menu ---default-contextmenu: hide; will always hide the default context menu - -This rule is inherited like normal CSS rules, so nesting works as expected -*/ -function processDefaultContextMenu(event) { - - // Debug builds always show the menu - if (IsDebug()) { - return; - } - - // Process default context menu - const element = event.target; - const computedStyle = window.getComputedStyle(element); - const defaultContextMenuAction = computedStyle.getPropertyValue("--default-contextmenu").trim(); - switch (defaultContextMenuAction) { - case "show": - return; - case "hide": - event.preventDefault(); - return; - default: - // Check if contentEditable is true - if (element.isContentEditable) { - return; - } - - // Check if text has been selected - const selection = window.getSelection(); - const hasSelection = (selection.toString().length > 0) - if (hasSelection) { - for (let i = 0; i < selection.rangeCount; i++) { - const range = selection.getRangeAt(i); - const rects = range.getClientRects(); - for (let j = 0; j < rects.length; j++) { - const rect = rects[j]; - if (document.elementFromPoint(rect.left, rect.top) === element) { - return; - } - } - } - } - // Check if tagname is input or textarea - if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { - if (hasSelection || (!element.readOnly && !element.disabled)) { - return; - } - } - - // hide default context menu - event.preventDefault(); - } -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.ts new file mode 100644 index 000000000..5f15bf237 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.ts @@ -0,0 +1,94 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { newRuntimeCaller, objectNames } from "./runtime.js"; +import { IsDebug } from "./system.js"; +import { eventTarget } from "./utils"; + +// setup +window.addEventListener('contextmenu', contextMenuHandler); + +const call = newRuntimeCaller(objectNames.ContextMenu); + +const ContextMenuOpen = 0; + +function openContextMenu(id: string, x: number, y: number, data: any): void { + void call(ContextMenuOpen, {id, x, y, data}); +} + +function contextMenuHandler(event: MouseEvent) { + const target = eventTarget(event); + + // Check for custom context menu + const customContextMenu = window.getComputedStyle(target).getPropertyValue("--custom-contextmenu").trim(); + + if (customContextMenu) { + event.preventDefault(); + const data = window.getComputedStyle(target).getPropertyValue("--custom-contextmenu-data"); + openContextMenu(customContextMenu, event.clientX, event.clientY, data); + } else { + processDefaultContextMenu(event, target); + } +} + + +/* +--default-contextmenu: auto; (default) will show the default context menu if contentEditable is true OR text has been selected OR element is input or textarea +--default-contextmenu: show; will always show the default context menu +--default-contextmenu: hide; will always hide the default context menu + +This rule is inherited like normal CSS rules, so nesting works as expected +*/ +function processDefaultContextMenu(event: MouseEvent, target: HTMLElement) { + // Debug builds always show the menu + if (IsDebug()) { + return; + } + + // Process default context menu + switch (window.getComputedStyle(target).getPropertyValue("--default-contextmenu").trim()) { + case 'show': + return; + case 'hide': + event.preventDefault(); + return; + } + + // Check if contentEditable is true + if (target.isContentEditable) { + return; + } + + // Check if text has been selected + const selection = window.getSelection(); + const hasSelection = selection && selection.toString().length > 0; + if (hasSelection) { + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + const rects = range.getClientRects(); + for (let j = 0; j < rects.length; j++) { + const rect = rects[j]; + if (document.elementFromPoint(rect.left, rect.top) === target) { + return; + } + } + } + } + + // Check if tag is input or textarea. + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + if (hasSelection || (!target.readOnly && !target.disabled)) { + return; + } + } + + // hide default context menu + event.preventDefault(); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/create.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/create.ts similarity index 69% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/create.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/create.ts index 6967eef09..72965eaa6 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/create.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/create.ts @@ -8,37 +8,27 @@ The electron alternative for Go (c) Lea Anthony 2019-present */ -/* jshint esversion: 9 */ - /** * Any is a dummy creation function for simple or unknown types. - * @template T - * @param {any} source - * @returns {T} */ -export function Any(source) { - return /** @type {T} */(source); +export function Any(source: any): T { + return source; } /** * ByteSlice is a creation function that replaces * null strings with empty strings. - * @param {any} source - * @returns {string} */ -export function ByteSlice(source) { - return /** @type {any} */((source == null) ? "" : source); +export function ByteSlice(source: any): string { + return ((source == null) ? "" : source); } /** * Array takes a creation function for an arbitrary type * and returns an in-place creation function for an array * whose elements are of that type. - * @template T - * @param {(source: any) => T} element - * @returns {(source: any) => T[]} */ -export function Array(element) { +export function Array(element: (source: any) => T): (source: any) => T[] { if (element === Any) { return (source) => (source === null ? [] : source); } @@ -58,12 +48,8 @@ export function Array(element) { * Map takes creation functions for two arbitrary types * and returns an in-place creation function for an object * whose keys and values are of those types. - * @template K, V - * @param {(source: any) => K} key - * @param {(source: any) => V} value - * @returns {(source: any) => { [_: K]: V }} */ -export function Map(key, value) { +export function Map(key: (source: any) => string, value: (source: any) => V): (source: any) => Record { if (value === Any) { return (source) => (source === null ? {} : source); } @@ -82,11 +68,8 @@ export function Map(key, value) { /** * Nullable takes a creation function for an arbitrary type * and returns a creation function for a nullable value of that type. - * @template T - * @param {(source: any) => T} element - * @returns {(source: any) => (T | null)} */ -export function Nullable(element) { +export function Nullable(element: (source: any) => T): (source: any) => (T | null) { if (element === Any) { return Any; } @@ -97,12 +80,10 @@ export function Nullable(element) { /** * Struct takes an object mapping field names to creation functions * and returns an in-place creation function for a struct. - * @template {{ [_: string]: ((source: any) => any) }} T - * @template {{ [Key in keyof T]?: ReturnType }} U - * @param {T} createField - * @returns {(source: any) => U} */ -export function Struct(createField) { +export function Struct(createField: Record any>): + = any>(source: any) => U +{ let allAny = true; for (const name in createField) { if (createField[name] !== Any) { diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.js deleted file mode 100644 index 134213752..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.js +++ /dev/null @@ -1,200 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -/** - * @typedef {Object} OpenFileDialogOptions - * @property {boolean} [CanChooseDirectories] - Indicates if directories can be chosen. - * @property {boolean} [CanChooseFiles] - Indicates if files can be chosen. - * @property {boolean} [CanCreateDirectories] - Indicates if directories can be created. - * @property {boolean} [ShowHiddenFiles] - Indicates if hidden files should be shown. - * @property {boolean} [ResolvesAliases] - Indicates if aliases should be resolved. - * @property {boolean} [AllowsMultipleSelection] - Indicates if multiple selection is allowed. - * @property {boolean} [HideExtension] - Indicates if the extension should be hidden. - * @property {boolean} [CanSelectHiddenExtension] - Indicates if hidden extensions can be selected. - * @property {boolean} [TreatsFilePackagesAsDirectories] - Indicates if file packages should be treated as directories. - * @property {boolean} [AllowsOtherFiletypes] - Indicates if other file types are allowed. - * @property {FileFilter[]} [Filters] - Array of file filters. - * @property {string} [Title] - Title of the dialog. - * @property {string} [Message] - Message to show in the dialog. - * @property {string} [ButtonText] - Text to display on the button. - * @property {string} [Directory] - Directory to open in the dialog. - * @property {boolean} [Detached] - Indicates if the dialog should appear detached from the main window. - */ - - -/** - * @typedef {Object} SaveFileDialogOptions - * @property {string} [Filename] - Default filename to use in the dialog. - * @property {boolean} [CanChooseDirectories] - Indicates if directories can be chosen. - * @property {boolean} [CanChooseFiles] - Indicates if files can be chosen. - * @property {boolean} [CanCreateDirectories] - Indicates if directories can be created. - * @property {boolean} [ShowHiddenFiles] - Indicates if hidden files should be shown. - * @property {boolean} [ResolvesAliases] - Indicates if aliases should be resolved. - * @property {boolean} [AllowsMultipleSelection] - Indicates if multiple selection is allowed. - * @property {boolean} [HideExtension] - Indicates if the extension should be hidden. - * @property {boolean} [CanSelectHiddenExtension] - Indicates if hidden extensions can be selected. - * @property {boolean} [TreatsFilePackagesAsDirectories] - Indicates if file packages should be treated as directories. - * @property {boolean} [AllowsOtherFiletypes] - Indicates if other file types are allowed. - * @property {FileFilter[]} [Filters] - Array of file filters. - * @property {string} [Title] - Title of the dialog. - * @property {string} [Message] - Message to show in the dialog. - * @property {string} [ButtonText] - Text to display on the button. - * @property {string} [Directory] - Directory to open in the dialog. - * @property {boolean} [Detached] - Indicates if the dialog should appear detached from the main window. - */ - -/** - * @typedef {Object} MessageDialogOptions - * @property {string} [Title] - The title of the dialog window. - * @property {string} [Message] - The main message to show in the dialog. - * @property {Button[]} [Buttons] - Array of button options to show in the dialog. - * @property {boolean} [Detached] - True if the dialog should appear detached from the main window (if applicable). - */ - -/** - * @typedef {Object} Button - * @property {string} [Label] - Text that appears within the button. - * @property {boolean} [IsCancel] - True if the button should cancel an operation when clicked. - * @property {boolean} [IsDefault] - True if the button should be the default action when the user presses enter. - */ - -/** - * @typedef {Object} FileFilter - * @property {string} [DisplayName] - Display name for the filter, it could be "Text Files", "Images" etc. - * @property {string} [Pattern] - Pattern to match for the filter, e.g. "*.txt;*.md" for text markdown files. - */ - -// setup -window._wails = window._wails || {}; -window._wails.dialogErrorCallback = dialogErrorCallback; -window._wails.dialogResultCallback = dialogResultCallback; - -import {newRuntimeCallerWithID, objectNames} from "./runtime"; - -import { nanoid } from './nanoid.js'; - -// Define constants from the `methods` object in Title Case -const DialogInfo = 0; -const DialogWarning = 1; -const DialogError = 2; -const DialogQuestion = 3; -const DialogOpenFile = 4; -const DialogSaveFile = 5; - -const call = newRuntimeCallerWithID(objectNames.Dialog, ''); -const dialogResponses = new Map(); - -/** - * Generates a unique id that is not present in dialogResponses. - * @returns {string} unique id - */ -function generateID() { - let result; - do { - result = nanoid(); - } while (dialogResponses.has(result)); - return result; -} - -/** - * Shows a dialog of specified type with the given options. - * @param {number} type - type of dialog - * @param {MessageDialogOptions|OpenFileDialogOptions|SaveFileDialogOptions} options - options for the dialog - * @returns {Promise} promise that resolves with result of dialog - */ -function dialog(type, options = {}) { - const id = generateID(); - options["dialog-id"] = id; - return new Promise((resolve, reject) => { - dialogResponses.set(id, {resolve, reject}); - call(type, options).catch((error) => { - reject(error); - dialogResponses.delete(id); - }); - }); -} - -/** - * Handles the callback from a dialog. - * - * @param {string} id - The ID of the dialog response. - * @param {string} data - The data received from the dialog. - * @param {boolean} isJSON - Flag indicating whether the data is in JSON format. - * - * @return {undefined} - */ -function dialogResultCallback(id, data, isJSON) { - let p = dialogResponses.get(id); - if (p) { - if (isJSON) { - p.resolve(JSON.parse(data)); - } else { - p.resolve(data); - } - dialogResponses.delete(id); - } -} - -/** - * Callback function for handling errors in dialog. - * - * @param {string} id - The id of the dialog response. - * @param {string} message - The error message. - * - * @return {void} - */ -function dialogErrorCallback(id, message) { - let p = dialogResponses.get(id); - if (p) { - p.reject(message); - dialogResponses.delete(id); - } -} - - -// Replace `methods` with constants in Title Case - -/** - * @param {MessageDialogOptions} options - Dialog options - * @returns {Promise} - The label of the button pressed - */ -export const Info = (options) => dialog(DialogInfo, options); - -/** - * @param {MessageDialogOptions} options - Dialog options - * @returns {Promise} - The label of the button pressed - */ -export const Warning = (options) => dialog(DialogWarning, options); - -/** - * @param {MessageDialogOptions} options - Dialog options - * @returns {Promise} - The label of the button pressed - */ -export const Error = (options) => dialog(DialogError, options); - -/** - * @param {MessageDialogOptions} options - Dialog options - * @returns {Promise} - The label of the button pressed - */ -export const Question = (options) => dialog(DialogQuestion, options); - -/** - * @param {OpenFileDialogOptions} options - Dialog options - * @returns {Promise} Returns selected file or list of files. Returns blank string if no file is selected. - */ -export const OpenFile = (options) => dialog(DialogOpenFile, options); - -/** - * @param {SaveFileDialogOptions} options - Dialog options - * @returns {Promise} Returns the selected file. Returns blank string if no file is selected. - */ -export const SaveFile = (options) => dialog(DialogSaveFile, options); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.ts new file mode 100644 index 000000000..bb0625b2e --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.ts @@ -0,0 +1,255 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import {newRuntimeCaller, objectNames} from "./runtime.js"; +import { nanoid } from './nanoid.js'; + +// setup +window._wails = window._wails || {}; +window._wails.dialogErrorCallback = dialogErrorCallback; +window._wails.dialogResultCallback = dialogResultCallback; + +type PromiseResolvers = Omit, "promise">; + +const call = newRuntimeCaller(objectNames.Dialog); +const dialogResponses = new Map(); + +// Define constants from the `methods` object in Title Case +const DialogInfo = 0; +const DialogWarning = 1; +const DialogError = 2; +const DialogQuestion = 3; +const DialogOpenFile = 4; +const DialogSaveFile = 5; + +export interface OpenFileDialogOptions { + /** Indicates if directories can be chosen. */ + CanChooseDirectories?: boolean; + /** Indicates if files can be chosen. */ + CanChooseFiles?: boolean; + /** Indicates if directories can be created. */ + CanCreateDirectories?: boolean; + /** Indicates if hidden files should be shown. */ + ShowHiddenFiles?: boolean; + /** Indicates if aliases should be resolved. */ + ResolvesAliases?: boolean; + /** Indicates if multiple selection is allowed. */ + AllowsMultipleSelection?: boolean; + /** Indicates if the extension should be hidden. */ + HideExtension?: boolean; + /** Indicates if hidden extensions can be selected. */ + CanSelectHiddenExtension?: boolean; + /** Indicates if file packages should be treated as directories. */ + TreatsFilePackagesAsDirectories?: boolean; + /** Indicates if other file types are allowed. */ + AllowsOtherFiletypes?: boolean; + /** Array of file filters. */ + Filters?: FileFilter[]; + /** Title of the dialog. */ + Title?: string; + /** Message to show in the dialog. */ + Message?: string; + /** Text to display on the button. */ + ButtonText?: string; + /** Directory to open in the dialog. */ + Directory?: string; + /** Indicates if the dialog should appear detached from the main window. */ + Detached?: boolean; +} + +export interface SaveFileDialogOptions { + /** Default filename to use in the dialog. */ + Filename?: string; + /** Indicates if directories can be chosen. */ + CanChooseDirectories?: boolean; + /** Indicates if files can be chosen. */ + CanChooseFiles?: boolean; + /** Indicates if directories can be created. */ + CanCreateDirectories?: boolean; + /** Indicates if hidden files should be shown. */ + ShowHiddenFiles?: boolean; + /** Indicates if aliases should be resolved. */ + ResolvesAliases?: boolean; + /** Indicates if the extension should be hidden. */ + HideExtension?: boolean; + /** Indicates if hidden extensions can be selected. */ + CanSelectHiddenExtension?: boolean; + /** Indicates if file packages should be treated as directories. */ + TreatsFilePackagesAsDirectories?: boolean; + /** Indicates if other file types are allowed. */ + AllowsOtherFiletypes?: boolean; + /** Array of file filters. */ + Filters?: FileFilter[]; + /** Title of the dialog. */ + Title?: string; + /** Message to show in the dialog. */ + Message?: string; + /** Text to display on the button. */ + ButtonText?: string; + /** Directory to open in the dialog. */ + Directory?: string; + /** Indicates if the dialog should appear detached from the main window. */ + Detached?: boolean; +} + +export interface MessageDialogOptions { + /** The title of the dialog window. */ + Title?: string; + /** The main message to show in the dialog. */ + Message?: string; + /** Array of button options to show in the dialog. */ + Buttons?: Button[]; + /** True if the dialog should appear detached from the main window (if applicable). */ + Detached?: boolean; +} + +export interface Button { + /** Text that appears within the button. */ + Label?: string; + /** True if the button should cancel an operation when clicked. */ + IsCancel?: boolean; + /** True if the button should be the default action when the user presses enter. */ + IsDefault?: boolean; +} + +export interface FileFilter { + /** Display name for the filter, it could be "Text Files", "Images" etc. */ + DisplayName?: string; + /** Pattern to match for the filter, e.g. "*.txt;*.md" for text markdown files. */ + Pattern?: string; +} + +/** + * Handles the result of a dialog request. + * + * @param id - The id of the request to handle the result for. + * @param data - The result data of the request. + * @param isJSON - Indicates whether the data is JSON or not. + */ +function dialogResultCallback(id: string, data: string, isJSON: boolean): void { + let resolvers = getAndDeleteResponse(id); + if (!resolvers) { + return; + } + + if (isJSON) { + try { + resolvers.resolve(JSON.parse(data)); + } catch (err: any) { + resolvers.reject(new TypeError("could not parse result: " + err.message, { cause: err })); + } + } else { + resolvers.resolve(data); + } +} + +/** + * Handles the error from a dialog request. + * + * @param id - The id of the promise handler. + * @param message - An error message. + */ +function dialogErrorCallback(id: string, message: string): void { + getAndDeleteResponse(id)?.reject(new window.Error(message)); +} + +/** + * Retrieves and removes the response associated with the given ID from the dialogResponses map. + * + * @param id - The ID of the response to be retrieved and removed. + * @returns The response object associated with the given ID, if any. + */ +function getAndDeleteResponse(id: string): PromiseResolvers | undefined { + const response = dialogResponses.get(id); + dialogResponses.delete(id); + return response; +} + +/** + * Generates a unique ID using the nanoid library. + * + * @returns A unique ID that does not exist in the dialogResponses set. + */ +function generateID(): string { + let result; + do { + result = nanoid(); + } while (dialogResponses.has(result)); + return result; +} + +/** + * Presents a dialog of specified type with the given options. + * + * @param type - Dialog type. + * @param options - Options for the dialog. + * @returns A promise that resolves with result of dialog. + */ +function dialog(type: number, options: MessageDialogOptions | OpenFileDialogOptions | SaveFileDialogOptions = {}): Promise { + const id = generateID(); + return new Promise((resolve, reject) => { + dialogResponses.set(id, { resolve, reject }); + call(type, Object.assign({ "dialog-id": id }, options)).catch((err: any) => { + dialogResponses.delete(id); + reject(err); + }); + }); +} + +/** + * Presents an info dialog. + * + * @param options - Dialog options + * @returns A promise that resolves with the label of the chosen button. + */ +export function Info(options: MessageDialogOptions): Promise { return dialog(DialogInfo, options); } + +/** + * Presents a warning dialog. + * + * @param options - Dialog options. + * @returns A promise that resolves with the label of the chosen button. + */ +export function Warning(options: MessageDialogOptions): Promise { return dialog(DialogWarning, options); } + +/** + * Presents an error dialog. + * + * @param options - Dialog options. + * @returns A promise that resolves with the label of the chosen button. + */ +export function Error(options: MessageDialogOptions): Promise { return dialog(DialogError, options); } + +/** + * Presents a question dialog. + * + * @param options - Dialog options. + * @returns A promise that resolves with the label of the chosen button. + */ +export function Question(options: MessageDialogOptions): Promise { return dialog(DialogQuestion, options); } + +/** + * Presents a file selection dialog to pick one or more files to open. + * + * @param options - Dialog options. + * @returns Selected file or list of files, or a blank string/empty list if no file has been selected. + */ +export function OpenFile(options: OpenFileDialogOptions & { AllowsMultipleSelection: true }): Promise; +export function OpenFile(options: OpenFileDialogOptions & { AllowsMultipleSelection?: false | undefined }): Promise; +export function OpenFile(options: OpenFileDialogOptions): Promise; +export function OpenFile(options: OpenFileDialogOptions): Promise { return dialog(DialogOpenFile, options) ?? []; } + +/** + * Presents a file selection dialog to pick a file to save. + * + * @param options - Dialog options. + * @returns Selected file, or a blank string if no file has been selected. + */ +export function SaveFile(options: SaveFileDialogOptions): Promise { return dialog(DialogSaveFile, options); } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.js deleted file mode 100644 index 9c841796c..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ -/* jshint esversion: 9 */ -import {invoke, IsWindows} from "./system"; -import {GetFlag} from "./flags"; - -// Setup -let shouldDrag = false; -let resizable = false; -let resizeEdge = null; -let defaultCursor = "auto"; - -window._wails = window._wails || {}; - -window._wails.setResizable = function(value) { - resizable = value; -}; - -window._wails.endDrag = function() { - document.body.style.cursor = 'default'; - shouldDrag = false; -}; - -window.addEventListener('mousedown', onMouseDown); -window.addEventListener('mousemove', onMouseMove); -window.addEventListener('mouseup', onMouseUp); - - -function dragTest(e) { - let val = window.getComputedStyle(e.target).getPropertyValue("--wails-draggable"); - let mousePressed = e.buttons !== undefined ? e.buttons : e.which; - if (!val || val === "" || val.trim() !== "drag" || mousePressed === 0) { - return false; - } - return e.detail === 1; -} - -function onMouseDown(e) { - - // Check for resizing - if (resizeEdge) { - invoke("wails:resize:" + resizeEdge); - e.preventDefault(); - return; - } - - if (dragTest(e)) { - // This checks for clicks on the scroll bar - if (e.offsetX > e.target.clientWidth || e.offsetY > e.target.clientHeight) { - return; - } - shouldDrag = true; - } else { - shouldDrag = false; - } -} - -function onMouseUp() { - shouldDrag = false; -} - -function setResize(cursor) { - document.documentElement.style.cursor = cursor || defaultCursor; - resizeEdge = cursor; -} - -function onMouseMove(e) { - if (shouldDrag) { - shouldDrag = false; - let mousePressed = e.buttons !== undefined ? e.buttons : e.which; - if (mousePressed > 0) { - invoke("wails:drag"); - return; - } - } - if (!resizable || !IsWindows()) { - return; - } - if (defaultCursor == null) { - defaultCursor = document.documentElement.style.cursor; - } - let resizeHandleHeight = GetFlag("system.resizeHandleHeight") || 5; - let resizeHandleWidth = GetFlag("system.resizeHandleWidth") || 5; - - // Extra pixels for the corner areas - let cornerExtra = GetFlag("resizeCornerExtra") || 10; - - let rightBorder = window.outerWidth - e.clientX < resizeHandleWidth; - let leftBorder = e.clientX < resizeHandleWidth; - let topBorder = e.clientY < resizeHandleHeight; - let bottomBorder = window.outerHeight - e.clientY < resizeHandleHeight; - - // Adjust for corners - let rightCorner = window.outerWidth - e.clientX < (resizeHandleWidth + cornerExtra); - let leftCorner = e.clientX < (resizeHandleWidth + cornerExtra); - let topCorner = e.clientY < (resizeHandleHeight + cornerExtra); - let bottomCorner = window.outerHeight - e.clientY < (resizeHandleHeight + cornerExtra); - - // If we aren't on an edge, but were, reset the cursor to default - if (!leftBorder && !rightBorder && !topBorder && !bottomBorder && resizeEdge !== undefined) { - setResize(); - } - // Adjusted for corner areas - else if (rightCorner && bottomCorner) setResize("se-resize"); - else if (leftCorner && bottomCorner) setResize("sw-resize"); - else if (leftCorner && topCorner) setResize("nw-resize"); - else if (topCorner && rightCorner) setResize("ne-resize"); - else if (leftBorder) setResize("w-resize"); - else if (topBorder) setResize("n-resize"); - else if (bottomBorder) setResize("s-resize"); - else if (rightBorder) setResize("e-resize"); -} \ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts new file mode 100644 index 000000000..919c03c2c --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts @@ -0,0 +1,237 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { invoke, IsWindows } from "./system.js"; +import { GetFlag } from "./flags.js"; +import { canTrackButtons, eventTarget } from "./utils.js"; + +// Setup +let canDrag = false; +let dragging = false; + +let resizable = false; +let canResize = false; +let resizing = false; +let resizeEdge: string = ""; +let defaultCursor = "auto"; + +let buttons = 0; +const buttonsTracked = canTrackButtons(); + +window._wails = window._wails || {}; +window._wails.setResizable = (value: boolean): void => { + resizable = value; + if (!resizable) { + // Stop resizing if in progress. + canResize = resizing = false; + setResize(); + } +}; + +window.addEventListener('mousedown', update, { capture: true }); +window.addEventListener('mousemove', update, { capture: true }); +window.addEventListener('mouseup', update, { capture: true }); +for (const ev of ['click', 'contextmenu', 'dblclick']) { + window.addEventListener(ev, suppressEvent, { capture: true }); +} + +function suppressEvent(event: Event) { + // Suppress click events while resizing or dragging. + if (dragging || resizing) { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } +} + +// Use constants to avoid comparing strings multiple times. +const MouseDown = 0; +const MouseUp = 1; +const MouseMove = 2; + +function update(event: MouseEvent) { + // Windows suppresses mouse events at the end of dragging or resizing, + // so we need to be smart and synthesize button events. + + let eventType: number, eventButtons = event.buttons; + switch (event.type) { + case 'mousedown': + eventType = MouseDown; + if (!buttonsTracked) { eventButtons = buttons | (1 << event.button); } + break; + case 'mouseup': + eventType = MouseUp; + if (!buttonsTracked) { eventButtons = buttons & ~(1 << event.button); } + break; + default: + eventType = MouseMove; + if (!buttonsTracked) { eventButtons = buttons; } + break; + } + + let released = buttons & ~eventButtons; + let pressed = eventButtons & ~buttons; + + buttons = eventButtons; + + // Synthesize a release-press sequence if we detect a press of an already pressed button. + if (eventType === MouseDown && !(pressed & event.button)) { + released |= (1 << event.button); + pressed |= (1 << event.button); + } + + // Suppress all button events during dragging and resizing, + // unless this is a mouseup event that is ending a drag action. + if ( + eventType !== MouseMove // Fast path for mousemove + && resizing + || ( + dragging + && ( + eventType === MouseDown + || event.button !== 0 + ) + ) + ) { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } + + // Handle releases + if (released & 1) { primaryUp(event); } + // Handle presses + if (pressed & 1) { primaryDown(event); } + + // Handle mousemove + if (eventType === MouseMove) { onMouseMove(event); }; +} + +function primaryDown(event: MouseEvent): void { + // Reset readiness state. + canDrag = false; + canResize = false; + + // Ignore repeated clicks on macOS and Linux. + if (!IsWindows()) { + if (event.type === 'mousedown' && event.button === 0 && event.detail !== 1) { + return; + } + } + + if (resizeEdge) { + // Ready to resize if the primary button was pressed for the first time. + canResize = true; + // Do not start drag operations when on resize edges. + return; + } + + // Retrieve target element + const target = eventTarget(event); + + // Ready to drag if the primary button was pressed for the first time on a draggable element. + // Ignore clicks on the scrollbar. + const style = window.getComputedStyle(target); + canDrag = ( + style.getPropertyValue("--wails-draggable").trim() === "drag" + && ( + event.offsetX - parseFloat(style.paddingLeft) < target.clientWidth + && event.offsetY - parseFloat(style.paddingTop) < target.clientHeight + ) + ); +} + +function primaryUp(event: MouseEvent) { + // Stop dragging and resizing. + canDrag = false; + dragging = false; + canResize = false; + resizing = false; +} + +const cursorForEdge = Object.freeze({ + "se-resize": "nwse-resize", + "sw-resize": "nesw-resize", + "nw-resize": "nwse-resize", + "ne-resize": "nesw-resize", + "w-resize": "ew-resize", + "n-resize": "ns-resize", + "s-resize": "ns-resize", + "e-resize": "ew-resize", +}) + +function setResize(edge?: keyof typeof cursorForEdge): void { + if (edge) { + if (!resizeEdge) { defaultCursor = document.body.style.cursor; } + document.body.style.cursor = cursorForEdge[edge]; + } else if (!edge && resizeEdge) { + document.body.style.cursor = defaultCursor; + } + + resizeEdge = edge || ""; +} + +function onMouseMove(event: MouseEvent): void { + if (canResize && resizeEdge) { + // Start resizing. + resizing = true; + invoke("wails:resize:" + resizeEdge); + } else if (canDrag) { + // Start dragging. + dragging = true; + invoke("wails:drag"); + } + + if (dragging || resizing) { + // Either drag or resize is ongoing, + // reset readiness and stop processing. + canDrag = canResize = false; + return; + } + + if (!resizable || !IsWindows()) { + if (resizeEdge) { setResize(); } + return; + } + + const resizeHandleHeight = GetFlag("system.resizeHandleHeight") || 5; + const resizeHandleWidth = GetFlag("system.resizeHandleWidth") || 5; + + // Extra pixels for the corner areas. + const cornerExtra = GetFlag("resizeCornerExtra") || 10; + + const rightBorder = (window.outerWidth - event.clientX) < resizeHandleWidth; + const leftBorder = event.clientX < resizeHandleWidth; + const topBorder = event.clientY < resizeHandleHeight; + const bottomBorder = (window.outerHeight - event.clientY) < resizeHandleHeight; + + // Adjust for corner areas. + const rightCorner = (window.outerWidth - event.clientX) < (resizeHandleWidth + cornerExtra); + const leftCorner = event.clientX < (resizeHandleWidth + cornerExtra); + const topCorner = event.clientY < (resizeHandleHeight + cornerExtra); + const bottomCorner = (window.outerHeight - event.clientY) < (resizeHandleHeight + cornerExtra); + + if (!leftCorner && !topCorner && !bottomCorner && !rightCorner) { + // Optimisation: out of all corner areas implies out of borders. + setResize(); + } + // Detect corners. + else if (rightCorner && bottomCorner) setResize("se-resize"); + else if (leftCorner && bottomCorner) setResize("sw-resize"); + else if (leftCorner && topCorner) setResize("nw-resize"); + else if (topCorner && rightCorner) setResize("ne-resize"); + // Detect borders. + else if (leftBorder) setResize("w-resize"); + else if (topBorder) setResize("n-resize"); + else if (bottomBorder) setResize("s-resize"); + else if (rightBorder) setResize("e-resize"); + // Out of border area. + else setResize(); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.ts similarity index 95% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.ts index 9c7feed7c..9360224da 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.ts @@ -1,6 +1,18 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ -export const EventTypes = { - Windows: { +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +export const Types = Object.freeze({ + Windows: Object.freeze({ APMPowerSettingChange: "windows:APMPowerSettingChange", APMPowerStatusChange: "windows:APMPowerStatusChange", APMResumeAutomatic: "windows:APMResumeAutomatic", @@ -45,8 +57,8 @@ export const EventTypes = { WindowUnMinimise: "windows:WindowUnMinimise", WindowMaximise: "windows:WindowMaximise", WindowUnMaximise: "windows:WindowUnMaximise", - }, - Mac: { + }), + Mac: Object.freeze({ ApplicationDidBecomeActive: "mac:ApplicationDidBecomeActive", ApplicationDidChangeBackingProperties: "mac:ApplicationDidChangeBackingProperties", ApplicationDidChangeEffectiveAppearance: "mac:ApplicationDidChangeEffectiveAppearance", @@ -179,8 +191,8 @@ export const EventTypes = { WindowZoomIn: "mac:WindowZoomIn", WindowZoomOut: "mac:WindowZoomOut", WindowZoomReset: "mac:WindowZoomReset", - }, - Linux: { + }), + Linux: Object.freeze({ ApplicationStartup: "linux:ApplicationStartup", SystemThemeChanged: "linux:SystemThemeChanged", WindowDeleteEvent: "linux:WindowDeleteEvent", @@ -189,10 +201,11 @@ export const EventTypes = { WindowFocusIn: "linux:WindowFocusIn", WindowFocusOut: "linux:WindowFocusOut", WindowLoadChanged: "linux:WindowLoadChanged", - }, - Common: { + }), + Common: Object.freeze({ ApplicationOpenedWithFile: "common:ApplicationOpenedWithFile", ApplicationStarted: "common:ApplicationStarted", + ApplicationLaunchedWithUrl: "common:ApplicationLaunchedWithUrl", ThemeChanged: "common:ThemeChanged", WindowClosing: "common:WindowClosing", WindowDidMove: "common:WindowDidMove", @@ -205,6 +218,7 @@ export const EventTypes = { WindowLostFocus: "common:WindowLostFocus", WindowMaximise: "common:WindowMaximise", WindowMinimise: "common:WindowMinimise", + WindowToggleFrameless: "common:WindowToggleFrameless", WindowRestore: "common:WindowRestore", WindowRuntimeReady: "common:WindowRuntimeReady", WindowShow: "common:WindowShow", @@ -215,5 +229,5 @@ export const EventTypes = { WindowZoomIn: "common:WindowZoomIn", WindowZoomOut: "common:WindowZoomOut", WindowZoomReset: "common:WindowZoomReset", - }, -}; + }), +}); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.js deleted file mode 100644 index afbcbb922..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -/** - * @typedef {import("./types").WailsEvent} WailsEvent - */ -import {newRuntimeCallerWithID, objectNames} from "./runtime"; - -import {EventTypes} from "./event_types"; -export const Types = EventTypes; - -// Setup -window._wails = window._wails || {}; -window._wails.dispatchWailsEvent = dispatchWailsEvent; - -const call = newRuntimeCallerWithID(objectNames.Events, ''); -const EmitMethod = 0; -const eventListeners = new Map(); - -class Listener { - constructor(eventName, callback, maxCallbacks) { - this.eventName = eventName; - this.maxCallbacks = maxCallbacks || -1; - this.Callback = (data) => { - callback(data); - if (this.maxCallbacks === -1) return false; - this.maxCallbacks -= 1; - return this.maxCallbacks === 0; - }; - } -} - -export class WailsEvent { - constructor(name, data = null) { - this.name = name; - this.data = data; - } -} - -export function setup() { -} - -function dispatchWailsEvent(event) { - let listeners = eventListeners.get(event.name); - if (listeners) { - let toRemove = listeners.filter(listener => { - let remove = listener.Callback(event); - if (remove) return true; - }); - if (toRemove.length > 0) { - listeners = listeners.filter(l => !toRemove.includes(l)); - if (listeners.length === 0) eventListeners.delete(event.name); - else eventListeners.set(event.name, listeners); - } - } -} - -/** - * Register a callback function to be called multiple times for a specific event. - * - * @param {string} eventName - The name of the event to register the callback for. - * @param {function} callback - The callback function to be called when the event is triggered. - * @param {number} maxCallbacks - The maximum number of times the callback can be called for the event. Once the maximum number is reached, the callback will no longer be called. - * - @return {function} - A function that, when called, will unregister the callback from the event. - */ -export function OnMultiple(eventName, callback, maxCallbacks) { - let listeners = eventListeners.get(eventName) || []; - const thisListener = new Listener(eventName, callback, maxCallbacks); - listeners.push(thisListener); - eventListeners.set(eventName, listeners); - return () => listenerOff(thisListener); -} - -/** - * Registers a callback function to be executed when the specified event occurs. - * - * @param {string} eventName - The name of the event. - * @param {function} callback - The callback function to be executed. It takes no parameters. - * @return {function} - A function that, when called, will unregister the callback from the event. */ -export function On(eventName, callback) { return OnMultiple(eventName, callback, -1); } - -/** - * Registers a callback function to be executed only once for the specified event. - * - * @param {string} eventName - The name of the event. - * @param {function} callback - The function to be executed when the event occurs. - * @return {function} - A function that, when called, will unregister the callback from the event. - */ -export function Once(eventName, callback) { return OnMultiple(eventName, callback, 1); } - -/** - * Removes the specified listener from the event listeners collection. - * If all listeners for the event are removed, the event key is deleted from the collection. - * - * @param {Object} listener - The listener to be removed. - */ -function listenerOff(listener) { - const eventName = listener.eventName; - let listeners = eventListeners.get(eventName).filter(l => l !== listener); - if (listeners.length === 0) eventListeners.delete(eventName); - else eventListeners.set(eventName, listeners); -} - - -/** - * Removes event listeners for the specified event names. - * - * @param {string} eventName - The name of the event to remove listeners for. - * @param {...string} additionalEventNames - Additional event names to remove listeners for. - * @return {undefined} - */ -export function Off(eventName, ...additionalEventNames) { - let eventsToRemove = [eventName, ...additionalEventNames]; - eventsToRemove.forEach(eventName => eventListeners.delete(eventName)); -} -/** - * Removes all event listeners. - * - * @function OffAll - * @returns {void} - */ -export function OffAll() { eventListeners.clear(); } - -/** - * Emits an event using the given event name. - * - * @param {WailsEvent} event - The name of the event to emit. - * @returns {any} - The result of the emitted event. - */ -export function Emit(event) { return call(EmitMethod, event); } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.test.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.test.js index c46868cb0..e8157a17a 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.test.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.test.js @@ -1,18 +1,28 @@ - -import { On, Off, OffAll, OnMultiple, WailsEvent, dispatchWailsEvent, eventListeners, Once } from './events'; - +import { On, Off, OffAll, OnMultiple, WailsEvent, Once } from './events'; +import { eventListeners } from "./listener"; import { expect, describe, it, vi, afterEach, beforeEach } from 'vitest'; +const dispatchWailsEvent = window._wails.dispatchWailsEvent; + afterEach(() => { OffAll(); vi.resetAllMocks(); }); -describe('OnMultiple', () => { - let testEvent = new WailsEvent('a', {}); +describe("OnMultiple", () => { + const testEvent = { name: 'a', data: ["hello", "events"] }; + const cb = vi.fn((ev) => { + expect(ev).toBeInstanceOf(WailsEvent); + expect(ev).toMatchObject(testEvent); + }); - it('should stop after a specified number of times', () => { - const cb = vi.fn(); + it("should dispatch a properly initialised WailsEvent", () => { + OnMultiple('a', cb, 5); + dispatchWailsEvent(testEvent); + expect(cb).toHaveBeenCalled(); + }); + + it("should stop after the specified number of times", () => { OnMultiple('a', cb, 5); dispatchWailsEvent(testEvent); dispatchWailsEvent(testEvent); @@ -20,77 +30,126 @@ describe('OnMultiple', () => { dispatchWailsEvent(testEvent); dispatchWailsEvent(testEvent); dispatchWailsEvent(testEvent); - expect(cb).toBeCalledTimes(5); + expect(cb).toHaveBeenCalledTimes(5); }); - it('should return a cancel fn', () => { - const cb = vi.fn() - const cancel = OnMultiple('a', cb, 5) - dispatchWailsEvent(testEvent) - dispatchWailsEvent(testEvent) - cancel() - dispatchWailsEvent(testEvent) - dispatchWailsEvent(testEvent) - expect(cb).toBeCalledTimes(2) - }) -}) - -describe('On', () => { - it('should create a listener with a count of -1', () => { - On('a', () => {}) - expect(eventListeners.get("a")[0].maxCallbacks).toBe(-1) - }) - - it('should return a cancel fn', () => { - const cancel = On('a', () => {}) + it("should return a cancel fn", () => { + const cancel = OnMultiple('a', cb, 5); + dispatchWailsEvent(testEvent); + dispatchWailsEvent(testEvent); cancel(); - }) -}) + dispatchWailsEvent(testEvent); + dispatchWailsEvent(testEvent); + expect(cb).toBeCalledTimes(2); + }); +}); -describe('Once', () => { - it('should create a listener with a count of 1', () => { - Once('a', () => {}) - expect(eventListeners.get("a")[0].maxCallbacks).toBe(1) - }) +describe("On", () => { + let testEvent = { name: 'a', data: ["hello", "events"], sender: "window" }; + const cb = vi.fn((ev) => { + expect(ev).toBeInstanceOf(WailsEvent); + expect(ev).toMatchObject(testEvent); + }); - it('should return a cancel fn', () => { - const cancel = EventsOn('a', () => {}) + it("should dispatch a properly initialised WailsEvent", () => { + On('a', cb); + dispatchWailsEvent(testEvent); + expect(cb).toHaveBeenCalled(); + }); + + it("should never stop", () => { + On('a', cb); + expect(eventListeners.get('a')[0].maxCallbacks).toBe(-1); + dispatchWailsEvent(testEvent); + expect(eventListeners.get('a')[0].maxCallbacks).toBe(-1); + }); + + it("should return a cancel fn", () => { + const cancel = On('a', cb) + dispatchWailsEvent(testEvent); cancel(); - }) + dispatchWailsEvent(testEvent); + expect(cb).toHaveBeenCalledTimes(1); + }); +}); + +describe("Once", () => { + const testEvent = { name: 'a', data: ["hello", "events"] }; + const cb = vi.fn((ev) => { + expect(ev).toBeInstanceOf(WailsEvent); + expect(ev).toMatchObject(testEvent); + }); + + it("should dispatch a properly initialised WailsEvent", () => { + Once('a', cb); + dispatchWailsEvent(testEvent); + expect(cb).toHaveBeenCalled(); + }); + + it("should stop after one time", () => { + Once('a', cb) + dispatchWailsEvent(testEvent); + dispatchWailsEvent(testEvent); + dispatchWailsEvent(testEvent); + expect(cb).toHaveBeenCalledTimes(1); + }); + + it("should return a cancel fn", () => { + const cancel = Once('a', cb) + cancel(); + dispatchWailsEvent(testEvent); + expect(cb).not.toHaveBeenCalled(); + }); }) -describe('Off', () => { +describe("Off", () => { + const cba = vi.fn(), cbb = vi.fn(), cbc = vi.fn(); + beforeEach(() => { - On('a', () => {}) - On('a', () => {}) - On('a', () => {}) - On('b', () => {}) - On('c', () => {}) - }) + On('a', cba); + On('a', cba); + On('a', cba); + On('b', cbb); + On('c', cbc); + On('c', cbc); + }); - it('should cancel all event listeners for a single type', () => { - Off('a') - expect(eventListeners.get('a')).toBeUndefined() - expect(eventListeners.get('b')).not.toBeUndefined() - expect(eventListeners.get('c')).not.toBeUndefined() - }) + it("should cancel all event listeners for a single type", () => { + Off('a'); + dispatchWailsEvent({ name: 'a' }); + dispatchWailsEvent({ name: 'b' }); + dispatchWailsEvent({ name: 'c' }); + expect(cba).not.toHaveBeenCalled(); + expect(cbb).toHaveBeenCalledTimes(1); + expect(cbc).toHaveBeenCalledTimes(2); + }); - it('should cancel all event listeners for multiple types', () => { - Off('a', 'b') - expect(eventListeners.get('a')).toBeUndefined() - expect(eventListeners.get('b')).toBeUndefined() - expect(eventListeners.get('c')).not.toBeUndefined() - }) -}) + it("should cancel all event listeners for multiple types", () => { + Off('a', 'c') + dispatchWailsEvent({ name: 'a' }); + dispatchWailsEvent({ name: 'b' }); + dispatchWailsEvent({ name: 'c' }); + expect(cba).not.toHaveBeenCalled(); + expect(cbb).toHaveBeenCalledTimes(1); + expect(cbc).not.toHaveBeenCalled(); + }); +}); -describe('OffAll', () => { - it('should cancel all event listeners', () => { - On('a', () => {}) - On('a', () => {}) - On('a', () => {}) - On('b', () => {}) - On('c', () => {}) - OffAll() - expect(eventListeners.size).toBe(0) - }) -}) +describe("OffAll", () => { + it("should cancel all event listeners", () => { + const cba = vi.fn(), cbb = vi.fn(), cbc = vi.fn(); + On('a', cba); + On('a', cba); + On('a', cba); + On('b', cbb); + On('c', cbc); + On('c', cbc); + OffAll(); + dispatchWailsEvent({ name: 'a' }); + dispatchWailsEvent({ name: 'b' }); + dispatchWailsEvent({ name: 'c' }); + expect(cba).not.toHaveBeenCalled(); + expect(cbb).not.toHaveBeenCalled(); + expect(cbc).not.toHaveBeenCalled(); + }); +}); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts new file mode 100644 index 000000000..7ad7642b6 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts @@ -0,0 +1,150 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { newRuntimeCaller, objectNames } from "./runtime.js"; +import { eventListeners, Listener, listenerOff } from "./listener.js"; + +// Setup +window._wails = window._wails || {}; +window._wails.dispatchWailsEvent = dispatchWailsEvent; + +const call = newRuntimeCaller(objectNames.Events); +const EmitMethod = 0; + +export { Types } from "./event_types.js"; + +/** + * The type of handlers for a given event. + */ +export type Callback = (ev: WailsEvent) => void; + +/** + * Represents a system event or a custom event emitted through wails-provided facilities. + */ +export class WailsEvent { + /** + * The name of the event. + */ + name: string; + + /** + * Optional data associated with the emitted event. + */ + data: any; + + /** + * Name of the originating window. Omitted for application events. + * Will be overridden if set manually. + */ + sender?: string; + + constructor(name: string, data: any = null) { + this.name = name; + this.data = data; + } +} + +function dispatchWailsEvent(event: any) { + let listeners = eventListeners.get(event.name); + if (!listeners) { + return; + } + + let wailsEvent = new WailsEvent(event.name, event.data); + if ('sender' in event) { + wailsEvent.sender = event.sender; + } + + listeners = listeners.filter(listener => !listener.dispatch(wailsEvent)); + if (listeners.length === 0) { + eventListeners.delete(event.name); + } else { + eventListeners.set(event.name, listeners); + } +} + +/** + * Register a callback function to be called multiple times for a specific event. + * + * @param eventName - The name of the event to register the callback for. + * @param callback - The callback function to be called when the event is triggered. + * @param maxCallbacks - The maximum number of times the callback can be called for the event. Once the maximum number is reached, the callback will no longer be called. + * @returns A function that, when called, will unregister the callback from the event. + */ +export function OnMultiple(eventName: string, callback: Callback, maxCallbacks: number) { + let listeners = eventListeners.get(eventName) || []; + const thisListener = new Listener(eventName, callback, maxCallbacks); + listeners.push(thisListener); + eventListeners.set(eventName, listeners); + return () => listenerOff(thisListener); +} + +/** + * Registers a callback function to be executed when the specified event occurs. + * + * @param eventName - The name of the event to register the callback for. + * @param callback - The callback function to be called when the event is triggered. + * @returns A function that, when called, will unregister the callback from the event. + */ +export function On(eventName: string, callback: Callback): () => void { + return OnMultiple(eventName, callback, -1); +} + +/** + * Registers a callback function to be executed only once for the specified event. + * + * @param eventName - The name of the event to register the callback for. + * @param callback - The callback function to be called when the event is triggered. + * @returns A function that, when called, will unregister the callback from the event. + */ +export function Once(eventName: string, callback: Callback): () => void { + return OnMultiple(eventName, callback, 1); +} + +/** + * Removes event listeners for the specified event names. + * + * @param eventNames - The name of the events to remove listeners for. + */ +export function Off(...eventNames: [string, ...string[]]): void { + eventNames.forEach(eventName => eventListeners.delete(eventName)); +} + +/** + * Removes all event listeners. + */ +export function OffAll(): void { + eventListeners.clear(); +} + +/** + * Emits an event using the name and data. + * + * @returns A promise that will be fulfilled once the event has been emitted. + * @param name - the name of the event to emit. + * @param data - the data to be sent with the event. + */ +export function Emit(name: string, data?: any): Promise { + let eventName: string; + let eventData: any; + + if (typeof name === 'object' && name !== null && 'name' in name && 'data' in name) { + // If name is an object with a name property, use it directly + eventName = name['name']; + eventData = name['data']; + } else { + // Otherwise use the standard parameters + eventName = name as string; + eventData = data; + } + + return call(EmitMethod, { name: eventName, data: eventData }); +} + diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.js deleted file mode 100644 index 26be59d76..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -/** - * Retrieves the value associated with the specified key from the flag map. - * - * @param {string} keyString - The key to retrieve the value for. - * @return {*} - The value associated with the specified key. - */ -export function GetFlag(keyString) { - try { - return window._wails.flags[keyString]; - } catch (e) { - throw new Error("Unable to retrieve flag '" + keyString + "': " + e); - } -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.ts new file mode 100644 index 000000000..9e4ad2427 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.ts @@ -0,0 +1,23 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +/** + * Retrieves the value associated with the specified key from the flag map. + * + * @param key - The key to retrieve the value for. + * @return The value associated with the specified key. + */ +export function GetFlag(key: string): any { + try { + return window._wails.flags[key]; + } catch (e) { + throw new Error("Unable to retrieve flag '" + key + "': " + e, { cause: e }); + } +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/global.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/global.d.ts new file mode 100644 index 000000000..231896ce1 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/global.d.ts @@ -0,0 +1,17 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +declare global { + interface Window { + _wails: Record; + } +} + +export {}; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.js deleted file mode 100644 index 071c60092..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -// Setup -window._wails = window._wails || {}; - -import "./contextmenu"; -import "./drag"; - -// Re-export public API -import * as Application from "./application"; -import * as Browser from "./browser"; -import * as Call from "./calls"; -import * as Clipboard from "./clipboard"; -import * as Create from "./create"; -import * as Dialogs from "./dialogs"; -import * as Events from "./events"; -import * as Flags from "./flags"; -import * as Screens from "./screens"; -import * as System from "./system"; -import Window from "./window"; -import * as WML from "./wml"; - -export { - Application, - Browser, - Call, - Clipboard, - Create, - Dialogs, - Events, - Flags, - Screens, - System, - Window, - WML -}; - -let initialised = false; -export function init() { - window._wails.invoke = System.invoke; - System.invoke("wails:runtime:ready"); - initialised = true; -} - -window.addEventListener("load", () => { - if (!initialised) { - init(); - } -}); - -// Notify backend - diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts new file mode 100644 index 000000000..09d0dc243 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts @@ -0,0 +1,57 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +// Setup +window._wails = window._wails || {}; + +import "./contextmenu.js"; +import "./drag.js"; + +// Re-export public API +import * as Application from "./application.js"; +import * as Browser from "./browser.js"; +import * as Call from "./calls.js"; +import * as Clipboard from "./clipboard.js"; +import * as Create from "./create.js"; +import * as Dialogs from "./dialogs.js"; +import * as Events from "./events.js"; +import * as Flags from "./flags.js"; +import * as Screens from "./screens.js"; +import * as System from "./system.js"; +import Window from "./window.js"; +import * as WML from "./wml.js"; + +export { + Application, + Browser, + Call, + Clipboard, + Dialogs, + Events, + Flags, + Screens, + System, + Window, + WML +}; + +/** + * An internal utility consumed by the binding generator. + * + * @ignore + * @internal + */ +export { Create }; + +export * from "./cancellable.js"; + +// Notify backend +window._wails.invoke = System.invoke; +System.invoke("wails:runtime:ready"); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/listener.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/listener.ts new file mode 100644 index 000000000..0d74debca --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/listener.ts @@ -0,0 +1,52 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +// The following utilities have been factored out of ./events.ts +// for testing purposes. + +export const eventListeners = new Map(); + +export class Listener { + eventName: string; + callback: (data: any) => void; + maxCallbacks: number; + + constructor(eventName: string, callback: (data: any) => void, maxCallbacks: number) { + this.eventName = eventName; + this.callback = callback; + this.maxCallbacks = maxCallbacks || -1; + } + + dispatch(data: any): boolean { + try { + this.callback(data); + } catch (err) { + console.error(err); + } + + if (this.maxCallbacks === -1) return false; + this.maxCallbacks -= 1; + return this.maxCallbacks === 0; + } +} + +export function listenerOff(listener: Listener): void { + let listeners = eventListeners.get(listener.eventName); + if (!listeners) { + return; + } + + listeners = listeners.filter(l => l !== listener); + if (listeners.length === 0) { + eventListeners.delete(listener.eventName); + } else { + eventListeners.set(listener.eventName, listeners); + } +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.ts similarity index 96% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.ts index 37adf3fb0..bfe83048f 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.ts @@ -27,10 +27,10 @@ // `'use`, `andom`, and `rict'` // References to the brotli default dictionary: // `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf` -let urlAlphabet = +const urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' -export let nanoid = (size = 21) => { +export function nanoid(size: number = 21): string { let id = '' // A compact alternative for `for (var i = 0; i < step; i++)`. let i = size | 0 @@ -39,4 +39,4 @@ export let nanoid = (size = 21) => { id += urlAlphabet[(Math.random() * 64) | 0] } return id -} \ No newline at end of file +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/promises_aplus.test.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/promises_aplus.test.js new file mode 100644 index 000000000..baf51e3c0 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/promises_aplus.test.js @@ -0,0 +1,44 @@ +import * as util from "util"; +import * as V from "vitest"; +import { CancellablePromise } from "./cancellable"; + +// The Promises/A+ suite handles some errors late. +process.on('rejectionHandled', function () {}); + +// The Promises/A+ suite leaves some errors unhandled. +process.on('unhandledRejection', function (reason, promise) { + if (promise instanceof CancellablePromise && reason != null && typeof reason === 'object') { + for (const key of ['dummy', 'other', 'sentinel']) { + if (reason[key] === key) { + return; + } + } + } + throw new Error(`Unhandled rejection at: ${util.inspect(promise)}; reason: ${util.inspect(reason)}`, { cause: reason }); +}); + +// Emulate a minimal version of the mocha BDD API using vitest primitives. +global.context = global.describe = V.describe; +global.specify = global.it = function it(desc, fn) { + let viTestFn = fn; + if (fn?.length) { + viTestFn = () => new Promise((done) => fn(done)); + } + V.it(desc, viTestFn); +} +global.before = function(desc, fn) { V.beforeAll(typeof desc === 'function' ? desc : fn) }; +global.after = function(desc, fn) { V.afterAll(typeof desc === 'function' ? desc : fn) }; +global.beforeEach = function(desc, fn) { V.beforeEach(typeof desc === 'function' ? desc : fn) }; +global.afterEach = function(desc, fn) { V.afterEach(typeof desc === 'function' ? desc : fn) }; + +require('promises-aplus-tests').mocha({ + resolved(value) { + return CancellablePromise.resolve(value); + }, + rejected(reason) { + return CancellablePromise.reject(reason); + }, + deferred() { + return CancellablePromise.withResolvers(); + } +}); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.js deleted file mode 100644 index a61bdc6e5..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ -import { nanoid } from './nanoid.js'; - -const runtimeURL = window.location.origin + "/wails/runtime"; - -// Object Names -export const objectNames = { - Call: 0, - Clipboard: 1, - Application: 2, - Events: 3, - ContextMenu: 4, - Dialog: 5, - Window: 6, - Screens: 7, - System: 8, - Browser: 9, - CancelCall: 10, -} -export let clientId = nanoid(); - -/** - * Creates a runtime caller function that invokes a specified method on a given object within a specified window context. - * - * @param {Object} object - The object on which the method is to be invoked. - * @param {string} windowName - The name of the window context in which the method should be called. - * @returns {Function} A runtime caller function that takes the method name and optionally arguments and invokes the method within the specified window context. - */ -export function newRuntimeCaller(object, windowName) { - return function (method, args=null) { - return runtimeCall(object + "." + method, windowName, args); - }; -} - -/** - * Creates a new runtime caller with specified ID. - * - * @param {object} object - The object to invoke the method on. - * @param {string} windowName - The name of the window. - * @return {Function} - The new runtime caller function. - */ -export function newRuntimeCallerWithID(object, windowName) { - return function (method, args=null) { - return runtimeCallWithID(object, method, windowName, args); - }; -} - - -function runtimeCall(method, windowName, args) { - let url = new URL(runtimeURL); - if( method ) { - url.searchParams.append("method", method); - } - let fetchOptions = { - headers: {}, - }; - if (windowName) { - fetchOptions.headers["x-wails-window-name"] = windowName; - } - if (args) { - url.searchParams.append("args", JSON.stringify(args)); - } - fetchOptions.headers["x-wails-client-id"] = clientId; - - return new Promise((resolve, reject) => { - fetch(url, fetchOptions) - .then(response => { - if (response.ok) { - // check content type - if (response.headers.get("Content-Type") && response.headers.get("Content-Type").indexOf("application/json") !== -1) { - return response.json(); - } else { - return response.text(); - } - } - reject(Error(response.statusText)); - }) - .then(data => resolve(data)) - .catch(error => reject(error)); - }); -} - -function runtimeCallWithID(objectID, method, windowName, args) { - let url = new URL(runtimeURL); - url.searchParams.append("object", objectID); - url.searchParams.append("method", method); - let fetchOptions = { - headers: {}, - }; - if (windowName) { - fetchOptions.headers["x-wails-window-name"] = windowName; - } - if (args) { - url.searchParams.append("args", JSON.stringify(args)); - } - fetchOptions.headers["x-wails-client-id"] = clientId; - return new Promise((resolve, reject) => { - fetch(url, fetchOptions) - .then(response => { - if (response.ok) { - // check content type - if (response.headers.get("Content-Type") && response.headers.get("Content-Type").indexOf("application/json") !== -1) { - return response.json(); - } else { - return response.text(); - } - } - reject(Error(response.statusText)); - }) - .then(data => resolve(data)) - .catch(error => reject(error)); - }); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts new file mode 100644 index 000000000..412427ef5 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts @@ -0,0 +1,67 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { nanoid } from './nanoid.js'; + +const runtimeURL = window.location.origin + "/wails/runtime"; + +// Object Names +export const objectNames = Object.freeze({ + Call: 0, + Clipboard: 1, + Application: 2, + Events: 3, + ContextMenu: 4, + Dialog: 5, + Window: 6, + Screens: 7, + System: 8, + Browser: 9, + CancelCall: 10, +}); +export let clientId = nanoid(); + +/** + * Creates a new runtime caller with specified ID. + * + * @param object - The object to invoke the method on. + * @param windowName - The name of the window. + * @return The new runtime caller function. + */ +export function newRuntimeCaller(object: number, windowName: string = '') { + return function (method: number, args: any = null) { + return runtimeCallWithID(object, method, windowName, args); + }; +} + +async function runtimeCallWithID(objectID: number, method: number, windowName: string, args: any): Promise { + let url = new URL(runtimeURL); + url.searchParams.append("object", objectID.toString()); + url.searchParams.append("method", method.toString()); + if (args) { url.searchParams.append("args", JSON.stringify(args)); } + + let headers: Record = { + ["x-wails-client-id"]: clientId + } + if (windowName) { + headers["x-wails-window-name"] = windowName; + } + + let response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(await response.text()); + } + + if ((response.headers.get("Content-Type")?.indexOf("application/json") ?? -1) !== -1) { + return response.json(); + } else { + return response.text(); + } +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.js deleted file mode 100644 index 97fc2af02..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -/** - * @typedef {Object} Size - * @property {number} Width - The width. - * @property {number} Height - The height. - */ - -/** - * @typedef {Object} Rect - * @property {number} X - The X coordinate of the origin. - * @property {number} Y - The Y coordinate of the origin. - * @property {number} Width - The width of the rectangle. - * @property {number} Height - The height of the rectangle. - */ - -/** - * @typedef {Object} Screen - * @property {string} ID - Unique identifier for the screen. - * @property {string} Name - Human readable name of the screen. - * @property {number} ScaleFactor - The scale factor of the screen (DPI/96). 1 = standard DPI, 2 = HiDPI (Retina), etc. - * @property {number} X - The X coordinate of the screen. - * @property {number} Y - The Y coordinate of the screen. - * @property {Size} Size - Contains the width and height of the screen. - * @property {Rect} Bounds - Contains the bounds of the screen in terms of X, Y, Width, and Height. - * @property {Rect} PhysicalBounds - Contains the physical bounds of the screen in terms of X, Y, Width, and Height (before scaling). - * @property {Rect} WorkArea - Contains the area of the screen that is actually usable (excluding taskbar and other system UI). - * @property {Rect} PhysicalWorkArea - Contains the physical WorkArea of the screen (before scaling). - * @property {boolean} IsPrimary - True if this is the primary monitor selected by the user in the operating system. - * @property {number} Rotation - The rotation of the screen. - */ - -import { newRuntimeCallerWithID, objectNames } from "./runtime"; -const call = newRuntimeCallerWithID(objectNames.Screens, ""); - -const getAll = 0; -const getPrimary = 1; -const getCurrent = 2; - -/** - * Gets all screens. - * @returns {Promise} A promise that resolves to an array of Screen objects. - */ -export function GetAll() { - return call(getAll); -} -/** - * Gets the primary screen. - * @returns {Promise} A promise that resolves to the primary screen. - */ -export function GetPrimary() { - return call(getPrimary); -} -/** - * Gets the current active screen. - * - * @returns {Promise} A promise that resolves with the current active screen. - */ -export function GetCurrent() { - return call(getCurrent); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts new file mode 100644 index 000000000..c0ecfd7be --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts @@ -0,0 +1,88 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Size { + /** The width of a rectangular area. */ + Width: number; + /** The height of a rectangular area. */ + Height: number; +} + +export interface Rect { + /** The X coordinate of the origin. */ + X: number; + /** The Y coordinate of the origin. */ + Y: number; + /** The width of the rectangle. */ + Width: number; + /** The height of the rectangle. */ + Height: number; +} + +export interface Screen { + /** Unique identifier for the screen. */ + ID: string; + /** Human-readable name of the screen. */ + Name: string; + /** The scale factor of the screen (DPI/96). 1 = standard DPI, 2 = HiDPI (Retina), etc. */ + ScaleFactor: number; + /** The X coordinate of the screen. */ + X: number; + /** The Y coordinate of the screen. */ + Y: number; + /** Contains the width and height of the screen. */ + Size: Size; + /** Contains the bounds of the screen in terms of X, Y, Width, and Height. */ + Bounds: Rect; + /** Contains the physical bounds of the screen in terms of X, Y, Width, and Height (before scaling). */ + PhysicalBounds: Rect; + /** Contains the area of the screen that is actually usable (excluding taskbar and other system UI). */ + WorkArea: Rect; + /** Contains the physical WorkArea of the screen (before scaling). */ + PhysicalWorkArea: Rect; + /** True if this is the primary monitor selected by the user in the operating system. */ + IsPrimary: boolean; + /** The rotation of the screen. */ + Rotation: number; +} + +import { newRuntimeCaller, objectNames } from "./runtime.js"; +const call = newRuntimeCaller(objectNames.Screens); + +const getAll = 0; +const getPrimary = 1; +const getCurrent = 2; + +/** + * Gets all screens. + * + * @returns A promise that resolves to an array of Screen objects. + */ +export function GetAll(): Promise { + return call(getAll); +} + +/** + * Gets the primary screen. + * + * @returns A promise that resolves to the primary screen. + */ +export function GetPrimary(): Promise { + return call(getPrimary); +} + +/** + * Gets the current active screen. + * + * @returns A promise that resolves with the current active screen. + */ +export function GetCurrent(): Promise { + return call(getCurrent); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/system.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/system.js deleted file mode 100644 index 18287e656..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/system.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -import {newRuntimeCallerWithID, objectNames} from "./runtime"; -let call = newRuntimeCallerWithID(objectNames.System, ''); -const systemIsDarkMode = 0; -const environment = 1; - -const _invoke = (() => { - try { - if(window?.chrome?.webview) { - return (msg) => window.chrome.webview.postMessage(msg); - } - if(window?.webkit?.messageHandlers?.external) { - return (msg) => window.webkit.messageHandlers.external.postMessage(msg); - } - } catch(e) { - console.warn('\n%c⚠️ Browser Environment Detected %c\n\n%cOnly UI previews are available in the browser. For full functionality, please run the application in desktop mode.\nMore information at: https://v3.wails.io/learn/build/#using-a-browser-for-development\n', - 'background: #ffffff; color: #000000; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 2px solid #000000;', - 'background: transparent;', - 'color: #ffffff; font-style: italic; font-weight: bold;'); - } - return null; -})(); - -export function invoke(msg) { - if (!_invoke) return; - return _invoke(msg); -} - -/** - * @function - * Retrieves the system dark mode status. - * @returns {Promise} - A promise that resolves to a boolean value indicating if the system is in dark mode. - */ -export function IsDarkMode() { - return call(systemIsDarkMode); -} - -/** - * Fetches the capabilities of the application from the server. - * - * @async - * @function Capabilities - * @returns {Promise} A promise that resolves to an object containing the capabilities. - */ -export function Capabilities() { - let response = fetch("/wails/capabilities"); - return response.json(); -} - -/** - * @typedef {Object} OSInfo - * @property {string} Branding - The branding of the OS. - * @property {string} ID - The ID of the OS. - * @property {string} Name - The name of the OS. - * @property {string} Version - The version of the OS. - */ - -/** - * @typedef {Object} EnvironmentInfo - * @property {string} Arch - The architecture of the system. - * @property {boolean} Debug - True if the application is running in debug mode, otherwise false. - * @property {string} OS - The operating system in use. - * @property {OSInfo} OSInfo - Details of the operating system. - * @property {Object} PlatformInfo - Additional platform information. - */ - -/** - * @function - * Retrieves environment details. - * @returns {Promise} - A promise that resolves to an object containing OS and system architecture. - */ -export function Environment() { - return call(environment); -} - -/** - * Checks if the current operating system is Windows. - * - * @return {boolean} True if the operating system is Windows, otherwise false. - */ -export function IsWindows() { - return window._wails.environment.OS === "windows"; -} - -/** - * Checks if the current operating system is Linux. - * - * @returns {boolean} Returns true if the current operating system is Linux, false otherwise. - */ -export function IsLinux() { - return window._wails.environment.OS === "linux"; -} - -/** - * Checks if the current environment is a macOS operating system. - * - * @returns {boolean} True if the environment is macOS, false otherwise. - */ -export function IsMac() { - return window._wails.environment.OS === "darwin"; -} - -/** - * Checks if the current environment architecture is AMD64. - * @returns {boolean} True if the current environment architecture is AMD64, false otherwise. - */ -export function IsAMD64() { - return window._wails.environment.Arch === "amd64"; -} - -/** - * Checks if the current architecture is ARM. - * - * @returns {boolean} True if the current architecture is ARM, false otherwise. - */ -export function IsARM() { - return window._wails.environment.Arch === "arm"; -} - -/** - * Checks if the current environment is ARM64 architecture. - * - * @returns {boolean} - Returns true if the environment is ARM64 architecture, otherwise returns false. - */ -export function IsARM64() { - return window._wails.environment.Arch === "arm64"; -} - -export function IsDebug() { - return window._wails.environment.Debug === true; -} - diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts new file mode 100644 index 000000000..aa95ecd24 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts @@ -0,0 +1,156 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { newRuntimeCaller, objectNames } from "./runtime.js"; + +const call = newRuntimeCaller(objectNames.System); + +const SystemIsDarkMode = 0; +const SystemEnvironment = 1; + +const _invoke = (function () { + try { + if ((window as any).chrome?.webview?.postMessage) { + return (window as any).chrome.webview.postMessage.bind((window as any).chrome.webview); + } else if ((window as any).webkit?.messageHandlers?.['external']?.postMessage) { + return (window as any).webkit.messageHandlers['external'].postMessage.bind((window as any).webkit.messageHandlers['external']); + } + } catch(e) {} + + console.warn('\n%c⚠️ Browser Environment Detected %c\n\n%cOnly UI previews are available in the browser. For full functionality, please run the application in desktop mode.\nMore information at: https://v3.wails.io/learn/build/#using-a-browser-for-development\n', + 'background: #ffffff; color: #000000; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 2px solid #000000;', + 'background: transparent;', + 'color: #ffffff; font-style: italic; font-weight: bold;'); + return null; +})(); + +export function invoke(msg: any): void { + _invoke?.(msg); +} + +/** + * Retrieves the system dark mode status. + * + * @returns A promise that resolves to a boolean value indicating if the system is in dark mode. + */ +export function IsDarkMode(): Promise { + return call(SystemIsDarkMode); +} + +/** + * Fetches the capabilities of the application from the server. + * + * @returns A promise that resolves to an object containing the capabilities. + */ +export async function Capabilities(): Promise> { + let response = await fetch("/wails/capabilities"); + if (response.ok) { + return response.json(); + } else { + throw new Error("could not fetch capabilities: " + response.statusText); + } +} + +export interface OSInfo { + /** The branding of the OS. */ + Branding: string; + /** The ID of the OS. */ + ID: string; + /** The name of the OS. */ + Name: string; + /** The version of the OS. */ + Version: string; +} + +export interface EnvironmentInfo { + /** The architecture of the system. */ + Arch: string; + /** True if the application is running in debug mode, otherwise false. */ + Debug: boolean; + /** The operating system in use. */ + OS: string; + /** Details of the operating system. */ + OSInfo: OSInfo; + /** Additional platform information. */ + PlatformInfo: Record; +} + +/** + * Retrieves environment details. + * + * @returns A promise that resolves to an object containing OS and system architecture. + */ +export function Environment(): Promise { + return call(SystemEnvironment); +} + +/** + * Checks if the current operating system is Windows. + * + * @return True if the operating system is Windows, otherwise false. + */ +export function IsWindows(): boolean { + return window._wails.environment.OS === "windows"; +} + +/** + * Checks if the current operating system is Linux. + * + * @returns Returns true if the current operating system is Linux, false otherwise. + */ +export function IsLinux(): boolean { + return window._wails.environment.OS === "linux"; +} + +/** + * Checks if the current environment is a macOS operating system. + * + * @returns True if the environment is macOS, false otherwise. + */ +export function IsMac(): boolean { + return window._wails.environment.OS === "darwin"; +} + +/** + * Checks if the current environment architecture is AMD64. + * + * @returns True if the current environment architecture is AMD64, false otherwise. + */ +export function IsAMD64(): boolean { + return window._wails.environment.Arch === "amd64"; +} + +/** + * Checks if the current architecture is ARM. + * + * @returns True if the current architecture is ARM, false otherwise. + */ +export function IsARM(): boolean { + return window._wails.environment.Arch === "arm"; +} + +/** + * Checks if the current environment is ARM64 architecture. + * + * @returns Returns true if the environment is ARM64 architecture, otherwise returns false. + */ +export function IsARM64(): boolean { + return window._wails.environment.Arch === "arm64"; +} + +/** + * Reports whether the app is being run in debug mode. + * + * @returns True if the app is being run in debug mode. + */ +export function IsDebug(): boolean { + return Boolean(window._wails.environment.Debug); +} + diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/utils.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/utils.ts similarity index 75% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/utils.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/utils.ts index b11035a54..35b09463b 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/utils.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/utils.ts @@ -10,10 +10,10 @@ The electron alternative for Go /** * Logs a message to the console with custom formatting. - * @param {string} message - The message to be logged. - * @return {void} + * + * @param message - The message to be logged. */ -export function debugLog(message) { +export function debugLog(message: any) { // eslint-disable-next-line console.log( '%c wails3 %c ' + message + ' ', @@ -22,11 +22,17 @@ export function debugLog(message) { ); } +/** + * Checks whether the webview supports the {@link MouseEvent#buttons} property. + * Looking at you macOS High Sierra! + */ +export function canTrackButtons(): boolean { + return (new MouseEvent('mousedown')).buttons === 0; +} + /** * Checks whether the browser supports removing listeners by triggering an AbortSignal - * (see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#signal) - * - * @return {boolean} + * (see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#signal). */ export function canAbortListeners() { if (!EventTarget || !AbortSignal || !AbortController) @@ -43,6 +49,19 @@ export function canAbortListeners() { return result; } +/** + * Resolves the closest HTMLElement ancestor of an event's target. + */ +export function eventTarget(event: Event): HTMLElement { + if (event.target instanceof HTMLElement) { + return event.target; + } else if (!(event.target instanceof HTMLElement) && event.target instanceof Node) { + return event.target.parentElement ?? document.body; + } else { + return document.body; + } +} + /*** This technique for proper load detection is taken from HTMX: @@ -75,9 +94,9 @@ export function canAbortListeners() { ***/ let isReady = false; -document.addEventListener('DOMContentLoaded', () => isReady = true); +document.addEventListener('DOMContentLoaded', () => { isReady = true }); -export function whenReady(callback) { +export function whenReady(callback: () => void) { if (isReady || document.readyState === 'complete') { callback(); } else { diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/window.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/window.js deleted file mode 100644 index b02729974..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/window.js +++ /dev/null @@ -1,638 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -// Import screen jsdoc definition from ./screens.js -/** - * @typedef {import("./screens").Screen} Screen - */ - - -/** - * A record describing the position of a window. - * - * @typedef {Object} Position - * @property {number} x - The horizontal position of the window - * @property {number} y - The vertical position of the window - */ - - -/** - * A record describing the size of a window. - * - * @typedef {Object} Size - * @property {number} width - The width of the window - * @property {number} height - The height of the window - */ - - -import {newRuntimeCallerWithID, objectNames} from "./runtime"; - -const PositionMethod = 0; -const CenterMethod = 1; -const CloseMethod = 2; -const DisableSizeConstraintsMethod = 3; -const EnableSizeConstraintsMethod = 4; -const FocusMethod = 5; -const ForceReloadMethod = 6; -const FullscreenMethod = 7; -const GetScreenMethod = 8; -const GetZoomMethod = 9; -const HeightMethod = 10; -const HideMethod = 11; -const IsFocusedMethod = 12; -const IsFullscreenMethod = 13; -const IsMaximisedMethod = 14; -const IsMinimisedMethod = 15; -const MaximiseMethod = 16; -const MinimiseMethod = 17; -const NameMethod = 18; -const OpenDevToolsMethod = 19; -const RelativePositionMethod = 20; -const ReloadMethod = 21; -const ResizableMethod = 22; -const RestoreMethod = 23; -const SetPositionMethod = 24; -const SetAlwaysOnTopMethod = 25; -const SetBackgroundColourMethod = 26; -const SetFramelessMethod = 27; -const SetFullscreenButtonEnabledMethod = 28; -const SetMaxSizeMethod = 29; -const SetMinSizeMethod = 30; -const SetRelativePositionMethod = 31; -const SetResizableMethod = 32; -const SetSizeMethod = 33; -const SetTitleMethod = 34; -const SetZoomMethod = 35; -const ShowMethod = 36; -const SizeMethod = 37; -const ToggleFullscreenMethod = 38; -const ToggleMaximiseMethod = 39; -const UnFullscreenMethod = 40; -const UnMaximiseMethod = 41; -const UnMinimiseMethod = 42; -const WidthMethod = 43; -const ZoomMethod = 44; -const ZoomInMethod = 45; -const ZoomOutMethod = 46; -const ZoomResetMethod = 47; - -/** - * @type {symbol} - */ -const caller = Symbol(); - -export class Window { - /** - * Initialises a window object with the specified name. - * - * @private - * @param {string} name - The name of the target window. - */ - constructor(name = '') { - /** - * @private - * @name {@link caller} - * @type {(...args: any[]) => any} - */ - this[caller] = newRuntimeCallerWithID(objectNames.Window, name) - - // bind instance method to make them easily usable in event handlers - for (const method of Object.getOwnPropertyNames(Window.prototype)) { - if ( - method !== "constructor" - && typeof this[method] === "function" - ) { - this[method] = this[method].bind(this); - } - } - } - - /** - * Gets the specified window. - * - * @public - * @param {string} name - The name of the window to get. - * @return {Window} - The corresponding window object. - */ - Get(name) { - return new Window(name); - } - - /** - * Returns the absolute position of the window. - * - * @public - * @return {Promise} - The current absolute position of the window. - */ - Position() { - return this[caller](PositionMethod); - } - - /** - * Centers the window on the screen. - * - * @public - * @return {Promise} - */ - Center() { - return this[caller](CenterMethod); - } - - /** - * Closes the window. - * - * @public - * @return {Promise} - */ - Close() { - return this[caller](CloseMethod); - } - - /** - * Disables min/max size constraints. - * - * @public - * @return {Promise} - */ - DisableSizeConstraints() { - return this[caller](DisableSizeConstraintsMethod); - } - - /** - * Enables min/max size constraints. - * - * @public - * @return {Promise} - */ - EnableSizeConstraints() { - return this[caller](EnableSizeConstraintsMethod); - } - - /** - * Focuses the window. - * - * @public - * @return {Promise} - */ - Focus() { - return this[caller](FocusMethod); - } - - /** - * Forces the window to reload the page assets. - * - * @public - * @return {Promise} - */ - ForceReload() { - return this[caller](ForceReloadMethod); - } - - /** - * Doc. - * - * @public - * @return {Promise} - */ - Fullscreen() { - return this[caller](FullscreenMethod); - } - - /** - * Returns the screen that the window is on. - * - * @public - * @return {Promise} - The screen the window is currently on - */ - GetScreen() { - return this[caller](GetScreenMethod); - } - - /** - * Returns the current zoom level of the window. - * - * @public - * @return {Promise} - The current zoom level - */ - GetZoom() { - return this[caller](GetZoomMethod); - } - - /** - * Returns the height of the window. - * - * @public - * @return {Promise} - The current height of the window - */ - Height() { - return this[caller](HeightMethod); - } - - /** - * Hides the window. - * - * @public - * @return {Promise} - */ - Hide() { - return this[caller](HideMethod); - } - - /** - * Returns true if the window is focused. - * - * @public - * @return {Promise} - Whether the window is currently focused - */ - IsFocused() { - return this[caller](IsFocusedMethod); - } - - /** - * Returns true if the window is fullscreen. - * - * @public - * @return {Promise} - Whether the window is currently fullscreen - */ - IsFullscreen() { - return this[caller](IsFullscreenMethod); - } - - /** - * Returns true if the window is maximised. - * - * @public - * @return {Promise} - Whether the window is currently maximised - */ - IsMaximised() { - return this[caller](IsMaximisedMethod); - } - - /** - * Returns true if the window is minimised. - * - * @public - * @return {Promise} - Whether the window is currently minimised - */ - IsMinimised() { - return this[caller](IsMinimisedMethod); - } - - /** - * Maximises the window. - * - * @public - * @return {Promise} - */ - Maximise() { - return this[caller](MaximiseMethod); - } - - /** - * Minimises the window. - * - * @public - * @return {Promise} - */ - Minimise() { - return this[caller](MinimiseMethod); - } - - /** - * Returns the name of the window. - * - * @public - * @return {Promise} - The name of the window - */ - Name() { - return this[caller](NameMethod); - } - - /** - * Opens the development tools pane. - * - * @public - * @return {Promise} - */ - OpenDevTools() { - return this[caller](OpenDevToolsMethod); - } - - /** - * Returns the relative position of the window to the screen. - * - * @public - * @return {Promise} - The current relative position of the window - */ - RelativePosition() { - return this[caller](RelativePositionMethod); - } - - /** - * Reloads the page assets. - * - * @public - * @return {Promise} - */ - Reload() { - return this[caller](ReloadMethod); - } - - /** - * Returns true if the window is resizable. - * - * @public - * @return {Promise} - Whether the window is currently resizable - */ - Resizable() { - return this[caller](ResizableMethod); - } - - /** - * Restores the window to its previous state if it was previously minimised, maximised or fullscreen. - * - * @public - * @return {Promise} - */ - Restore() { - return this[caller](RestoreMethod); - } - - /** - * Sets the absolute position of the window. - * - * @public - * @param {number} x - The desired horizontal absolute position of the window - * @param {number} y - The desired vertical absolute position of the window - * @return {Promise} - */ - SetPosition(x, y) { - return this[caller](SetPositionMethod, { x, y }); - } - - /** - * Sets the window to be always on top. - * - * @public - * @param {boolean} alwaysOnTop - Whether the window should stay on top - * @return {Promise} - */ - SetAlwaysOnTop(alwaysOnTop) { - return this[caller](SetAlwaysOnTopMethod, { alwaysOnTop }); - } - - /** - * Sets the background colour of the window. - * - * @public - * @param {number} r - The desired red component of the window background - * @param {number} g - The desired green component of the window background - * @param {number} b - The desired blue component of the window background - * @param {number} a - The desired alpha component of the window background - * @return {Promise} - */ - SetBackgroundColour(r, g, b, a) { - return this[caller](SetBackgroundColourMethod, { r, g, b, a }); - } - - /** - * Removes the window frame and title bar. - * - * @public - * @param {boolean} frameless - Whether the window should be frameless - * @return {Promise} - */ - SetFrameless(frameless) { - return this[caller](SetFramelessMethod, { frameless }); - } - - /** - * Disables the system fullscreen button. - * - * @public - * @param {boolean} enabled - Whether the fullscreen button should be enabled - * @return {Promise} - */ - SetFullscreenButtonEnabled(enabled) { - return this[caller](SetFullscreenButtonEnabledMethod, { enabled }); - } - - /** - * Sets the maximum size of the window. - * - * @public - * @param {number} width - The desired maximum width of the window - * @param {number} height - The desired maximum height of the window - * @return {Promise} - */ - SetMaxSize(width, height) { - return this[caller](SetMaxSizeMethod, { width, height }); - } - - /** - * Sets the minimum size of the window. - * - * @public - * @param {number} width - The desired minimum width of the window - * @param {number} height - The desired minimum height of the window - * @return {Promise} - */ - SetMinSize(width, height) { - return this[caller](SetMinSizeMethod, { width, height }); - } - - /** - * Sets the relative position of the window to the screen. - * - * @public - * @param {number} x - The desired horizontal relative position of the window - * @param {number} y - The desired vertical relative position of the window - * @return {Promise} - */ - SetRelativePosition(x, y) { - return this[caller](SetRelativePositionMethod, { x, y }); - } - - /** - * Sets whether the window is resizable. - * - * @public - * @param {boolean} resizable - Whether the window should be resizable - * @return {Promise} - */ - SetResizable(resizable) { - return this[caller](SetResizableMethod, { resizable }); - } - - /** - * Sets the size of the window. - * - * @public - * @param {number} width - The desired width of the window - * @param {number} height - The desired height of the window - * @return {Promise} - */ - SetSize(width, height) { - return this[caller](SetSizeMethod, { width, height }); - } - - /** - * Sets the title of the window. - * - * @public - * @param {string} title - The desired title of the window - * @return {Promise} - */ - SetTitle(title) { - return this[caller](SetTitleMethod, { title }); - } - - /** - * Sets the zoom level of the window. - * - * @public - * @param {number} zoom - The desired zoom level - * @return {Promise} - */ - SetZoom(zoom) { - return this[caller](SetZoomMethod, { zoom }); - } - - /** - * Shows the window. - * - * @public - * @return {Promise} - */ - Show() { - return this[caller](ShowMethod); - } - - /** - * Returns the size of the window. - * - * @public - * @return {Promise} - The current size of the window - */ - Size() { - return this[caller](SizeMethod); - } - - /** - * Toggles the window between fullscreen and normal. - * - * @public - * @return {Promise} - */ - ToggleFullscreen() { - return this[caller](ToggleFullscreenMethod); - } - - /** - * Toggles the window between maximised and normal. - * - * @public - * @return {Promise} - */ - ToggleMaximise() { - return this[caller](ToggleMaximiseMethod); - } - - /** - * Un-fullscreens the window. - * - * @public - * @return {Promise} - */ - UnFullscreen() { - return this[caller](UnFullscreenMethod); - } - - /** - * Un-maximises the window. - * - * @public - * @return {Promise} - */ - UnMaximise() { - return this[caller](UnMaximiseMethod); - } - - /** - * Un-minimises the window. - * - * @public - * @return {Promise} - */ - UnMinimise() { - return this[caller](UnMinimiseMethod); - } - - /** - * Returns the width of the window. - * - * @public - * @return {Promise} - The current width of the window - */ - Width() { - return this[caller](WidthMethod); - } - - /** - * Zooms the window. - * - * @public - * @return {Promise} - */ - Zoom() { - return this[caller](ZoomMethod); - } - - /** - * Increases the zoom level of the webview content. - * - * @public - * @return {Promise} - */ - ZoomIn() { - return this[caller](ZoomInMethod); - } - - /** - * Decreases the zoom level of the webview content. - * - * @public - * @return {Promise} - */ - ZoomOut() { - return this[caller](ZoomOutMethod); - } - - /** - * Resets the zoom level of the webview content. - * - * @public - * @return {Promise} - */ - ZoomReset() { - return this[caller](ZoomResetMethod); - } -} - -/** - * The window within which the script is running. - * - * @type {Window} - */ -const thisWindow = new Window(''); - -export default thisWindow; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts new file mode 100644 index 000000000..11c9c8b52 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts @@ -0,0 +1,528 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import {newRuntimeCaller, objectNames} from "./runtime.js"; +import type { Screen } from "./screens.js"; + +const PositionMethod = 0; +const CenterMethod = 1; +const CloseMethod = 2; +const DisableSizeConstraintsMethod = 3; +const EnableSizeConstraintsMethod = 4; +const FocusMethod = 5; +const ForceReloadMethod = 6; +const FullscreenMethod = 7; +const GetScreenMethod = 8; +const GetZoomMethod = 9; +const HeightMethod = 10; +const HideMethod = 11; +const IsFocusedMethod = 12; +const IsFullscreenMethod = 13; +const IsMaximisedMethod = 14; +const IsMinimisedMethod = 15; +const MaximiseMethod = 16; +const MinimiseMethod = 17; +const NameMethod = 18; +const OpenDevToolsMethod = 19; +const RelativePositionMethod = 20; +const ReloadMethod = 21; +const ResizableMethod = 22; +const RestoreMethod = 23; +const SetPositionMethod = 24; +const SetAlwaysOnTopMethod = 25; +const SetBackgroundColourMethod = 26; +const SetFramelessMethod = 27; +const SetFullscreenButtonEnabledMethod = 28; +const SetMaxSizeMethod = 29; +const SetMinSizeMethod = 30; +const SetRelativePositionMethod = 31; +const SetResizableMethod = 32; +const SetSizeMethod = 33; +const SetTitleMethod = 34; +const SetZoomMethod = 35; +const ShowMethod = 36; +const SizeMethod = 37; +const ToggleFullscreenMethod = 38; +const ToggleMaximiseMethod = 39; +const ToggleFramelessMethod = 40; +const UnFullscreenMethod = 41; +const UnMaximiseMethod = 42; +const UnMinimiseMethod = 43; +const WidthMethod = 44; +const ZoomMethod = 45; +const ZoomInMethod = 46; +const ZoomOutMethod = 47; +const ZoomResetMethod = 48; + +/** + * A record describing the position of a window. + */ +interface Position { + /** The horizontal position of the window. */ + x: number; + /** The vertical position of the window. */ + y: number; +} + +/** + * A record describing the size of a window. + */ +interface Size { + /** The width of the window. */ + width: number; + /** The height of the window. */ + height: number; +} + +// Private field names. +const callerSym = Symbol("caller"); + +class Window { + // Private fields. + private [callerSym]: (message: number, args?: any) => Promise; + + /** + * Initialises a window object with the specified name. + * + * @private + * @param name - The name of the target window. + */ + constructor(name: string = '') { + this[callerSym] = newRuntimeCaller(objectNames.Window, name) + + // bind instance method to make them easily usable in event handlers + for (const method of Object.getOwnPropertyNames(Window.prototype)) { + if ( + method !== "constructor" + && typeof (this as any)[method] === "function" + ) { + (this as any)[method] = (this as any)[method].bind(this); + } + } + } + + /** + * Gets the specified window. + * + * @param name - The name of the window to get. + * @returns The corresponding window object. + */ + Get(name: string): Window { + return new Window(name); + } + + /** + * Returns the absolute position of the window. + * + * @returns The current absolute position of the window. + */ + Position(): Promise { + return this[callerSym](PositionMethod); + } + + /** + * Centers the window on the screen. + */ + Center(): Promise { + return this[callerSym](CenterMethod); + } + + /** + * Closes the window. + */ + Close(): Promise { + return this[callerSym](CloseMethod); + } + + /** + * Disables min/max size constraints. + */ + DisableSizeConstraints(): Promise { + return this[callerSym](DisableSizeConstraintsMethod); + } + + /** + * Enables min/max size constraints. + */ + EnableSizeConstraints(): Promise { + return this[callerSym](EnableSizeConstraintsMethod); + } + + /** + * Focuses the window. + */ + Focus(): Promise { + return this[callerSym](FocusMethod); + } + + /** + * Forces the window to reload the page assets. + */ + ForceReload(): Promise { + return this[callerSym](ForceReloadMethod); + } + + /** + * Switches the window to fullscreen mode. + */ + Fullscreen(): Promise { + return this[callerSym](FullscreenMethod); + } + + /** + * Returns the screen that the window is on. + * + * @returns The screen the window is currently on. + */ + GetScreen(): Promise { + return this[callerSym](GetScreenMethod); + } + + /** + * Returns the current zoom level of the window. + * + * @returns The current zoom level. + */ + GetZoom(): Promise { + return this[callerSym](GetZoomMethod); + } + + /** + * Returns the height of the window. + * + * @returns The current height of the window. + */ + Height(): Promise { + return this[callerSym](HeightMethod); + } + + /** + * Hides the window. + */ + Hide(): Promise { + return this[callerSym](HideMethod); + } + + /** + * Returns true if the window is focused. + * + * @returns Whether the window is currently focused. + */ + IsFocused(): Promise { + return this[callerSym](IsFocusedMethod); + } + + /** + * Returns true if the window is fullscreen. + * + * @returns Whether the window is currently fullscreen. + */ + IsFullscreen(): Promise { + return this[callerSym](IsFullscreenMethod); + } + + /** + * Returns true if the window is maximised. + * + * @returns Whether the window is currently maximised. + */ + IsMaximised(): Promise { + return this[callerSym](IsMaximisedMethod); + } + + /** + * Returns true if the window is minimised. + * + * @returns Whether the window is currently minimised. + */ + IsMinimised(): Promise { + return this[callerSym](IsMinimisedMethod); + } + + /** + * Maximises the window. + */ + Maximise(): Promise { + return this[callerSym](MaximiseMethod); + } + + /** + * Minimises the window. + */ + Minimise(): Promise { + return this[callerSym](MinimiseMethod); + } + + /** + * Returns the name of the window. + * + * @returns The name of the window. + */ + Name(): Promise { + return this[callerSym](NameMethod); + } + + /** + * Opens the development tools pane. + */ + OpenDevTools(): Promise { + return this[callerSym](OpenDevToolsMethod); + } + + /** + * Returns the relative position of the window to the screen. + * + * @returns The current relative position of the window. + */ + RelativePosition(): Promise { + return this[callerSym](RelativePositionMethod); + } + + /** + * Reloads the page assets. + */ + Reload(): Promise { + return this[callerSym](ReloadMethod); + } + + /** + * Returns true if the window is resizable. + * + * @returns Whether the window is currently resizable. + */ + Resizable(): Promise { + return this[callerSym](ResizableMethod); + } + + /** + * Restores the window to its previous state if it was previously minimised, maximised or fullscreen. + */ + Restore(): Promise { + return this[callerSym](RestoreMethod); + } + + /** + * Sets the absolute position of the window. + * + * @param x - The desired horizontal absolute position of the window. + * @param y - The desired vertical absolute position of the window. + */ + SetPosition(x: number, y: number): Promise { + return this[callerSym](SetPositionMethod, { x, y }); + } + + /** + * Sets the window to be always on top. + * + * @param alwaysOnTop - Whether the window should stay on top. + */ + SetAlwaysOnTop(alwaysOnTop: boolean): Promise { + return this[callerSym](SetAlwaysOnTopMethod, { alwaysOnTop }); + } + + /** + * Sets the background colour of the window. + * + * @param r - The desired red component of the window background. + * @param g - The desired green component of the window background. + * @param b - The desired blue component of the window background. + * @param a - The desired alpha component of the window background. + */ + SetBackgroundColour(r: number, g: number, b: number, a: number): Promise { + return this[callerSym](SetBackgroundColourMethod, { r, g, b, a }); + } + + /** + * Removes the window frame and title bar. + * + * @param frameless - Whether the window should be frameless. + */ + SetFrameless(frameless: boolean): Promise { + return this[callerSym](SetFramelessMethod, { frameless }); + } + + /** + * Disables the system fullscreen button. + * + * @param enabled - Whether the fullscreen button should be enabled. + */ + SetFullscreenButtonEnabled(enabled: boolean): Promise { + return this[callerSym](SetFullscreenButtonEnabledMethod, { enabled }); + } + + /** + * Sets the maximum size of the window. + * + * @param width - The desired maximum width of the window. + * @param height - The desired maximum height of the window. + */ + SetMaxSize(width: number, height: number): Promise { + return this[callerSym](SetMaxSizeMethod, { width, height }); + } + + /** + * Sets the minimum size of the window. + * + * @param width - The desired minimum width of the window. + * @param height - The desired minimum height of the window. + */ + SetMinSize(width: number, height: number): Promise { + return this[callerSym](SetMinSizeMethod, { width, height }); + } + + /** + * Sets the relative position of the window to the screen. + * + * @param x - The desired horizontal relative position of the window. + * @param y - The desired vertical relative position of the window. + */ + SetRelativePosition(x: number, y: number): Promise { + return this[callerSym](SetRelativePositionMethod, { x, y }); + } + + /** + * Sets whether the window is resizable. + * + * @param resizable - Whether the window should be resizable. + */ + SetResizable(resizable: boolean): Promise { + return this[callerSym](SetResizableMethod, { resizable }); + } + + /** + * Sets the size of the window. + * + * @param width - The desired width of the window. + * @param height - The desired height of the window. + */ + SetSize(width: number, height: number): Promise { + return this[callerSym](SetSizeMethod, { width, height }); + } + + /** + * Sets the title of the window. + * + * @param title - The desired title of the window. + */ + SetTitle(title: string): Promise { + return this[callerSym](SetTitleMethod, { title }); + } + + /** + * Sets the zoom level of the window. + * + * @param zoom - The desired zoom level. + */ + SetZoom(zoom: number): Promise { + return this[callerSym](SetZoomMethod, { zoom }); + } + + /** + * Shows the window. + */ + Show(): Promise { + return this[callerSym](ShowMethod); + } + + /** + * Returns the size of the window. + * + * @returns The current size of the window. + */ + Size(): Promise { + return this[callerSym](SizeMethod); + } + + /** + * Toggles the window between fullscreen and normal. + */ + ToggleFullscreen(): Promise { + return this[callerSym](ToggleFullscreenMethod); + } + + /** + * Toggles the window between maximised and normal. + */ + ToggleMaximise(): Promise { + return this[callerSym](ToggleMaximiseMethod); + } + + /** + * Toggles the window between frameless and normal. + */ + ToggleFrameless(): Promise { + return this[callerSym](ToggleFramelessMethod); + } + + /** + * Un-fullscreens the window. + */ + UnFullscreen(): Promise { + return this[callerSym](UnFullscreenMethod); + } + + /** + * Un-maximises the window. + */ + UnMaximise(): Promise { + return this[callerSym](UnMaximiseMethod); + } + + /** + * Un-minimises the window. + */ + UnMinimise(): Promise { + return this[callerSym](UnMinimiseMethod); + } + + /** + * Returns the width of the window. + * + * @returns The current width of the window. + */ + Width(): Promise { + return this[callerSym](WidthMethod); + } + + /** + * Zooms the window. + */ + Zoom(): Promise { + return this[callerSym](ZoomMethod); + } + + /** + * Increases the zoom level of the webview content. + */ + ZoomIn(): Promise { + return this[callerSym](ZoomInMethod); + } + + /** + * Decreases the zoom level of the webview content. + */ + ZoomOut(): Promise { + return this[callerSym](ZoomOutMethod); + } + + /** + * Resets the zoom level of the webview content. + */ + ZoomReset(): Promise { + return this[callerSym](ZoomResetMethod); + } +} + +/** + * The window within which the script is running. + */ +const thisWindow = new Window(''); + +export default thisWindow; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.js deleted file mode 100644 index 819758514..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.js +++ /dev/null @@ -1,250 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -import {OpenURL} from "./browser"; -import {Question} from "./dialogs"; -import {Emit, WailsEvent} from "./events"; -import {canAbortListeners, whenReady} from "./utils"; -import Window from "./window"; - -/** - * Sends an event with the given name and optional data. - * - * @param {string} eventName - The name of the event to send. - * @param {any} [data=null] - Optional data to send along with the event. - * - * @return {void} - */ -function sendEvent(eventName, data=null) { - Emit(new WailsEvent(eventName, data)); -} - -/** - * Calls a method on a specified window. - * @param {string} windowName - The name of the window to call the method on. - * @param {string} methodName - The name of the method to call. - */ -function callWindowMethod(windowName, methodName) { - const targetWindow = Window.Get(windowName); - const method = targetWindow[methodName]; - - if (typeof method !== "function") { - console.error(`Window method '${methodName}' not found`); - return; - } - - try { - method.call(targetWindow); - } catch (e) { - console.error(`Error calling window method '${methodName}': `, e); - } -} - -/** - * Responds to a triggering event by running appropriate WML actions for the current target - * - * @param {Event} ev - * @return {void} - */ -function onWMLTriggered(ev) { - const element = ev.currentTarget; - - function runEffect(choice = "Yes") { - if (choice !== "Yes") - return; - - const eventType = element.getAttribute('data-wml-event'); - const targetWindow = element.getAttribute('data-wml-target-window') || ""; - const windowMethod = element.getAttribute('data-wml-window'); - const url = element.getAttribute('data-wml-openURL'); - - if (eventType !== null) - sendEvent(eventType); - if (windowMethod !== null) - callWindowMethod(targetWindow, windowMethod); - if (url !== null) - void OpenURL(url); - } - - const confirm = element.getAttribute('data-wml-confirm'); - - if (confirm) { - Question({ - Title: "Confirm", - Message: confirm, - Detached: false, - Buttons: [ - { Label: "Yes" }, - { Label: "No", IsDefault: true } - ] - }).then(runEffect); - } else { - runEffect(); - } -} - -/** - * @type {symbol} - */ -const controller = Symbol(); - -/** - * AbortControllerRegistry does not actually remember active event listeners: instead - * it ties them to an AbortSignal and uses an AbortController to remove them all at once. - */ -class AbortControllerRegistry { - constructor() { - /** - * Stores the AbortController that can be used to remove all currently active listeners. - * - * @private - * @name {@link controller} - * @member {AbortController} - */ - this[controller] = new AbortController(); - } - - /** - * Returns an options object for addEventListener that ties the listener - * to the AbortSignal from the current AbortController. - * - * @param {HTMLElement} element An HTML element - * @param {string[]} triggers The list of active WML trigger events for the specified elements - * @returns {AddEventListenerOptions} - */ - set(element, triggers) { - return { signal: this[controller].signal }; - } - - /** - * Removes all registered event listeners. - * - * @returns {void} - */ - reset() { - this[controller].abort(); - this[controller] = new AbortController(); - } -} - -/** - * @type {symbol} - */ -const triggerMap = Symbol(); - -/** - * @type {symbol} - */ -const elementCount = Symbol(); - -/** - * WeakMapRegistry maps active trigger events to each DOM element through a WeakMap. - * This ensures that the mapping remains private to this module, while still allowing garbage - * collection of the involved elements. - */ -class WeakMapRegistry { - constructor() { - /** - * Stores the current element-to-trigger mapping. - * - * @private - * @name {@link triggerMap} - * @member {WeakMap} - */ - this[triggerMap] = new WeakMap(); - - /** - * Counts the number of elements with active WML triggers. - * - * @private - * @name {@link elementCount} - * @member {number} - */ - this[elementCount] = 0; - } - - /** - * Sets the active triggers for the specified element. - * - * @param {HTMLElement} element An HTML element - * @param {string[]} triggers The list of active WML trigger events for the specified element - * @returns {AddEventListenerOptions} - */ - set(element, triggers) { - this[elementCount] += !this[triggerMap].has(element); - this[triggerMap].set(element, triggers); - return {}; - } - - /** - * Removes all registered event listeners. - * - * @returns {void} - */ - reset() { - if (this[elementCount] <= 0) - return; - - for (const element of document.body.querySelectorAll('*')) { - if (this[elementCount] <= 0) - break; - - const triggers = this[triggerMap].get(element); - this[elementCount] -= (typeof triggers !== "undefined"); - - for (const trigger of triggers || []) - element.removeEventListener(trigger, onWMLTriggered); - } - - this[triggerMap] = new WeakMap(); - this[elementCount] = 0; - } -} - -const triggerRegistry = canAbortListeners() ? new AbortControllerRegistry() : new WeakMapRegistry(); - -/** - * Adds event listeners to the specified element. - * - * @param {HTMLElement} element - * @return {void} - */ -function addWMLListeners(element) { - const triggerRegExp = /\S+/g; - const triggerAttr = (element.getAttribute('data-wml-trigger') || "click"); - const triggers = []; - - let match; - while ((match = triggerRegExp.exec(triggerAttr)) !== null) - triggers.push(match[0]); - - const options = triggerRegistry.set(element, triggers); - for (const trigger of triggers) - element.addEventListener(trigger, onWMLTriggered, options); -} - -/** - * Schedules an automatic reload of WML to be performed as soon as the document is fully loaded. - * - * @return {void} - */ -export function Enable() { - whenReady(Reload); -} - -/** - * Reloads the WML page by adding necessary event listeners and browser listeners. - * - * @return {void} - */ -export function Reload() { - triggerRegistry.reset(); - document.body.querySelectorAll('[data-wml-event], [data-wml-window], [data-wml-openURL]').forEach(addWMLListeners); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.ts new file mode 100644 index 000000000..148432d64 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.ts @@ -0,0 +1,209 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { OpenURL } from "./browser.js"; +import { Question } from "./dialogs.js"; +import { Emit } from "./events.js"; +import { canAbortListeners, whenReady } from "./utils.js"; +import Window from "./window.js"; + +/** + * Sends an event with the given name and optional data. + * + * @param eventName - - The name of the event to send. + * @param [data=null] - - Optional data to send along with the event. + */ +function sendEvent(eventName: string, data: any = null): void { + void Emit(eventName, data); +} + +/** + * Calls a method on a specified window. + * + * @param windowName - The name of the window to call the method on. + * @param methodName - The name of the method to call. + */ +function callWindowMethod(windowName: string, methodName: string) { + const targetWindow = Window.Get(windowName); + const method = (targetWindow as any)[methodName]; + + if (typeof method !== "function") { + console.error(`Window method '${methodName}' not found`); + return; + } + + try { + method.call(targetWindow); + } catch (e) { + console.error(`Error calling window method '${methodName}': `, e); + } +} + +/** + * Responds to a triggering event by running appropriate WML actions for the current target. + */ +function onWMLTriggered(ev: Event): void { + const element = ev.currentTarget as Element; + + function runEffect(choice = "Yes") { + if (choice !== "Yes") + return; + + const eventType = element.getAttribute('wml-event') || element.getAttribute('data-wml-event'); + const targetWindow = element.getAttribute('wml-target-window') || element.getAttribute('data-wml-target-window') || ""; + const windowMethod = element.getAttribute('wml-window') || element.getAttribute('data-wml-window'); + const url = element.getAttribute('wml-openurl') || element.getAttribute('data-wml-openurl'); + + if (eventType !== null) + sendEvent(eventType); + if (windowMethod !== null) + callWindowMethod(targetWindow, windowMethod); + if (url !== null) + void OpenURL(url); + } + + const confirm = element.getAttribute('wml-confirm') || element.getAttribute('data-wml-confirm'); + + if (confirm) { + Question({ + Title: "Confirm", + Message: confirm, + Detached: false, + Buttons: [ + { Label: "Yes" }, + { Label: "No", IsDefault: true } + ] + }).then(runEffect); + } else { + runEffect(); + } +} + +// Private field names. +const controllerSym = Symbol("controller"); +const triggerMapSym = Symbol("triggerMap"); +const elementCountSym = Symbol("elementCount"); + +/** + * AbortControllerRegistry does not actually remember active event listeners: instead + * it ties them to an AbortSignal and uses an AbortController to remove them all at once. + */ +class AbortControllerRegistry { + // Private fields. + [controllerSym]: AbortController; + + constructor() { + this[controllerSym] = new AbortController(); + } + + /** + * Returns an options object for addEventListener that ties the listener + * to the AbortSignal from the current AbortController. + * + * @param element - An HTML element + * @param triggers - The list of active WML trigger events for the specified elements + */ + set(element: Element, triggers: string[]): AddEventListenerOptions { + return { signal: this[controllerSym].signal }; + } + + /** + * Removes all registered event listeners and resets the registry. + */ + reset(): void { + this[controllerSym].abort(); + this[controllerSym] = new AbortController(); + } +} + +/** + * WeakMapRegistry maps active trigger events to each DOM element through a WeakMap. + * This ensures that the mapping remains private to this module, while still allowing garbage + * collection of the involved elements. + */ +class WeakMapRegistry { + /** Stores the current element-to-trigger mapping. */ + [triggerMapSym]: WeakMap; + /** Counts the number of elements with active WML triggers. */ + [elementCountSym]: number; + + constructor() { + this[triggerMapSym] = new WeakMap(); + this[elementCountSym] = 0; + } + + /** + * Sets active triggers for the specified element. + * + * @param element - An HTML element + * @param triggers - The list of active WML trigger events for the specified element + */ + set(element: Element, triggers: string[]): AddEventListenerOptions { + if (!this[triggerMapSym].has(element)) { this[elementCountSym]++; } + this[triggerMapSym].set(element, triggers); + return {}; + } + + /** + * Removes all registered event listeners. + */ + reset(): void { + if (this[elementCountSym] <= 0) + return; + + for (const element of document.body.querySelectorAll('*')) { + if (this[elementCountSym] <= 0) + break; + + const triggers = this[triggerMapSym].get(element); + if (triggers != null) { this[elementCountSym]--; } + + for (const trigger of triggers || []) + element.removeEventListener(trigger, onWMLTriggered); + } + + this[triggerMapSym] = new WeakMap(); + this[elementCountSym] = 0; + } +} + +const triggerRegistry = canAbortListeners() ? new AbortControllerRegistry() : new WeakMapRegistry(); + +/** + * Adds event listeners to the specified element. + */ +function addWMLListeners(element: Element): void { + const triggerRegExp = /\S+/g; + const triggerAttr = (element.getAttribute('wml-trigger') || element.getAttribute('data-wml-trigger') || "click"); + const triggers: string[] = []; + + let match; + while ((match = triggerRegExp.exec(triggerAttr)) !== null) + triggers.push(match[0]); + + const options = triggerRegistry.set(element, triggers); + for (const trigger of triggers) + element.addEventListener(trigger, onWMLTriggered, options); +} + +/** + * Schedules an automatic reload of WML to be performed as soon as the document is fully loaded. + */ +export function Enable(): void { + whenReady(Reload); +} + +/** + * Reloads the WML page by adding necessary event listeners and browser listeners. + */ +export function Reload(): void { + triggerRegistry.reset(); + document.body.querySelectorAll('[wml-event], [wml-window], [wml-openurl], [data-wml-event], [data-wml-window], [data-wml-openurl]').forEach(addWMLListeners); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.json b/v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.json index 2cbc06eed..1175a4af4 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.json +++ b/v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.json @@ -1,20 +1,36 @@ { "include": ["./src/**/*"], + "exclude": ["./src/**/*.test.*"], "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, + "composite": true, + + "allowJs": false, + + "noEmitOnError": true, "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", + "declarationMap": false, + "declarationDir": "types", "outDir": "dist", + "rootDir": "src", + + "target": "ES2017", + "module": "ES2015", + "moduleResolution": "bundler", + "isolatedModules": true, + "verbatimModuleSyntax": true, + "stripInternal": true, + + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "strict": true, - "target": "es6", + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitAny": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true } } \ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/application.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/application.d.ts deleted file mode 100644 index 5794550c0..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/application.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Hides a certain method by calling the HideMethod function. - * - * @return {Promise} - * - */ -export function Hide(): Promise; -/** - * Calls the ShowMethod and returns the result. - * - * @return {Promise} - */ -export function Show(): Promise; -/** - * Calls the QuitMethod to terminate the program. - * - * @return {Promise} - */ -export function Quit(): Promise; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/browser.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/browser.d.ts deleted file mode 100644 index 95e0ba377..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/browser.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Open a browser window to the given URL - * @param {string} url - The URL to open - * @returns {Promise} - */ -export function OpenURL(url: string): Promise; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/calls.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/calls.d.ts deleted file mode 100644 index 44f9eea0c..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/calls.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Call method. - * - * @param {Object} options - The options for the method. - * @returns {Object} - The result of the call. - */ -export function Call(options: any): any; -/** - * Executes a method by name. - * - * @param {string} methodName - The name of the method in the format 'package.struct.method'. - * @param {...*} args - The arguments to pass to the method. - * @throws {Error} If the name is not a string or is not in the correct format. - * @returns {*} The result of the method execution. - */ -export function ByName(methodName: string, ...args: any[]): any; -/** - * Calls a method by its ID with the specified arguments. - * - * @param {number} methodID - The ID of the method to call. - * @param {...*} args - The arguments to pass to the method. - * @return {*} - The result of the method call. - */ -export function ByID(methodID: number, ...args: any[]): any; -/** - * Calls a method on a plugin. - * - * @param {string} pluginName - The name of the plugin. - * @param {string} methodName - The name of the method to call. - * @param {...*} args - The arguments to pass to the method. - * @returns {*} - The result of the method call. - */ -export function Plugin(pluginName: string, methodName: string, ...args: any[]): any; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/clipboard.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/clipboard.d.ts deleted file mode 100644 index 8f7bfabc1..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/clipboard.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Sets the text to the Clipboard. - * - * @param {string} text - The text to be set to the Clipboard. - * @return {Promise} - A Promise that resolves when the operation is successful. - */ -export function SetText(text: string): Promise; -/** - * Get the Clipboard text - * @returns {Promise} A promise that resolves with the text from the Clipboard. - */ -export function Text(): Promise; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/contextmenu.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/contextmenu.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/contextmenu.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/create.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/create.d.ts deleted file mode 100644 index 10c505ff8..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/create.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Any is a dummy creation function for simple or unknown types. - * @template T - * @param {any} source - * @returns {T} - */ -export function Any(source: any): T; -/** - * ByteSlice is a creation function that replaces - * null strings with empty strings. - * @param {any} source - * @returns {string} - */ -export function ByteSlice(source: any): string; -/** - * Array takes a creation function for an arbitrary type - * and returns an in-place creation function for an array - * whose elements are of that type. - * @template T - * @param {(source: any) => T} element - * @returns {(source: any) => T[]} - */ -export function Array(element: (source: any) => T): (source: any) => T[]; -/** - * Map takes creation functions for two arbitrary types - * and returns an in-place creation function for an object - * whose keys and values are of those types. - * @template K, V - * @param {(source: any) => K} key - * @param {(source: any) => V} value - * @returns {(source: any) => { [_: K]: V }} - */ -export function Map(key: (source: any) => K, value: (source: any) => V): (source: any) => {}; -/** - * Nullable takes a creation function for an arbitrary type - * and returns a creation function for a nullable value of that type. - * @template T - * @param {(source: any) => T} element - * @returns {(source: any) => (T | null)} - */ -export function Nullable(element: (source: any) => T): (source: any) => T; -/** - * Struct takes an object mapping field names to creation functions - * and returns an in-place creation function for a struct. - * @template {{ [_: string]: ((source: any) => any) }} T - * @template {{ [Key in keyof T]?: ReturnType }} U - * @param {T} createField - * @returns {(source: any) => U} - */ -export function Struct any; -}, U extends { [Key in keyof T]?: ReturnType; }>(createField: T): (source: any) => U; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/dialogs.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/dialogs.d.ts deleted file mode 100644 index 1a90bb0ec..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/dialogs.d.ts +++ /dev/null @@ -1,184 +0,0 @@ -export function Info(options: MessageDialogOptions): Promise; -export function Warning(options: MessageDialogOptions): Promise; -export function Error(options: MessageDialogOptions): Promise; -export function Question(options: MessageDialogOptions): Promise; -export function OpenFile(options: OpenFileDialogOptions): Promise; -export function SaveFile(options: SaveFileDialogOptions): Promise; -export type OpenFileDialogOptions = { - /** - * - Indicates if directories can be chosen. - */ - CanChooseDirectories?: boolean; - /** - * - Indicates if files can be chosen. - */ - CanChooseFiles?: boolean; - /** - * - Indicates if directories can be created. - */ - CanCreateDirectories?: boolean; - /** - * - Indicates if hidden files should be shown. - */ - ShowHiddenFiles?: boolean; - /** - * - Indicates if aliases should be resolved. - */ - ResolvesAliases?: boolean; - /** - * - Indicates if multiple selection is allowed. - */ - AllowsMultipleSelection?: boolean; - /** - * - Indicates if the extension should be hidden. - */ - HideExtension?: boolean; - /** - * - Indicates if hidden extensions can be selected. - */ - CanSelectHiddenExtension?: boolean; - /** - * - Indicates if file packages should be treated as directories. - */ - TreatsFilePackagesAsDirectories?: boolean; - /** - * - Indicates if other file types are allowed. - */ - AllowsOtherFiletypes?: boolean; - /** - * - Array of file filters. - */ - Filters?: FileFilter[]; - /** - * - Title of the dialog. - */ - Title?: string; - /** - * - Message to show in the dialog. - */ - Message?: string; - /** - * - Text to display on the button. - */ - ButtonText?: string; - /** - * - Directory to open in the dialog. - */ - Directory?: string; - /** - * - Indicates if the dialog should appear detached from the main window. - */ - Detached?: boolean; -}; -export type SaveFileDialogOptions = { - /** - * - Default filename to use in the dialog. - */ - Filename?: string; - /** - * - Indicates if directories can be chosen. - */ - CanChooseDirectories?: boolean; - /** - * - Indicates if files can be chosen. - */ - CanChooseFiles?: boolean; - /** - * - Indicates if directories can be created. - */ - CanCreateDirectories?: boolean; - /** - * - Indicates if hidden files should be shown. - */ - ShowHiddenFiles?: boolean; - /** - * - Indicates if aliases should be resolved. - */ - ResolvesAliases?: boolean; - /** - * - Indicates if multiple selection is allowed. - */ - AllowsMultipleSelection?: boolean; - /** - * - Indicates if the extension should be hidden. - */ - HideExtension?: boolean; - /** - * - Indicates if hidden extensions can be selected. - */ - CanSelectHiddenExtension?: boolean; - /** - * - Indicates if file packages should be treated as directories. - */ - TreatsFilePackagesAsDirectories?: boolean; - /** - * - Indicates if other file types are allowed. - */ - AllowsOtherFiletypes?: boolean; - /** - * - Array of file filters. - */ - Filters?: FileFilter[]; - /** - * - Title of the dialog. - */ - Title?: string; - /** - * - Message to show in the dialog. - */ - Message?: string; - /** - * - Text to display on the button. - */ - ButtonText?: string; - /** - * - Directory to open in the dialog. - */ - Directory?: string; - /** - * - Indicates if the dialog should appear detached from the main window. - */ - Detached?: boolean; -}; -export type MessageDialogOptions = { - /** - * - The title of the dialog window. - */ - Title?: string; - /** - * - The main message to show in the dialog. - */ - Message?: string; - /** - * - Array of button options to show in the dialog. - */ - Buttons?: Button[]; - /** - * - True if the dialog should appear detached from the main window (if applicable). - */ - Detached?: boolean; -}; -export type Button = { - /** - * - Text that appears within the button. - */ - Label?: string; - /** - * - True if the button should cancel an operation when clicked. - */ - IsCancel?: boolean; - /** - * - True if the button should be the default action when the user presses enter. - */ - IsDefault?: boolean; -}; -export type FileFilter = { - /** - * - Display name for the filter, it could be "Text Files", "Images" etc. - */ - DisplayName?: string; - /** - * - Pattern to match for the filter, e.g. "*.txt;*.md" for text markdown files. - */ - Pattern?: string; -}; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/drag.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/drag.d.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/drag.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/event_types.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/event_types.d.ts deleted file mode 100644 index e8742de59..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/event_types.d.ts +++ /dev/null @@ -1,219 +0,0 @@ - -export declare const EventTypes: { - Windows: { - APMPowerSettingChange: string, - APMPowerStatusChange: string, - APMResumeAutomatic: string, - APMResumeSuspend: string, - APMSuspend: string, - ApplicationStarted: string, - SystemThemeChanged: string, - WebViewNavigationCompleted: string, - WindowActive: string, - WindowBackgroundErase: string, - WindowClickActive: string, - WindowClosing: string, - WindowDidMove: string, - WindowDidResize: string, - WindowDPIChanged: string, - WindowDragDrop: string, - WindowDragEnter: string, - WindowDragLeave: string, - WindowDragOver: string, - WindowEndMove: string, - WindowEndResize: string, - WindowFullscreen: string, - WindowHide: string, - WindowInactive: string, - WindowKeyDown: string, - WindowKeyUp: string, - WindowKillFocus: string, - WindowNonClientHit: string, - WindowNonClientMouseDown: string, - WindowNonClientMouseLeave: string, - WindowNonClientMouseMove: string, - WindowNonClientMouseUp: string, - WindowPaint: string, - WindowRestore: string, - WindowSetFocus: string, - WindowShow: string, - WindowStartMove: string, - WindowStartResize: string, - WindowUnFullscreen: string, - WindowZOrderChanged: string, - WindowMinimise: string, - WindowUnMinimise: string, - WindowMaximise: string, - WindowUnMaximise: string, - }, - Mac: { - ApplicationDidBecomeActive: string, - ApplicationDidChangeBackingProperties: string, - ApplicationDidChangeEffectiveAppearance: string, - ApplicationDidChangeIcon: string, - ApplicationDidChangeOcclusionState: string, - ApplicationDidChangeScreenParameters: string, - ApplicationDidChangeStatusBarFrame: string, - ApplicationDidChangeStatusBarOrientation: string, - ApplicationDidChangeTheme: string, - ApplicationDidFinishLaunching: string, - ApplicationDidHide: string, - ApplicationDidResignActive: string, - ApplicationDidUnhide: string, - ApplicationDidUpdate: string, - ApplicationShouldHandleReopen: string, - ApplicationWillBecomeActive: string, - ApplicationWillFinishLaunching: string, - ApplicationWillHide: string, - ApplicationWillResignActive: string, - ApplicationWillTerminate: string, - ApplicationWillUnhide: string, - ApplicationWillUpdate: string, - MenuDidAddItem: string, - MenuDidBeginTracking: string, - MenuDidClose: string, - MenuDidDisplayItem: string, - MenuDidEndTracking: string, - MenuDidHighlightItem: string, - MenuDidOpen: string, - MenuDidPopUp: string, - MenuDidRemoveItem: string, - MenuDidSendAction: string, - MenuDidSendActionToItem: string, - MenuDidUpdate: string, - MenuWillAddItem: string, - MenuWillBeginTracking: string, - MenuWillDisplayItem: string, - MenuWillEndTracking: string, - MenuWillHighlightItem: string, - MenuWillOpen: string, - MenuWillPopUp: string, - MenuWillRemoveItem: string, - MenuWillSendAction: string, - MenuWillSendActionToItem: string, - MenuWillUpdate: string, - WebViewDidCommitNavigation: string, - WebViewDidFinishNavigation: string, - WebViewDidReceiveServerRedirectForProvisionalNavigation: string, - WebViewDidStartProvisionalNavigation: string, - WindowDidBecomeKey: string, - WindowDidBecomeMain: string, - WindowDidBeginSheet: string, - WindowDidChangeAlpha: string, - WindowDidChangeBackingLocation: string, - WindowDidChangeBackingProperties: string, - WindowDidChangeCollectionBehavior: string, - WindowDidChangeEffectiveAppearance: string, - WindowDidChangeOcclusionState: string, - WindowDidChangeOrderingMode: string, - WindowDidChangeScreen: string, - WindowDidChangeScreenParameters: string, - WindowDidChangeScreenProfile: string, - WindowDidChangeScreenSpace: string, - WindowDidChangeScreenSpaceProperties: string, - WindowDidChangeSharingType: string, - WindowDidChangeSpace: string, - WindowDidChangeSpaceOrderingMode: string, - WindowDidChangeTitle: string, - WindowDidChangeToolbar: string, - WindowDidDeminiaturize: string, - WindowDidEndSheet: string, - WindowDidEnterFullScreen: string, - WindowDidEnterVersionBrowser: string, - WindowDidExitFullScreen: string, - WindowDidExitVersionBrowser: string, - WindowDidExpose: string, - WindowDidFocus: string, - WindowDidMiniaturize: string, - WindowDidMove: string, - WindowDidOrderOffScreen: string, - WindowDidOrderOnScreen: string, - WindowDidResignKey: string, - WindowDidResignMain: string, - WindowDidResize: string, - WindowDidUpdate: string, - WindowDidUpdateAlpha: string, - WindowDidUpdateCollectionBehavior: string, - WindowDidUpdateCollectionProperties: string, - WindowDidUpdateShadow: string, - WindowDidUpdateTitle: string, - WindowDidUpdateToolbar: string, - WindowDidZoom: string, - WindowFileDraggingEntered: string, - WindowFileDraggingExited: string, - WindowFileDraggingPerformed: string, - WindowHide: string, - WindowMaximise: string, - WindowUnMaximise: string, - WindowMinimise: string, - WindowUnMinimise: string, - WindowShouldClose: string, - WindowShow: string, - WindowWillBecomeKey: string, - WindowWillBecomeMain: string, - WindowWillBeginSheet: string, - WindowWillChangeOrderingMode: string, - WindowWillClose: string, - WindowWillDeminiaturize: string, - WindowWillEnterFullScreen: string, - WindowWillEnterVersionBrowser: string, - WindowWillExitFullScreen: string, - WindowWillExitVersionBrowser: string, - WindowWillFocus: string, - WindowWillMiniaturize: string, - WindowWillMove: string, - WindowWillOrderOffScreen: string, - WindowWillOrderOnScreen: string, - WindowWillResignMain: string, - WindowWillResize: string, - WindowWillUnfocus: string, - WindowWillUpdate: string, - WindowWillUpdateAlpha: string, - WindowWillUpdateCollectionBehavior: string, - WindowWillUpdateCollectionProperties: string, - WindowWillUpdateShadow: string, - WindowWillUpdateTitle: string, - WindowWillUpdateToolbar: string, - WindowWillUpdateVisibility: string, - WindowWillUseStandardFrame: string, - WindowZoomIn: string, - WindowZoomOut: string, - WindowZoomReset: string, - }, - Linux: { - ApplicationStartup: string, - SystemThemeChanged: string, - WindowDeleteEvent: string, - WindowDidMove: string, - WindowDidResize: string, - WindowFocusIn: string, - WindowFocusOut: string, - WindowLoadChanged: string, - }, - Common: { - ApplicationOpenedWithFile: string, - ApplicationStarted: string, - ThemeChanged: string, - WindowClosing: string, - WindowDidMove: string, - WindowDidResize: string, - WindowDPIChanged: string, - WindowFilesDropped: string, - WindowFocus: string, - WindowFullscreen: string, - WindowHide: string, - WindowLostFocus: string, - WindowMaximise: string, - WindowMinimise: string, - WindowRestore: string, - WindowRuntimeReady: string, - WindowShow: string, - WindowUnFullscreen: string, - WindowUnMaximise: string, - WindowUnMinimise: string, - WindowZoom: string, - WindowZoomIn: string, - WindowZoomOut: string, - WindowZoomReset: string, - }, -}; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/events.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/events.d.ts deleted file mode 100644 index 7cded34c5..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/events.d.ts +++ /dev/null @@ -1,271 +0,0 @@ -export function setup(): void; -/** - * Register a callback function to be called multiple times for a specific event. - * - * @param {string} eventName - The name of the event to register the callback for. - * @param {function} callback - The callback function to be called when the event is triggered. - * @param {number} maxCallbacks - The maximum number of times the callback can be called for the event. Once the maximum number is reached, the callback will no longer be called. - * - @return {function} - A function that, when called, will unregister the callback from the event. - */ -export function OnMultiple(eventName: string, callback: Function, maxCallbacks: number): Function; -/** - * Registers a callback function to be executed when the specified event occurs. - * - * @param {string} eventName - The name of the event. - * @param {function} callback - The callback function to be executed. It takes no parameters. - * @return {function} - A function that, when called, will unregister the callback from the event. */ -export function On(eventName: string, callback: Function): Function; -/** - * Registers a callback function to be executed only once for the specified event. - * - * @param {string} eventName - The name of the event. - * @param {function} callback - The function to be executed when the event occurs. - * @return {function} - A function that, when called, will unregister the callback from the event. - */ -export function Once(eventName: string, callback: Function): Function; -/** - * Removes event listeners for the specified event names. - * - * @param {string} eventName - The name of the event to remove listeners for. - * @param {...string} additionalEventNames - Additional event names to remove listeners for. - * @return {undefined} - */ -export function Off(eventName: string, ...additionalEventNames: string[]): undefined; -/** - * Removes all event listeners. - * - * @function OffAll - * @returns {void} - */ -export function OffAll(): void; -/** - * Emits an event using the given event name. - * - * @param {WailsEvent} event - The name of the event to emit. - * @returns {any} - The result of the emitted event. - */ -export function Emit(event: WailsEvent): any; -export const Types: { - Windows: { - APMPowerSettingChange: string; - APMPowerStatusChange: string; - APMResumeAutomatic: string; - APMResumeSuspend: string; - APMSuspend: string; - ApplicationStarted: string; - SystemThemeChanged: string; - WebViewNavigationCompleted: string; - WindowActive: string; - WindowBackgroundErase: string; - WindowClickActive: string; - WindowClosing: string; - WindowDidMove: string; - WindowDidResize: string; - WindowDPIChanged: string; - WindowDragDrop: string; - WindowDragEnter: string; - WindowDragLeave: string; - WindowDragOver: string; - WindowEndMove: string; - WindowEndResize: string; - WindowFullscreen: string; - WindowHide: string; - WindowInactive: string; - WindowKeyDown: string; - WindowKeyUp: string; - WindowKillFocus: string; - WindowNonClientHit: string; - WindowNonClientMouseDown: string; - WindowNonClientMouseLeave: string; - WindowNonClientMouseMove: string; - WindowNonClientMouseUp: string; - WindowPaint: string; - WindowRestore: string; - WindowSetFocus: string; - WindowShow: string; - WindowStartMove: string; - WindowStartResize: string; - WindowUnFullscreen: string; - WindowZOrderChanged: string; - WindowMinimise: string; - WindowUnMinimise: string; - WindowMaximise: string; - WindowUnMaximise: string; - }; - Mac: { - ApplicationDidBecomeActive: string; - ApplicationDidChangeBackingProperties: string; - ApplicationDidChangeEffectiveAppearance: string; - ApplicationDidChangeIcon: string; - ApplicationDidChangeOcclusionState: string; - ApplicationDidChangeScreenParameters: string; - ApplicationDidChangeStatusBarFrame: string; - ApplicationDidChangeStatusBarOrientation: string; - ApplicationDidChangeTheme: string; - ApplicationDidFinishLaunching: string; - ApplicationDidHide: string; - ApplicationDidResignActive: string; - ApplicationDidUnhide: string; - ApplicationDidUpdate: string; - ApplicationShouldHandleReopen: string; - ApplicationWillBecomeActive: string; - ApplicationWillFinishLaunching: string; - ApplicationWillHide: string; - ApplicationWillResignActive: string; - ApplicationWillTerminate: string; - ApplicationWillUnhide: string; - ApplicationWillUpdate: string; - MenuDidAddItem: string; - MenuDidBeginTracking: string; - MenuDidClose: string; - MenuDidDisplayItem: string; - MenuDidEndTracking: string; - MenuDidHighlightItem: string; - MenuDidOpen: string; - MenuDidPopUp: string; - MenuDidRemoveItem: string; - MenuDidSendAction: string; - MenuDidSendActionToItem: string; - MenuDidUpdate: string; - MenuWillAddItem: string; - MenuWillBeginTracking: string; - MenuWillDisplayItem: string; - MenuWillEndTracking: string; - MenuWillHighlightItem: string; - MenuWillOpen: string; - MenuWillPopUp: string; - MenuWillRemoveItem: string; - MenuWillSendAction: string; - MenuWillSendActionToItem: string; - MenuWillUpdate: string; - WebViewDidCommitNavigation: string; - WebViewDidFinishNavigation: string; - WebViewDidReceiveServerRedirectForProvisionalNavigation: string; - WebViewDidStartProvisionalNavigation: string; - WindowDidBecomeKey: string; - WindowDidBecomeMain: string; - WindowDidBeginSheet: string; - WindowDidChangeAlpha: string; - WindowDidChangeBackingLocation: string; - WindowDidChangeBackingProperties: string; - WindowDidChangeCollectionBehavior: string; - WindowDidChangeEffectiveAppearance: string; - WindowDidChangeOcclusionState: string; - WindowDidChangeOrderingMode: string; - WindowDidChangeScreen: string; - WindowDidChangeScreenParameters: string; - WindowDidChangeScreenProfile: string; - WindowDidChangeScreenSpace: string; - WindowDidChangeScreenSpaceProperties: string; - WindowDidChangeSharingType: string; - WindowDidChangeSpace: string; - WindowDidChangeSpaceOrderingMode: string; - WindowDidChangeTitle: string; - WindowDidChangeToolbar: string; - WindowDidDeminiaturize: string; - WindowDidEndSheet: string; - WindowDidEnterFullScreen: string; - WindowDidEnterVersionBrowser: string; - WindowDidExitFullScreen: string; - WindowDidExitVersionBrowser: string; - WindowDidExpose: string; - WindowDidFocus: string; - WindowDidMiniaturize: string; - WindowDidMove: string; - WindowDidOrderOffScreen: string; - WindowDidOrderOnScreen: string; - WindowDidResignKey: string; - WindowDidResignMain: string; - WindowDidResize: string; - WindowDidUpdate: string; - WindowDidUpdateAlpha: string; - WindowDidUpdateCollectionBehavior: string; - WindowDidUpdateCollectionProperties: string; - WindowDidUpdateShadow: string; - WindowDidUpdateTitle: string; - WindowDidUpdateToolbar: string; - WindowDidZoom: string; - WindowFileDraggingEntered: string; - WindowFileDraggingExited: string; - WindowFileDraggingPerformed: string; - WindowHide: string; - WindowMaximise: string; - WindowUnMaximise: string; - WindowMinimise: string; - WindowUnMinimise: string; - WindowShouldClose: string; - WindowShow: string; - WindowWillBecomeKey: string; - WindowWillBecomeMain: string; - WindowWillBeginSheet: string; - WindowWillChangeOrderingMode: string; - WindowWillClose: string; - WindowWillDeminiaturize: string; - WindowWillEnterFullScreen: string; - WindowWillEnterVersionBrowser: string; - WindowWillExitFullScreen: string; - WindowWillExitVersionBrowser: string; - WindowWillFocus: string; - WindowWillMiniaturize: string; - WindowWillMove: string; - WindowWillOrderOffScreen: string; - WindowWillOrderOnScreen: string; - WindowWillResignMain: string; - WindowWillResize: string; - WindowWillUnfocus: string; - WindowWillUpdate: string; - WindowWillUpdateAlpha: string; - WindowWillUpdateCollectionBehavior: string; - WindowWillUpdateCollectionProperties: string; - WindowWillUpdateShadow: string; - WindowWillUpdateTitle: string; - WindowWillUpdateToolbar: string; - WindowWillUpdateVisibility: string; - WindowWillUseStandardFrame: string; - WindowZoomIn: string; - WindowZoomOut: string; - WindowZoomReset: string; - }; - Linux: { - ApplicationStartup: string; - SystemThemeChanged: string; - WindowDeleteEvent: string; - WindowDidMove: string; - WindowDidResize: string; - WindowFocusIn: string; - WindowFocusOut: string; - WindowLoadChanged: string; - }; - Common: { - ApplicationOpenedWithFile: string; - ApplicationStarted: string; - ThemeChanged: string; - WindowClosing: string; - WindowDidMove: string; - WindowDidResize: string; - WindowDPIChanged: string; - WindowFilesDropped: string; - WindowFocus: string; - WindowFullscreen: string; - WindowHide: string; - WindowLostFocus: string; - WindowMaximise: string; - WindowMinimise: string; - WindowRestore: string; - WindowRuntimeReady: string; - WindowShow: string; - WindowUnFullscreen: string; - WindowUnMaximise: string; - WindowUnMinimise: string; - WindowZoom: string; - WindowZoomIn: string; - WindowZoomOut: string; - WindowZoomReset: string; - }; -}; -export class WailsEvent { - constructor(name: any, data?: any); - name: any; - data: any; -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/flags.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/flags.d.ts deleted file mode 100644 index 212436d04..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/flags.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Retrieves the value associated with the specified key from the flag map. - * - * @param {string} keyString - The key to retrieve the value for. - * @return {*} - The value associated with the specified key. - */ -export function GetFlag(keyString: string): any; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/index.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/index.d.ts deleted file mode 100644 index c1dc50fad..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function init(): void; -import * as Application from "./application"; -import * as Browser from "./browser"; -import * as Call from "./calls"; -import * as Clipboard from "./clipboard"; -import * as Create from "./create"; -import * as Dialogs from "./dialogs"; -import * as Events from "./events"; -import * as Flags from "./flags"; -import * as Screens from "./screens"; -import * as System from "./system"; -import Window from "./window"; -import * as WML from "./wml"; -export { Application, Browser, Call, Clipboard, Create, Dialogs, Events, Flags, Screens, System, Window, WML }; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/nanoid.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/nanoid.d.ts deleted file mode 100644 index dce98a05e..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/nanoid.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function nanoid(size?: number): string; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/runtime.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/runtime.d.ts deleted file mode 100644 index c412ce9d9..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/runtime.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Creates a runtime caller function that invokes a specified method on a given object within a specified window context. - * - * @param {Object} object - The object on which the method is to be invoked. - * @param {string} windowName - The name of the window context in which the method should be called. - * @returns {Function} A runtime caller function that takes the method name and optionally arguments and invokes the method within the specified window context. - */ -export function newRuntimeCaller(object: any, windowName: string): Function; -/** - * Creates a new runtime caller with specified ID. - * - * @param {object} object - The object to invoke the method on. - * @param {string} windowName - The name of the window. - * @return {Function} - The new runtime caller function. - */ -export function newRuntimeCallerWithID(object: object, windowName: string): Function; -export namespace objectNames { - let Call: number; - let Clipboard: number; - let Application: number; - let Events: number; - let ContextMenu: number; - let Dialog: number; - let Window: number; - let Screens: number; - let System: number; - let Browser: number; - let CancelCall: number; -} -export let clientId: string; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/screens.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/screens.d.ts deleted file mode 100644 index 7409875db..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/screens.d.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Gets all screens. - * @returns {Promise} A promise that resolves to an array of Screen objects. - */ -export function GetAll(): Promise; -/** - * Gets the primary screen. - * @returns {Promise} A promise that resolves to the primary screen. - */ -export function GetPrimary(): Promise; -/** - * Gets the current active screen. - * - * @returns {Promise} A promise that resolves with the current active screen. - */ -export function GetCurrent(): Promise; -export type Size = { - /** - * - The width. - */ - Width: number; - /** - * - The height. - */ - Height: number; -}; -export type Rect = { - /** - * - The X coordinate of the origin. - */ - X: number; - /** - * - The Y coordinate of the origin. - */ - Y: number; - /** - * - The width of the rectangle. - */ - Width: number; - /** - * - The height of the rectangle. - */ - Height: number; -}; -export type Screen = { - /** - * - Unique identifier for the screen. - */ - ID: string; - /** - * - Human readable name of the screen. - */ - Name: string; - /** - * - The scale factor of the screen (DPI/96). 1 = standard DPI, 2 = HiDPI (Retina), etc. - */ - ScaleFactor: number; - /** - * - The X coordinate of the screen. - */ - X: number; - /** - * - The Y coordinate of the screen. - */ - Y: number; - /** - * - Contains the width and height of the screen. - */ - Size: Size; - /** - * - Contains the bounds of the screen in terms of X, Y, Width, and Height. - */ - Bounds: Rect; - /** - * - Contains the physical bounds of the screen in terms of X, Y, Width, and Height (before scaling). - */ - PhysicalBounds: Rect; - /** - * - Contains the area of the screen that is actually usable (excluding taskbar and other system UI). - */ - WorkArea: Rect; - /** - * - Contains the physical WorkArea of the screen (before scaling). - */ - PhysicalWorkArea: Rect; - /** - * - True if this is the primary monitor selected by the user in the operating system. - */ - IsPrimary: boolean; - /** - * - The rotation of the screen. - */ - Rotation: number; -}; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/system.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/system.d.ts deleted file mode 100644 index b3a08f200..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/system.d.ts +++ /dev/null @@ -1,112 +0,0 @@ -export function invoke(msg: any): any; -/** - * @function - * Retrieves the system dark mode status. - * @returns {Promise} - A promise that resolves to a boolean value indicating if the system is in dark mode. - */ -export function IsDarkMode(): Promise; -/** - * Fetches the capabilities of the application from the server. - * - * @async - * @function Capabilities - * @returns {Promise} A promise that resolves to an object containing the capabilities. - */ -export function Capabilities(): Promise; -/** - * @typedef {Object} OSInfo - * @property {string} Branding - The branding of the OS. - * @property {string} ID - The ID of the OS. - * @property {string} Name - The name of the OS. - * @property {string} Version - The version of the OS. - */ -/** - * @typedef {Object} EnvironmentInfo - * @property {string} Arch - The architecture of the system. - * @property {boolean} Debug - True if the application is running in debug mode, otherwise false. - * @property {string} OS - The operating system in use. - * @property {OSInfo} OSInfo - Details of the operating system. - * @property {Object} PlatformInfo - Additional platform information. - */ -/** - * @function - * Retrieves environment details. - * @returns {Promise} - A promise that resolves to an object containing OS and system architecture. - */ -export function Environment(): Promise; -/** - * Checks if the current operating system is Windows. - * - * @return {boolean} True if the operating system is Windows, otherwise false. - */ -export function IsWindows(): boolean; -/** - * Checks if the current operating system is Linux. - * - * @returns {boolean} Returns true if the current operating system is Linux, false otherwise. - */ -export function IsLinux(): boolean; -/** - * Checks if the current environment is a macOS operating system. - * - * @returns {boolean} True if the environment is macOS, false otherwise. - */ -export function IsMac(): boolean; -/** - * Checks if the current environment architecture is AMD64. - * @returns {boolean} True if the current environment architecture is AMD64, false otherwise. - */ -export function IsAMD64(): boolean; -/** - * Checks if the current architecture is ARM. - * - * @returns {boolean} True if the current architecture is ARM, false otherwise. - */ -export function IsARM(): boolean; -/** - * Checks if the current environment is ARM64 architecture. - * - * @returns {boolean} - Returns true if the environment is ARM64 architecture, otherwise returns false. - */ -export function IsARM64(): boolean; -export function IsDebug(): boolean; -export type OSInfo = { - /** - * - The branding of the OS. - */ - Branding: string; - /** - * - The ID of the OS. - */ - ID: string; - /** - * - The name of the OS. - */ - Name: string; - /** - * - The version of the OS. - */ - Version: string; -}; -export type EnvironmentInfo = { - /** - * - The architecture of the system. - */ - Arch: string; - /** - * - True if the application is running in debug mode, otherwise false. - */ - Debug: boolean; - /** - * - The operating system in use. - */ - OS: string; - /** - * - Details of the operating system. - */ - OSInfo: OSInfo; - /** - * - Additional platform information. - */ - PlatformInfo: any; -}; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/utils.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/utils.d.ts deleted file mode 100644 index 25fa1d62a..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/utils.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Logs a message to the console with custom formatting. - * @param {string} message - The message to be logged. - * @return {void} - */ -export function debugLog(message: string): void; -/** - * Checks whether the browser supports removing listeners by triggering an AbortSignal - * (see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#signal) - * - * @return {boolean} - */ -export function canAbortListeners(): boolean; -export function whenReady(callback: any): void; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/window.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/window.d.ts deleted file mode 100644 index 4a86325ce..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/window.d.ts +++ /dev/null @@ -1,407 +0,0 @@ -export class Window { - /** - * Initialises a window object with the specified name. - * - * @private - * @param {string} name - The name of the target window. - */ - private constructor(); - /** - * Gets the specified window. - * - * @public - * @param {string} name - The name of the window to get. - * @return {Window} - The corresponding window object. - */ - public Get(name: string): Window; - /** - * Returns the absolute position of the window. - * - * @public - * @return {Promise} - The current absolute position of the window. - */ - public Position(): Promise; - /** - * Centers the window on the screen. - * - * @public - * @return {Promise} - */ - public Center(): Promise; - /** - * Closes the window. - * - * @public - * @return {Promise} - */ - public Close(): Promise; - /** - * Disables min/max size constraints. - * - * @public - * @return {Promise} - */ - public DisableSizeConstraints(): Promise; - /** - * Enables min/max size constraints. - * - * @public - * @return {Promise} - */ - public EnableSizeConstraints(): Promise; - /** - * Focuses the window. - * - * @public - * @return {Promise} - */ - public Focus(): Promise; - /** - * Forces the window to reload the page assets. - * - * @public - * @return {Promise} - */ - public ForceReload(): Promise; - /** - * Doc. - * - * @public - * @return {Promise} - */ - public Fullscreen(): Promise; - /** - * Returns the screen that the window is on. - * - * @public - * @return {Promise} - The screen the window is currently on - */ - public GetScreen(): Promise; - /** - * Returns the current zoom level of the window. - * - * @public - * @return {Promise} - The current zoom level - */ - public GetZoom(): Promise; - /** - * Returns the height of the window. - * - * @public - * @return {Promise} - The current height of the window - */ - public Height(): Promise; - /** - * Hides the window. - * - * @public - * @return {Promise} - */ - public Hide(): Promise; - /** - * Returns true if the window is focused. - * - * @public - * @return {Promise} - Whether the window is currently focused - */ - public IsFocused(): Promise; - /** - * Returns true if the window is fullscreen. - * - * @public - * @return {Promise} - Whether the window is currently fullscreen - */ - public IsFullscreen(): Promise; - /** - * Returns true if the window is maximised. - * - * @public - * @return {Promise} - Whether the window is currently maximised - */ - public IsMaximised(): Promise; - /** - * Returns true if the window is minimised. - * - * @public - * @return {Promise} - Whether the window is currently minimised - */ - public IsMinimised(): Promise; - /** - * Maximises the window. - * - * @public - * @return {Promise} - */ - public Maximise(): Promise; - /** - * Minimises the window. - * - * @public - * @return {Promise} - */ - public Minimise(): Promise; - /** - * Returns the name of the window. - * - * @public - * @return {Promise} - The name of the window - */ - public Name(): Promise; - /** - * Opens the development tools pane. - * - * @public - * @return {Promise} - */ - public OpenDevTools(): Promise; - /** - * Returns the relative position of the window to the screen. - * - * @public - * @return {Promise} - The current relative position of the window - */ - public RelativePosition(): Promise; - /** - * Reloads the page assets. - * - * @public - * @return {Promise} - */ - public Reload(): Promise; - /** - * Returns true if the window is resizable. - * - * @public - * @return {Promise} - Whether the window is currently resizable - */ - public Resizable(): Promise; - /** - * Restores the window to its previous state if it was previously minimised, maximised or fullscreen. - * - * @public - * @return {Promise} - */ - public Restore(): Promise; - /** - * Sets the absolute position of the window. - * - * @public - * @param {number} x - The desired horizontal absolute position of the window - * @param {number} y - The desired vertical absolute position of the window - * @return {Promise} - */ - public SetPosition(x: number, y: number): Promise; - /** - * Sets the window to be always on top. - * - * @public - * @param {boolean} alwaysOnTop - Whether the window should stay on top - * @return {Promise} - */ - public SetAlwaysOnTop(alwaysOnTop: boolean): Promise; - /** - * Sets the background colour of the window. - * - * @public - * @param {number} r - The desired red component of the window background - * @param {number} g - The desired green component of the window background - * @param {number} b - The desired blue component of the window background - * @param {number} a - The desired alpha component of the window background - * @return {Promise} - */ - public SetBackgroundColour(r: number, g: number, b: number, a: number): Promise; - /** - * Removes the window frame and title bar. - * - * @public - * @param {boolean} frameless - Whether the window should be frameless - * @return {Promise} - */ - public SetFrameless(frameless: boolean): Promise; - /** - * Disables the system fullscreen button. - * - * @public - * @param {boolean} enabled - Whether the fullscreen button should be enabled - * @return {Promise} - */ - public SetFullscreenButtonEnabled(enabled: boolean): Promise; - /** - * Sets the maximum size of the window. - * - * @public - * @param {number} width - The desired maximum width of the window - * @param {number} height - The desired maximum height of the window - * @return {Promise} - */ - public SetMaxSize(width: number, height: number): Promise; - /** - * Sets the minimum size of the window. - * - * @public - * @param {number} width - The desired minimum width of the window - * @param {number} height - The desired minimum height of the window - * @return {Promise} - */ - public SetMinSize(width: number, height: number): Promise; - /** - * Sets the relative position of the window to the screen. - * - * @public - * @param {number} x - The desired horizontal relative position of the window - * @param {number} y - The desired vertical relative position of the window - * @return {Promise} - */ - public SetRelativePosition(x: number, y: number): Promise; - /** - * Sets whether the window is resizable. - * - * @public - * @param {boolean} resizable - Whether the window should be resizable - * @return {Promise} - */ - public SetResizable(resizable: boolean): Promise; - /** - * Sets the size of the window. - * - * @public - * @param {number} width - The desired width of the window - * @param {number} height - The desired height of the window - * @return {Promise} - */ - public SetSize(width: number, height: number): Promise; - /** - * Sets the title of the window. - * - * @public - * @param {string} title - The desired title of the window - * @return {Promise} - */ - public SetTitle(title: string): Promise; - /** - * Sets the zoom level of the window. - * - * @public - * @param {number} zoom - The desired zoom level - * @return {Promise} - */ - public SetZoom(zoom: number): Promise; - /** - * Shows the window. - * - * @public - * @return {Promise} - */ - public Show(): Promise; - /** - * Returns the size of the window. - * - * @public - * @return {Promise} - The current size of the window - */ - public Size(): Promise; - /** - * Toggles the window between fullscreen and normal. - * - * @public - * @return {Promise} - */ - public ToggleFullscreen(): Promise; - /** - * Toggles the window between maximised and normal. - * - * @public - * @return {Promise} - */ - public ToggleMaximise(): Promise; - /** - * Un-fullscreens the window. - * - * @public - * @return {Promise} - */ - public UnFullscreen(): Promise; - /** - * Un-maximises the window. - * - * @public - * @return {Promise} - */ - public UnMaximise(): Promise; - /** - * Un-minimises the window. - * - * @public - * @return {Promise} - */ - public UnMinimise(): Promise; - /** - * Returns the width of the window. - * - * @public - * @return {Promise} - The current width of the window - */ - public Width(): Promise; - /** - * Zooms the window. - * - * @public - * @return {Promise} - */ - public Zoom(): Promise; - /** - * Increases the zoom level of the webview content. - * - * @public - * @return {Promise} - */ - public ZoomIn(): Promise; - /** - * Decreases the zoom level of the webview content. - * - * @public - * @return {Promise} - */ - public ZoomOut(): Promise; - /** - * Resets the zoom level of the webview content. - * - * @public - * @return {Promise} - */ - public ZoomReset(): Promise; -} -export default thisWindow; -export type Screen = import("./screens").Screen; -/** - * A record describing the position of a window. - */ -export type Position = { - /** - * - The horizontal position of the window - */ - x: number; - /** - * - The vertical position of the window - */ - y: number; -}; -/** - * A record describing the size of a window. - */ -export type Size = { - /** - * - The width of the window - */ - width: number; - /** - * - The height of the window - */ - height: number; -}; -/** - * The window within which the script is running. - * - * @type {Window} - */ -declare const thisWindow: Window; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/types/wml.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/types/wml.d.ts deleted file mode 100644 index 037912e56..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/types/wml.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Schedules an automatic reload of WML to be performed as soon as the document is fully loaded. - * - * @return {void} - */ -export function Enable(): void; -/** - * Reloads the WML page by adding necessary event listeners and browser listeners. - * - * @return {void} - */ -export function Reload(): void; diff --git a/v3/internal/runtime/vite.config.ts b/v3/internal/runtime/desktop/@wailsio/runtime/vitest.config.ts similarity index 51% rename from v3/internal/runtime/vite.config.ts rename to v3/internal/runtime/desktop/@wailsio/runtime/vitest.config.ts index eb0831c65..efb60170a 100644 --- a/v3/internal/runtime/vite.config.ts +++ b/v3/internal/runtime/desktop/@wailsio/runtime/vitest.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vitest/config' +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: 'happy-dom', + testTimeout: 200 }, -}) \ No newline at end of file +}); diff --git a/v3/internal/runtime/desktop/README.md b/v3/internal/runtime/desktop/README.md index 3da6938c4..0ca8ed53e 100644 --- a/v3/internal/runtime/desktop/README.md +++ b/v3/internal/runtime/desktop/README.md @@ -1,8 +1,10 @@ # README -The `main.js` file in this directory is the entrypoint for the `runtime.js` file that may be +The `index.js` file in the `compiled` directory is the entrypoint for the `runtime.js` file that may be loaded at runtime. This will add `window.wails` and `window._wails` to the global scope. NOTE: It is preferable to use the `@wailsio/runtime` package to use the runtime. -After updating any files in this directory, you must run `wails3 task build:runtime` to regenerate the compiled JS. \ No newline at end of file +⚠️ Do not rebuild the runtime manually after updating TS code: +the CI pipeline will take care of this. +PRs that touch build artifacts will be blocked from merging. \ No newline at end of file diff --git a/v3/internal/runtime/desktop/compiled/main.js b/v3/internal/runtime/desktop/compiled/main.js index ad35cbeea..d2b21dca1 100644 --- a/v3/internal/runtime/desktop/compiled/main.js +++ b/v3/internal/runtime/desktop/compiled/main.js @@ -11,8 +11,8 @@ The electron alternative for Go import * as Runtime from "../@wailsio/runtime/src"; // NOTE: the following methods MUST be imported explicitly because of how esbuild injection works -import {Enable as EnableWML} from "../@wailsio/runtime/src/wml"; -import {debugLog} from "../@wailsio/runtime/src/utils"; +import { Enable as EnableWML } from "../@wailsio/runtime/src/wml"; +import { debugLog } from "../@wailsio/runtime/src/utils"; window.wails = Runtime; EnableWML(); diff --git a/v3/internal/templates/_common/go.mod.tmpl b/v3/internal/templates/_common/go.mod.tmpl index 2cb53bbd7..bc6a656d1 100644 --- a/v3/internal/templates/_common/go.mod.tmpl +++ b/v3/internal/templates/_common/go.mod.tmpl @@ -1,19 +1,51 @@ module changeme -go 1.21 +go 1.24 require github.com/wailsapp/wails/v3 {{.WailsVersion}} require ( - github.com/json-iterator/go v1.1.12 // indirect - github.com/leaanthony/slicer v1.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/samber/lo v1.37.0 // indirect - github.com/wailsapp/mimetype v1.4.1 // indirect - golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect - golang.org/x/net v0.7.0 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.5 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/cloudflare/circl v1.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.13.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/lmittmann/tint v1.0.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/wailsapp/go-webview2 v1.0.19 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect ) -{{if gt (len .LocalModulePath) 0}} +{{if .LocalModulePath}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 {{end}} diff --git a/v3/internal/templates/_common/go.sum.tmpl b/v3/internal/templates/_common/go.sum.tmpl index c06e0dbc6..977b11504 100644 --- a/v3/internal/templates/_common/go.sum.tmpl +++ b/v3/internal/templates/_common/go.sum.tmpl @@ -1,33 +1,146 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= +github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= -github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= -github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= +github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= +github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= -github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU= +github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4= -golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v3/internal/templates/_common/main.go.tmpl b/v3/internal/templates/_common/main.go.tmpl index 43af2bdf0..be9310930 100644 --- a/v3/internal/templates/_common/main.go.tmpl +++ b/v3/internal/templates/_common/main.go.tmpl @@ -46,7 +46,7 @@ func main() { // 'Mac' options tailor the window when running on macOS. // 'BackgroundColour' is the background colour of the window. // 'URL' is the URL that will be loaded into the webview. - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Window 1", Mac: application.MacWindow{ InvisibleTitleBarHeight: 50, @@ -62,7 +62,7 @@ func main() { go func() { for { now := time.Now().Format(time.RFC1123) - app.EmitEvent("time", now) + app.Event.Emit("time", now) time.Sleep(time.Second) } }() diff --git a/v3/internal/templates/lit-ts/frontend/package.json b/v3/internal/templates/lit-ts/frontend/package.json index 80a5e0d96..d208c3fbd 100644 --- a/v3/internal/templates/lit-ts/frontend/package.json +++ b/v3/internal/templates/lit-ts/frontend/package.json @@ -10,11 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "lit": "^3.1.0" }, "devDependencies": { "typescript": "^5.2.2", - "vite": "^5.0.0", - "@wailsio/runtime": "latest" + "vite": "^5.0.0" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/lit-ts/frontend/public/style.css b/v3/internal/templates/lit-ts/frontend/public/style.css index 8403ee0fd..10550a378 100644 --- a/v3/internal/templates/lit-ts/frontend/public/style.css +++ b/v3/internal/templates/lit-ts/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/lit/frontend/package.json b/v3/internal/templates/lit/frontend/package.json index 7369a3f47..ec30e751a 100644 --- a/v3/internal/templates/lit/frontend/package.json +++ b/v3/internal/templates/lit/frontend/package.json @@ -10,10 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "lit": "^3.1.0" }, "devDependencies": { - "vite": "^5.0.0", - "@wailsio/runtime": "latest" + "vite": "^5.0.0" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/lit/frontend/public/style.css b/v3/internal/templates/lit/frontend/public/style.css index 8403ee0fd..10550a378 100644 --- a/v3/internal/templates/lit/frontend/public/style.css +++ b/v3/internal/templates/lit/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/preact-ts/frontend/package.json b/v3/internal/templates/preact-ts/frontend/package.json index e26f733b7..b5dd75296 100644 --- a/v3/internal/templates/preact-ts/frontend/package.json +++ b/v3/internal/templates/preact-ts/frontend/package.json @@ -10,12 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "preact": "^10.19.3" }, "devDependencies": { "@preact/preset-vite": "^2.7.0", "typescript": "^5.2.2", - "vite": "^5.0.8", - "@wailsio/runtime": "latest" + "vite": "^5.0.8" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/preact-ts/frontend/public/style.css b/v3/internal/templates/preact-ts/frontend/public/style.css index 5d7b7e09c..c4f073382 100644 --- a/v3/internal/templates/preact-ts/frontend/public/style.css +++ b/v3/internal/templates/preact-ts/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/preact/frontend/package.json b/v3/internal/templates/preact/frontend/package.json index 27506286f..863d1fc23 100644 --- a/v3/internal/templates/preact/frontend/package.json +++ b/v3/internal/templates/preact/frontend/package.json @@ -10,11 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "preact": "^10.19.3" }, "devDependencies": { "@preact/preset-vite": "^2.7.0", - "vite": "^5.0.8", - "@wailsio/runtime": "latest" + "vite": "^5.0.8" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/preact/frontend/public/style.css b/v3/internal/templates/preact/frontend/public/style.css index 5d7b7e09c..c4f073382 100644 --- a/v3/internal/templates/preact/frontend/public/style.css +++ b/v3/internal/templates/preact/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/qwik-ts/frontend/package.json b/v3/internal/templates/qwik-ts/frontend/package.json index 8452e0e60..b3f359a6b 100644 --- a/v3/internal/templates/qwik-ts/frontend/package.json +++ b/v3/internal/templates/qwik-ts/frontend/package.json @@ -9,12 +9,12 @@ "build": "tsc && vite build --mode production", "preview": "vite preview" }, - "devDependencies": { - "typescript": "^5.2.2", - "vite": "^5.0.8" - }, "dependencies": { "@builder.io/qwik": "^1.3.0", "@wailsio/runtime": "latest" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.0.8" } } diff --git a/v3/internal/templates/qwik-ts/frontend/public/style.css b/v3/internal/templates/qwik-ts/frontend/public/style.css index 2b447c0b6..c1d8d1a2e 100644 --- a/v3/internal/templates/qwik-ts/frontend/public/style.css +++ b/v3/internal/templates/qwik-ts/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/qwik/frontend/package.json b/v3/internal/templates/qwik/frontend/package.json index 782a10d42..3139e426b 100644 --- a/v3/internal/templates/qwik/frontend/package.json +++ b/v3/internal/templates/qwik/frontend/package.json @@ -9,12 +9,12 @@ "build": "vite build --mode production", "preview": "vite preview" }, - "devDependencies": { - "typescript": "^5.2.2", - "vite": "^5.0.8" - }, "dependencies": { "@builder.io/qwik": "^1.3.0", "@wailsio/runtime": "latest" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.0.8" } } diff --git a/v3/internal/templates/qwik/frontend/public/style.css b/v3/internal/templates/qwik/frontend/public/style.css index 2b447c0b6..c1d8d1a2e 100644 --- a/v3/internal/templates/qwik/frontend/public/style.css +++ b/v3/internal/templates/qwik/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/react-swc-ts/frontend/package.json b/v3/internal/templates/react-swc-ts/frontend/package.json index 99ad19d83..0dcc8cdcd 100644 --- a/v3/internal/templates/react-swc-ts/frontend/package.json +++ b/v3/internal/templates/react-swc-ts/frontend/package.json @@ -7,24 +7,18 @@ "dev": "vite", "build:dev": "tsc && vite build --minify false --mode development", "build": "tsc && vite build --mode production", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", - "@typescript-eslint/eslint-plugin": "^6.14.0", - "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react-swc": "^3.5.0", - "eslint": "^8.55.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", "typescript": "^5.2.2", - "vite": "^5.0.8", - "@wailsio/runtime": "latest" + "vite": "^5.0.8" } } diff --git a/v3/internal/templates/react-swc-ts/frontend/public/style.css b/v3/internal/templates/react-swc-ts/frontend/public/style.css index a36cce4f8..0ba9cf5cc 100644 --- a/v3/internal/templates/react-swc-ts/frontend/public/style.css +++ b/v3/internal/templates/react-swc-ts/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/react-swc/frontend/package.json b/v3/internal/templates/react-swc/frontend/package.json index ec94c1dd6..158ba6880 100644 --- a/v3/internal/templates/react-swc/frontend/package.json +++ b/v3/internal/templates/react-swc/frontend/package.json @@ -7,10 +7,10 @@ "dev": "vite", "build:dev": "vite build --minify false --mode development", "build": "vite build --mode production", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -18,11 +18,6 @@ "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react-swc": "^3.5.0", - "eslint": "^8.55.0", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", - "vite": "^5.0.8", - "@wailsio/runtime": "latest" + "vite": "^5.0.8" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/react-swc/frontend/public/style.css b/v3/internal/templates/react-swc/frontend/public/style.css index a36cce4f8..0ba9cf5cc 100644 --- a/v3/internal/templates/react-swc/frontend/public/style.css +++ b/v3/internal/templates/react-swc/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/react-ts/frontend/package.json b/v3/internal/templates/react-ts/frontend/package.json index 0ec3f06e4..f718c0073 100644 --- a/v3/internal/templates/react-ts/frontend/package.json +++ b/v3/internal/templates/react-ts/frontend/package.json @@ -7,24 +7,18 @@ "dev": "vite", "build:dev": "tsc && vite build --minify false --mode development", "build": "tsc && vite build --mode production", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", - "@typescript-eslint/eslint-plugin": "^6.14.0", - "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.55.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", "typescript": "^5.2.2", - "vite": "^5.0.8", - "@wailsio/runtime": "latest" + "vite": "^5.0.8" } } diff --git a/v3/internal/templates/react-ts/frontend/public/style.css b/v3/internal/templates/react-ts/frontend/public/style.css index a36cce4f8..0ba9cf5cc 100644 --- a/v3/internal/templates/react-ts/frontend/public/style.css +++ b/v3/internal/templates/react-ts/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/react/frontend/package.json b/v3/internal/templates/react/frontend/package.json index 3692c8c09..59d4d62b3 100644 --- a/v3/internal/templates/react/frontend/package.json +++ b/v3/internal/templates/react/frontend/package.json @@ -7,10 +7,10 @@ "dev": "vite", "build:dev": "vite build --minify false --mode development", "build": "vite build --mode production", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -18,11 +18,6 @@ "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.55.0", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", - "vite": "^5.0.8", - "@wailsio/runtime": "latest" + "vite": "^5.0.8" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/react/frontend/public/style.css b/v3/internal/templates/react/frontend/public/style.css index a36cce4f8..0ba9cf5cc 100644 --- a/v3/internal/templates/react/frontend/public/style.css +++ b/v3/internal/templates/react/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/solid-ts/frontend/package.json b/v3/internal/templates/solid-ts/frontend/package.json index 703d31562..741674ea7 100644 --- a/v3/internal/templates/solid-ts/frontend/package.json +++ b/v3/internal/templates/solid-ts/frontend/package.json @@ -10,12 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "solid-js": "^1.8.7" }, "devDependencies": { "typescript": "^5.2.2", "vite": "^5.0.8", - "vite-plugin-solid": "^2.8.0", - "@wailsio/runtime": "latest" + "vite-plugin-solid": "^2.8.0" } } diff --git a/v3/internal/templates/solid-ts/frontend/public/style.css b/v3/internal/templates/solid-ts/frontend/public/style.css index 2b7547eb4..892241249 100644 --- a/v3/internal/templates/solid-ts/frontend/public/style.css +++ b/v3/internal/templates/solid-ts/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/solid/frontend/package.json b/v3/internal/templates/solid/frontend/package.json index 9062b3686..03ed9141f 100644 --- a/v3/internal/templates/solid/frontend/package.json +++ b/v3/internal/templates/solid/frontend/package.json @@ -10,11 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "solid-js": "^1.8.7" }, "devDependencies": { "vite": "^5.0.8", - "vite-plugin-solid": "^2.8.0", - "@wailsio/runtime": "latest" + "vite-plugin-solid": "^2.8.0" } } diff --git a/v3/internal/templates/solid/frontend/public/style.css b/v3/internal/templates/solid/frontend/public/style.css index 2b7547eb4..892241249 100644 --- a/v3/internal/templates/solid/frontend/public/style.css +++ b/v3/internal/templates/solid/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/svelte-ts/frontend/package.json b/v3/internal/templates/svelte-ts/frontend/package.json index f3491e928..c14954e77 100644 --- a/v3/internal/templates/svelte-ts/frontend/package.json +++ b/v3/internal/templates/svelte-ts/frontend/package.json @@ -10,6 +10,9 @@ "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json" }, + "dependencies": { + "@wailsio/runtime": "latest" + }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.1", "@tsconfig/svelte": "^5.0.2", @@ -17,7 +20,6 @@ "svelte-check": "^3.6.2", "tslib": "^2.6.2", "typescript": "^5.2.2", - "vite": "^5.0.8", - "@wailsio/runtime": "latest" + "vite": "^5.0.8" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/svelte-ts/frontend/public/style.css b/v3/internal/templates/svelte-ts/frontend/public/style.css index 259397254..0b9c58279 100644 --- a/v3/internal/templates/svelte-ts/frontend/public/style.css +++ b/v3/internal/templates/svelte-ts/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/svelte/frontend/package.json b/v3/internal/templates/svelte/frontend/package.json index 3a9ca1053..4655c0e95 100644 --- a/v3/internal/templates/svelte/frontend/package.json +++ b/v3/internal/templates/svelte/frontend/package.json @@ -9,10 +9,12 @@ "build": "vite build --mode production", "preview": "vite preview" }, + "dependencies": { + "@wailsio/runtime": "latest" + }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.1", "svelte": "^4.2.8", - "vite": "^5.0.8", - "@wailsio/runtime": "latest" + "vite": "^5.0.8" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/svelte/frontend/public/style.css b/v3/internal/templates/svelte/frontend/public/style.css index 259397254..0b9c58279 100644 --- a/v3/internal/templates/svelte/frontend/public/style.css +++ b/v3/internal/templates/svelte/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/sveltekit-ts/frontend/package-lock.json b/v3/internal/templates/sveltekit-ts/frontend/package-lock.json deleted file mode 100644 index 4ba4bd3f3..000000000 --- a/v3/internal/templates/sveltekit-ts/frontend/package-lock.json +++ /dev/null @@ -1,1288 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.1", - "dependencies": { - "@sveltejs/adapter-static": "^3.0.5", - "@wailsio/runtime": "^3.0.0-alpha.28" - }, - "devDependencies": { - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.2.7", - "svelte-check": "^4.0.0", - "typescript": "^5.0.0", - "vite": "^5.0.3" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sveltejs/adapter-static": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.5.tgz", - "integrity": "sha512-kFJR7RxeB6FBvrKZWAEzIALatgy11ISaaZbcPup8JdWUdrmmfUHHTJ738YHJTEfnCiiXi6aX8Q6ePY7tnSMD6Q==", - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.7.1.tgz", - "integrity": "sha512-TBVnkwgYQT3EafGQK6Eyh5FlLEBlRhCmqPTwcdOs+QdnyUc3eCAxRWtXlFxIWtmk6pqv11zdng8qTpThdTogew==", - "hasInstallScript": true, - "dependencies": { - "@types/cookie": "^0.6.0", - "cookie": "^0.6.0", - "devalue": "^5.1.0", - "esm-env": "^1.0.0", - "import-meta-resolve": "^4.1.0", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", - "sirv": "^3.0.0", - "tiny-glob": "^0.2.9" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": ">=18.13" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz", - "integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==", - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", - "debug": "^4.3.4", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.10", - "svelte-hmr": "^0.16.0", - "vitefu": "^0.2.5" - }, - "engines": { - "node": "^18.0.0 || >=20" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", - "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.0.0 || >=20" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" - }, - "node_modules/@wailsio/runtime": { - "version": "3.0.0-alpha.28", - "resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.28.tgz", - "integrity": "sha512-caMnAcKxxDrIWYgCZAMY2kdL++X4ehO2+JvH5na21xfDqz3VnHkEjxsH3jfhgd34M8LY80QEH8iqoMYytDFE/g==", - "dependencies": { - "nanoid": "^5.0.7" - } - }, - "node_modules/@wailsio/runtime/node_modules/nanoid": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", - "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, - "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", - "dev": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/esm-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==" - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/fdir": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", - "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", - "dev": true, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" - }, - "node_modules/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" - }, - "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" - }, - "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", - "dev": true, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz", - "integrity": "sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==" - }, - "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/svelte": { - "version": "4.2.19", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", - "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/estree": "^1.0.1", - "acorn": "^8.9.0", - "aria-query": "^5.3.0", - "axobject-query": "^4.0.0", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", - "locate-character": "^3.0.0", - "magic-string": "^0.30.4", - "periscopic": "^3.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/svelte-check": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.5.tgz", - "integrity": "sha512-icBTBZ3ibBaywbXUat3cK6hB5Du+Kq9Z8CRuyLmm64XIe2/r+lQcbuBx/IQgsbrC+kT2jQ0weVpZSSRIPwB6jQ==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "chokidar": "^4.0.1", - "fdir": "^6.2.0", - "picocolors": "^1.0.0", - "sade": "^1.7.4" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "engines": { - "node": ">= 18.0.0" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" - } - }, - "node_modules/svelte-hmr": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", - "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", - "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" - } - }, - "node_modules/tiny-glob": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", - "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dependencies": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/vite": { - "version": "5.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", - "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vitefu": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", - "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - } - } -} diff --git a/v3/internal/templates/sveltekit-ts/frontend/package.json b/v3/internal/templates/sveltekit-ts/frontend/package.json index c545acab9..58a6b3eb7 100644 --- a/v3/internal/templates/sveltekit-ts/frontend/package.json +++ b/v3/internal/templates/sveltekit-ts/frontend/package.json @@ -1,26 +1,26 @@ { - "name": "frontend", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "vite dev", - "build:dev": "vite build --minify false --mode development", - "build": "vite build --mode production", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" - }, - "devDependencies": { - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.2.7", - "svelte-check": "^4.0.0", - "typescript": "^5.0.0", - "vite": "^5.0.3" - }, - "type": "module", - "dependencies": { - "@sveltejs/adapter-static": "^3.0.5", - "@wailsio/runtime": "^3.0.0-alpha.28" - } + "name": "frontend", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build:dev": "vite build --minify false --mode development", + "build": "vite build --mode production", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "dependencies": { + "@wailsio/runtime": "^3.0.0-alpha.28" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.5", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.2.7", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^5.0.3" + } } diff --git a/v3/internal/templates/sveltekit-ts/frontend/static/style.css b/v3/internal/templates/sveltekit-ts/frontend/static/style.css index 259397254..0b9c58279 100644 --- a/v3/internal/templates/sveltekit-ts/frontend/static/style.css +++ b/v3/internal/templates/sveltekit-ts/frontend/static/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/sveltekit/frontend/frontend/style.css b/v3/internal/templates/sveltekit/frontend/frontend/style.css index 259397254..0b9c58279 100644 --- a/v3/internal/templates/sveltekit/frontend/frontend/style.css +++ b/v3/internal/templates/sveltekit/frontend/frontend/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/sveltekit/frontend/package-lock.json b/v3/internal/templates/sveltekit/frontend/package-lock.json deleted file mode 100644 index 7e20bdb45..000000000 --- a/v3/internal/templates/sveltekit/frontend/package-lock.json +++ /dev/null @@ -1,1221 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.1", - "dependencies": { - "@sveltejs/adapter-static": "^3.0.5", - "@wailsio/runtime": "^3.0.0-alpha.28" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.2.7", - "vite": "^5.0.3" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sveltejs/adapter-auto": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.5.tgz", - "integrity": "sha512-27LR+uKccZ62lgq4N/hvyU2G+hTP9fxWEAfnZcl70HnyfAjMSsGk1z/SjAPXNCD1mVJIE7IFu3TQ8cQ/UH3c0A==", - "dev": true, - "dependencies": { - "import-meta-resolve": "^4.1.0" - }, - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/adapter-static": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.5.tgz", - "integrity": "sha512-kFJR7RxeB6FBvrKZWAEzIALatgy11ISaaZbcPup8JdWUdrmmfUHHTJ738YHJTEfnCiiXi6aX8Q6ePY7tnSMD6Q==", - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.7.1.tgz", - "integrity": "sha512-TBVnkwgYQT3EafGQK6Eyh5FlLEBlRhCmqPTwcdOs+QdnyUc3eCAxRWtXlFxIWtmk6pqv11zdng8qTpThdTogew==", - "hasInstallScript": true, - "dependencies": { - "@types/cookie": "^0.6.0", - "cookie": "^0.6.0", - "devalue": "^5.1.0", - "esm-env": "^1.0.0", - "import-meta-resolve": "^4.1.0", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", - "sirv": "^3.0.0", - "tiny-glob": "^0.2.9" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": ">=18.13" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz", - "integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==", - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", - "debug": "^4.3.4", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.10", - "svelte-hmr": "^0.16.0", - "vitefu": "^0.2.5" - }, - "engines": { - "node": "^18.0.0 || >=20" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", - "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.0.0 || >=20" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" - }, - "node_modules/@wailsio/runtime": { - "version": "3.0.0-alpha.28", - "resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.28.tgz", - "integrity": "sha512-caMnAcKxxDrIWYgCZAMY2kdL++X4ehO2+JvH5na21xfDqz3VnHkEjxsH3jfhgd34M8LY80QEH8iqoMYytDFE/g==", - "dependencies": { - "nanoid": "^5.0.7" - } - }, - "node_modules/@wailsio/runtime/node_modules/nanoid": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", - "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, - "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/esm-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==" - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" - }, - "node_modules/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" - }, - "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" - }, - "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz", - "integrity": "sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==" - }, - "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/svelte": { - "version": "4.2.19", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", - "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/estree": "^1.0.1", - "acorn": "^8.9.0", - "aria-query": "^5.3.0", - "axobject-query": "^4.0.0", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", - "locate-character": "^3.0.0", - "magic-string": "^0.30.4", - "periscopic": "^3.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/svelte-hmr": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", - "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", - "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" - } - }, - "node_modules/tiny-glob": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", - "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dependencies": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/vite": { - "version": "5.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", - "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vitefu": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", - "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - } - } -} diff --git a/v3/internal/templates/sveltekit/frontend/package.json b/v3/internal/templates/sveltekit/frontend/package.json index 03427d5b8..54d96b52c 100644 --- a/v3/internal/templates/sveltekit/frontend/package.json +++ b/v3/internal/templates/sveltekit/frontend/package.json @@ -1,25 +1,24 @@ { - "name": "frontend", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "vite dev", - "build:dev": "vite build --minify false --mode development", - "build": "vite build --mode production", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.2.7", - "vite": "^5.0.3" - }, - "type": "module", - "dependencies": { - "@sveltejs/adapter-static": "^3.0.5", - "@wailsio/runtime": "^3.0.0-alpha.28" - } + "name": "frontend", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build:dev": "vite build --minify false --mode development", + "build": "vite build --mode production", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch" + }, + "dependencies": { + "@wailsio/runtime": "^3.0.0-alpha.28" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.5", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.2.7", + "vite": "^5.0.3" + } } diff --git a/v3/internal/templates/sveltekit/frontend/static/style.css b/v3/internal/templates/sveltekit/frontend/static/style.css index 259397254..0b9c58279 100644 --- a/v3/internal/templates/sveltekit/frontend/static/style.css +++ b/v3/internal/templates/sveltekit/frontend/static/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/vanilla-ts/frontend/package.json b/v3/internal/templates/vanilla-ts/frontend/package.json index 4d675f189..b39da7ece 100644 --- a/v3/internal/templates/vanilla-ts/frontend/package.json +++ b/v3/internal/templates/vanilla-ts/frontend/package.json @@ -9,9 +9,11 @@ "build": "tsc && vite build --mode production", "preview": "vite preview" }, + "dependencies": { + "@wailsio/runtime": "latest" + }, "devDependencies": { "typescript": "^4.9.3", - "vite": "^5.0.0", - "@wailsio/runtime": "latest" + "vite": "^5.0.0" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/vanilla-ts/frontend/public/style.css b/v3/internal/templates/vanilla-ts/frontend/public/style.css index 259397254..0b9c58279 100644 --- a/v3/internal/templates/vanilla-ts/frontend/public/style.css +++ b/v3/internal/templates/vanilla-ts/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/vanilla/frontend/package.json b/v3/internal/templates/vanilla/frontend/package.json index 9ae87549e..0a118e984 100644 --- a/v3/internal/templates/vanilla/frontend/package.json +++ b/v3/internal/templates/vanilla/frontend/package.json @@ -9,8 +9,10 @@ "build": "vite build --mode production", "preview": "vite preview" }, - "devDependencies": { - "vite": "^5.0.0", + "dependencies": { "@wailsio/runtime": "latest" + }, + "devDependencies": { + "vite": "^5.0.0" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/vanilla/frontend/public/style.css b/v3/internal/templates/vanilla/frontend/public/style.css index 259397254..0b9c58279 100644 --- a/v3/internal/templates/vanilla/frontend/public/style.css +++ b/v3/internal/templates/vanilla/frontend/public/style.css @@ -13,9 +13,6 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; -} - -* { user-select: none; -webkit-user-select: none; -moz-user-select: none; diff --git a/v3/internal/templates/vue-ts/frontend/package.json b/v3/internal/templates/vue-ts/frontend/package.json index 60bd3c63d..dc08ffa11 100644 --- a/v3/internal/templates/vue-ts/frontend/package.json +++ b/v3/internal/templates/vue-ts/frontend/package.json @@ -10,13 +10,13 @@ "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "vue": "^3.2.45" }, "devDependencies": { "@vitejs/plugin-vue": "^4.0.0", "typescript": "^4.9.3", "vite": "^5.0.0", - "@wailsio/runtime": "latest", "vue-tsc": "^1.0.11" } -} \ No newline at end of file +} diff --git a/v3/internal/templates/vue/frontend/package.json b/v3/internal/templates/vue/frontend/package.json index 7cd67be52..6958956b3 100644 --- a/v3/internal/templates/vue/frontend/package.json +++ b/v3/internal/templates/vue/frontend/package.json @@ -10,11 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@wailsio/runtime": "latest", "vue": "^3.2.45" }, "devDependencies": { "@vitejs/plugin-vue": "^4.0.0", - "vite": "^5.0.0", - "@wailsio/runtime": "latest" + "vite": "^5.0.0" } -} \ No newline at end of file +} diff --git a/v3/internal/version/version.txt b/v3/internal/version/version.txt index 67268a4f5..a63a7c224 100644 --- a/v3/internal/version/version.txt +++ b/v3/internal/version/version.txt @@ -1 +1 @@ -v3.0.0-alpha.9 \ No newline at end of file +v3.0.0-alpha.17 \ No newline at end of file diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 3f0f7a48f..cb121df6a 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -7,27 +7,22 @@ import ( "errors" "fmt" "io" - "log" "log/slog" "net/http" "os" "runtime" + "slices" "strconv" "strings" "sync" - "github.com/wailsapp/wails/v3/internal/fileexplorer" + "github.com/wailsapp/wails/v3/internal/assetserver/bundledassets" - "github.com/wailsapp/wails/v3/internal/operatingsystem" - - "github.com/pkg/browser" - "github.com/samber/lo" "github.com/wailsapp/wails/v3/internal/signal" "github.com/wailsapp/wails/v3/internal/assetserver" "github.com/wailsapp/wails/v3/internal/assetserver/webview" "github.com/wailsapp/wails/v3/internal/capabilities" - "github.com/wailsapp/wails/v3/pkg/events" ) //go:embed assets/* @@ -82,7 +77,7 @@ func New(appOptions Options) *App { result.logStartup() result.logPlatformInfo() - result.customEventProcessor = NewWailsEventProcessor(result.dispatchEventToListeners) + result.customEventProcessor = NewWailsEventProcessor(result.Event.dispatch) messageProc := NewMessageProcessor(result.Logger) opts := &assetserver.Options{ @@ -98,22 +93,27 @@ func New(appOptions Options) *App { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { path := req.URL.Path switch path { + case "/wails/runtime.js": + err := assetserver.ServeFile(rw, path, bundledassets.RuntimeJS) + if err != nil { + result.fatal("unable to serve runtime.js: %w", err) + } case "/wails/runtime": messageProc.ServeHTTP(rw, req) case "/wails/capabilities": err := assetserver.ServeFile(rw, path, globalApplication.capabilities.AsBytes()) if err != nil { - result.handleFatalError(fmt.Errorf("unable to serve capabilities: %s", err.Error())) + result.fatal("unable to serve capabilities: %w", err) } case "/wails/flags": updatedOptions := result.impl.GetFlags(appOptions) flags, err := json.Marshal(updatedOptions) if err != nil { - result.handleFatalError(fmt.Errorf("invalid flags provided to application: %s", err.Error())) + result.fatal("invalid flags provided to application: %w", err) } err = assetserver.ServeFile(rw, path, flags) if err != nil { - result.handleFatalError(fmt.Errorf("unable to serve flags: %s", err.Error())) + result.fatal("unable to serve flags: %w", err) } default: next.ServeHTTP(rw, req) @@ -130,40 +130,14 @@ func New(appOptions Options) *App { srv, err := assetserver.NewAssetServer(opts) if err != nil { - result.handleFatalError(fmt.Errorf("Fatal error in application initialisation: " + err.Error())) + result.fatal("application initialisation failed: %w", err) } result.assets = srv result.assets.LogDetails() - result.bindings, err = NewBindings(appOptions.Services, appOptions.BindAliases) - if err != nil { - result.handleFatalError(fmt.Errorf("Fatal error in application initialisation: " + err.Error())) - } - - for i, service := range appOptions.Services { - if thisService, ok := service.instance.(ServiceStartup); ok { - err := thisService.ServiceStartup(result.ctx, service.options) - if err != nil { - name := service.options.Name - if name == "" { - name = getServiceName(service.instance) - } - globalApplication.Logger.Error("ServiceStartup() failed shutting down application:", "service", name, "error", err.Error()) - // Run shutdown on all services that have already started - for _, service := range appOptions.Services[:i] { - if thisService, ok := service.instance.(ServiceShutdown); ok { - err := thisService.ServiceShutdown() - if err != nil { - globalApplication.Logger.Error("Error shutting down service: " + err.Error()) - } - } - } - // Shutdown the application - os.Exit(1) - } - } - } + result.bindings = NewBindings(appOptions.MarshalError, appOptions.BindAliases) + result.options.Services = slices.Clone(appOptions.Services) // Process keybindings if result.options.KeyBindings != nil { @@ -181,11 +155,11 @@ func New(appOptions Options) *App { if errors.Is(err, alreadyRunningError) && manager != nil { err = manager.notifyFirstInstance() if err != nil { - globalApplication.error("Failed to notify first instance: " + err.Error()) + globalApplication.error("failed to notify first instance: %w", err) } os.Exit(appOptions.SingleInstance.ExitCode) } - result.handleFatalError(fmt.Errorf("failed to initialize single instance manager: %w", err)) + result.fatal("failed to initialize single instance manager: %w", err) } else { result.singleInstanceManager = manager } @@ -224,6 +198,7 @@ type ( GetFlags(options Options) map[string]any isOnMainThread() bool isDarkMode() bool + getAccentColor() string } runnable interface { @@ -297,8 +272,18 @@ type App struct { applicationEventHooks map[uint][]*eventHook applicationEventHooksLock sync.RWMutex - // Screens layout manager (handles DIP coordinate system) - screenManager ScreenManager + // Manager pattern for organized API + Window *WindowManager + ContextMenu *ContextMenuManager + KeyBinding *KeyBindingManager + Browser *BrowserManager + Env *EnvironmentManager + Dialog *DialogManager + Event *EventManager + Menu *MenuManager + Screen *ScreenManager + Clipboard *ClipboardManager + SystemTray *SystemTrayManager // Windows windows map[uint]Window @@ -314,7 +299,8 @@ type App struct { menuItems map[uint]*MenuItem menuItemsLock sync.Mutex - // Running + // Starting and running + starting bool running bool runLock sync.Mutex pendingRun []runnable @@ -324,15 +310,15 @@ type App struct { // platform app impl platformApp - // The main application menu - ApplicationMenu *Menu + // The main application menu (private - use app.Menu.GetApplicationMenu/SetApplicationMenu) + applicationMenu *Menu clipboard *Clipboard customEventProcessor *EventProcessor Logger *slog.Logger contextMenus map[string]*ContextMenu - contextMenusLock sync.Mutex + contextMenusLock sync.RWMutex assets *assetserver.AssetServer startURL string @@ -350,7 +336,9 @@ type App struct { keyBindingsLock sync.RWMutex // Shutdown - performingShutdown bool + performingShutdown bool + shutdownLock sync.Mutex + serviceShutdownLock sync.Mutex // Shutdown tasks are run when the application is shutting down. // They are run in the order they are added and run on the main thread. @@ -368,6 +356,16 @@ type App struct { singleInstanceManager *singleInstanceManager } +func (a *App) Config() Options { + return a.options +} + +// Context returns the application context that is canceled when the application shuts down. +// This context should be used for graceful shutdown of goroutines and long-running operations. +func (a *App) Context() context.Context { + return a.ctx +} + func (a *App) handleWarning(msg string) { if a.options.WarningHandler != nil { a.options.WarningHandler(msg) @@ -375,6 +373,7 @@ func (a *App) handleWarning(msg string) { a.Logger.Warn(msg) } } + func (a *App) handleError(err error) { if a.options.ErrorHandler != nil { a.options.ErrorHandler(err) @@ -383,47 +382,29 @@ func (a *App) handleError(err error) { } } -// EmitEvent will emit an event -func (a *App) EmitEvent(name string, data ...any) { - a.customEventProcessor.Emit(&CustomEvent{ - Name: name, - Data: data, - }) +// RegisterService appends the given service to the list of bound services. +// Registered services will be bound and initialised +// in registration order upon calling [App.Run]. +// +// RegisterService will log an error message +// and discard the given service +// if called after [App.Run]. +func (a *App) RegisterService(service Service) { + a.runLock.Lock() + defer a.runLock.Unlock() + + if a.starting || a.running { + a.error("services must be registered before running the application. Service '%s' will not be registered.", getServiceName(service)) + return + } + + a.options.Services = append(a.options.Services, service) } // EmitEvent will emit an event -func (a *App) emitEvent(event *CustomEvent) { - a.customEventProcessor.Emit(event) -} - -// OnEvent will listen for events -func (a *App) OnEvent(name string, callback func(event *CustomEvent)) func() { - return a.customEventProcessor.On(name, callback) -} - -// OffEvent will remove an event listener -func (a *App) OffEvent(name string) { - a.customEventProcessor.Off(name) -} - -// OnMultipleEvent will listen for events a set number of times before unsubscribing. -func (a *App) OnMultipleEvent(name string, callback func(event *CustomEvent), counter int) { - a.customEventProcessor.OnMultiple(name, callback, counter) -} - -// ResetEvents will remove all event listeners and hooks -func (a *App) ResetEvents() { - a.customEventProcessor.OffAll() -} func (a *App) handleFatalError(err error) { - var buffer strings.Builder - buffer.WriteString("\n\n************************ FATAL ******************************\n") - buffer.WriteString("* There has been a catastrophic failure in your application *\n") - buffer.WriteString("********************* Error Details *************************\n") - buffer.WriteString(err.Error()) - buffer.WriteString("*************************************************************\n") - a.handleError(fmt.Errorf(buffer.String())) + a.handleError(&FatalError{err: err}) os.Exit(1) } @@ -438,74 +419,25 @@ func (a *App) init() { a.Logger = a.options.Logger a.pid = os.Getpid() a.wailsEventListeners = make([]WailsEventListener, 0) -} -func (a *App) getSystemTrayID() uint { - a.systemTrayIDLock.Lock() - defer a.systemTrayIDLock.Unlock() - a.systemTrayID++ - return a.systemTrayID -} - -func (a *App) getWindowForID(id uint) Window { - a.windowsLock.RLock() - defer a.windowsLock.RUnlock() - return a.windows[id] -} - -func (a *App) deleteWindowByID(id uint) { - a.windowsLock.Lock() - defer a.windowsLock.Unlock() - delete(a.windows, id) + // Initialize managers + a.Window = newWindowManager(a) + a.ContextMenu = newContextMenuManager(a) + a.KeyBinding = newKeyBindingManager(a) + a.Browser = newBrowserManager(a) + a.Env = newEnvironmentManager(a) + a.Dialog = newDialogManager(a) + a.Event = newEventManager(a) + a.Menu = newMenuManager(a) + a.Screen = newScreenManager(a) + a.Clipboard = newClipboardManager(a) + a.SystemTray = newSystemTrayManager(a) } func (a *App) Capabilities() capabilities.Capabilities { return a.capabilities } -func (a *App) OnApplicationEvent(eventType events.ApplicationEventType, callback func(event *ApplicationEvent)) func() { - eventID := uint(eventType) - a.applicationEventListenersLock.Lock() - defer a.applicationEventListenersLock.Unlock() - listener := &EventListener{ - callback: callback, - } - a.applicationEventListeners[eventID] = append(a.applicationEventListeners[eventID], listener) - if a.impl != nil { - go func() { - defer handlePanic() - a.impl.on(eventID) - }() - } - - return func() { - // lock the map - a.applicationEventListenersLock.Lock() - defer a.applicationEventListenersLock.Unlock() - // Remove listener - a.applicationEventListeners[eventID] = lo.Without(a.applicationEventListeners[eventID], listener) - } -} - -// RegisterApplicationEventHook registers a hook for the given application event. -// Hooks are called before the event listeners and can cancel the event. -// The returned function can be called to remove the hook. -func (a *App) RegisterApplicationEventHook(eventType events.ApplicationEventType, callback func(event *ApplicationEvent)) func() { - eventID := uint(eventType) - a.applicationEventHooksLock.Lock() - defer a.applicationEventHooksLock.Unlock() - thisHook := &eventHook{ - callback: callback, - } - a.applicationEventHooks[eventID] = append(a.applicationEventHooks[eventID], thisHook) - - return func() { - a.applicationEventHooksLock.Lock() - a.applicationEventHooks[eventID] = lo.Without(a.applicationEventHooks[eventID], thisHook) - a.applicationEventHooksLock.Unlock() - } -} - //func (a *App) RegisterListener(listener WailsEventListener) { // a.wailsEventListenerLock.Lock() // a.wailsEventListeners = append(a.wailsEventListeners, listener) @@ -516,10 +448,6 @@ func (a *App) RegisterApplicationEventHook(eventType events.ApplicationEventType // a.assets.AttachServiceHandler(prefix, handler) //} -func (a *App) NewWebviewWindow() *WebviewWindow { - return a.NewWebviewWindowWithOptions(WebviewWindowOptions{}) -} - func (a *App) GetPID() int { return a.pid } @@ -555,38 +483,19 @@ func (a *App) error(message string, args ...any) { a.handleError(fmt.Errorf(message, args...)) } -func (a *App) NewWebviewWindowWithOptions(windowOptions WebviewWindowOptions) *WebviewWindow { - newWindow := NewWindow(windowOptions) - id := newWindow.ID() - - a.windowsLock.Lock() - a.windows[id] = newWindow - a.windowsLock.Unlock() - - // Call hooks - for _, hook := range a.windowCreatedCallbacks { - hook(newWindow) - } - - a.runOrDeferToAppRun(newWindow) - - return newWindow -} - -func (a *App) NewSystemTray() *SystemTray { - id := a.getSystemTrayID() - newSystemTray := newSystemTray(id) - - a.systemTraysLock.Lock() - a.systemTrays[id] = newSystemTray - a.systemTraysLock.Unlock() - - a.runOrDeferToAppRun(newSystemTray) - - return newSystemTray -} - func (a *App) Run() error { + a.runLock.Lock() + // Prevent double invocations. + if a.starting || a.running { + a.runLock.Unlock() + return errors.New("application is running or a previous run has failed") + } + // Block further service registrations. + a.starting = true + a.runLock.Unlock() + + // Ensure application context is canceled in case of failures. + defer a.cancel() // Call post-create hooks err := a.preRun() @@ -595,10 +504,29 @@ func (a *App) Run() error { } a.impl = newPlatformApp(a) + + // Ensure services are shut down in case of failures. + defer a.shutdownServices() + + // Ensure application context is canceled before service shutdown (duplicate calls don't hurt). + defer a.cancel() + + // Startup services before dispatching any events. + // No need to hold the lock here because a.options.Services may only change when a.running is false. + services := a.options.Services + a.options.Services = nil + for i, service := range services { + if err := a.startupService(service); err != nil { + return fmt.Errorf("error starting service '%s': %w", getServiceName(service), err) + } + // Schedule started services for shutdown. + a.options.Services = services[:i+1] + } + go func() { for { event := <-applicationEvents - go a.handleApplicationEvent(event) + go a.Event.handleApplicationEvent(event) } }() go func() { @@ -635,83 +563,84 @@ func (a *App) Run() error { go func() { for { menuItemID := <-menuItemClicked - go a.handleMenuItemClicked(menuItemID) + go a.Menu.handleMenuItemClicked(menuItemID) } }() a.runLock.Lock() a.running = true + a.runLock.Unlock() - for _, systray := range a.pendingRun { + // No need to hold the lock here because + // - a.pendingRun may only change while a.running is false. + // - runnables are scheduled asynchronously anyway. + for _, pending := range a.pendingRun { go func() { defer handlePanic() - systray.Run() + pending.Run() }() } a.pendingRun = nil - a.runLock.Unlock() - // set the application menu if runtime.GOOS == "darwin" { - a.impl.setApplicationMenu(a.ApplicationMenu) + a.impl.setApplicationMenu(a.applicationMenu) } if a.options.Icon != nil { a.impl.setIcon(a.options.Icon) } - err = a.impl.run() + return a.impl.run() +} + +func (a *App) startupService(service Service) error { + err := a.bindings.Add(service) if err != nil { - return err + return fmt.Errorf("cannot bind service methods: %w", err) } - // Cancel the context - a.cancel() - - for _, service := range a.options.Services { - // If it conforms to the ServiceShutdown interface, call the Shutdown method - if thisService, ok := service.instance.(ServiceShutdown); ok { - err := thisService.ServiceShutdown() - if err != nil { - a.error("Error shutting down service: " + err.Error()) - } + if service.options.Route != "" { + handler, ok := service.Instance().(http.Handler) + if !ok { + handler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + http.Error( + rw, + fmt.Sprintf("Service '%s' does not handle HTTP requests", getServiceName(service)), + http.StatusServiceUnavailable, + ) + }) } + a.assets.AttachServiceHandler(service.options.Route, handler) + } + + if s, ok := service.instance.(ServiceStartup); ok { + a.debug("Starting up service:", "name", getServiceName(service)) + return s.ServiceStartup(a.ctx, service.options) } return nil } -func (a *App) handleApplicationEvent(event *ApplicationEvent) { - defer handlePanic() - a.applicationEventListenersLock.RLock() - listeners, ok := a.applicationEventListeners[event.Id] - a.applicationEventListenersLock.RUnlock() - if !ok { - return - } +func (a *App) shutdownServices() { + // Acquire lock to prevent double calls (defer in Run() + OnShutdown) + a.serviceShutdownLock.Lock() + defer a.serviceShutdownLock.Unlock() - // Process Hooks - a.applicationEventHooksLock.RLock() - hooks, ok := a.applicationEventHooks[event.Id] - a.applicationEventHooksLock.RUnlock() - if ok { - for _, thisHook := range hooks { - thisHook.callback(event) - if event.IsCancelled() { - return + // Ensure app context is canceled first (duplicate calls don't hurt). + a.cancel() + + for len(a.options.Services) > 0 { + last := len(a.options.Services) - 1 + service := a.options.Services[last] + a.options.Services = a.options.Services[:last] // Prevent double shutdowns + + if s, ok := service.instance.(ServiceShutdown); ok { + a.debug("Shutting down service:", "name", getServiceName(service)) + if err := s.ServiceShutdown(); err != nil { + a.error("error shutting down service '%s': %w", getServiceName(service), err) } } } - - for _, listener := range listeners { - go func() { - if event.IsCancelled() { - return - } - defer handlePanic() - listener.callback(event) - }() - } } func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) { @@ -721,7 +650,7 @@ func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) { window, ok := a.windows[event.windowId] a.windowsLock.Unlock() if !ok { - log.Printf("WebviewWindow #%d not found", event.windowId) + a.warning("WebviewWindow #%d not found", event.windowId) return } // Get callback from window @@ -735,7 +664,7 @@ func (a *App) handleWindowMessage(event *windowMessage) { window, ok := a.windows[event.windowId] a.windowsLock.RUnlock() if !ok { - log.Printf("WebviewWindow #%d not found", event.windowId) + a.warning("WebviewWindow #%d not found", event.windowId) return } // Check if the message starts with "wails:" @@ -760,62 +689,47 @@ func (a *App) handleWindowEvent(event *windowEvent) { window, ok := a.windows[event.WindowID] a.windowsLock.RUnlock() if !ok { - log.Printf("Window #%d not found", event.WindowID) + a.warning("Window #%d not found", event.WindowID) return } window.HandleWindowEvent(event.EventID) } -func (a *App) handleMenuItemClicked(menuItemID uint) { - defer handlePanic() - - menuItem := getMenuItemByID(menuItemID) - if menuItem == nil { - log.Printf("MenuItem #%d not found", menuItemID) - return - } - menuItem.handleClick() -} - -func (a *App) CurrentWindow() *WebviewWindow { - if a.impl == nil { - return nil - } - id := a.impl.getCurrentWindowID() - a.windowsLock.RLock() - defer a.windowsLock.RUnlock() - result := a.windows[id] - if result == nil { - return nil - } - return result.(*WebviewWindow) -} - // OnShutdown adds a function to be run when the application is shutting down. func (a *App) OnShutdown(f func()) { if f == nil { return } - a.shutdownTasks = append(a.shutdownTasks, f) -} -func (a *App) destroySystemTray(tray *SystemTray) { - // Remove the system tray from the a.systemTrays map - a.systemTraysLock.Lock() - delete(a.systemTrays, tray.id) - a.systemTraysLock.Unlock() - tray.destroy() + a.shutdownLock.Lock() + + if !a.performingShutdown { + defer a.shutdownLock.Unlock() + a.shutdownTasks = append(a.shutdownTasks, f) + return + } + + a.shutdownLock.Unlock() + InvokeAsync(f) } func (a *App) cleanup() { + a.shutdownLock.Lock() if a.performingShutdown { + a.shutdownLock.Unlock() return } + a.cancel() // Cancel app context before running shutdown hooks. a.performingShutdown = true + a.shutdownLock.Unlock() + + // No need to hold the lock here because a.shutdownTasks + // may only change while a.performingShutdown is false. for _, shutdownTask := range a.shutdownTasks { InvokeSync(shutdownTask) } InvokeSync(func() { + a.shutdownServices() a.windowsLock.RLock() for _, window := range a.windows { window.Close() @@ -828,17 +742,23 @@ func (a *App) cleanup() { } a.systemTrays = nil a.systemTraysLock.Unlock() + + // Cleanup single instance manager + if a.singleInstanceManager != nil { + a.singleInstanceManager.cleanup() + } + + a.postQuit() + + if a.options.PostShutdown != nil { + a.options.PostShutdown() + } }) - // Cleanup single instance manager - if a.singleInstanceManager != nil { - a.singleInstanceManager.cleanup() - } } func (a *App) Quit() { if a.impl != nil { InvokeSync(a.impl.destroy) - a.postQuit() } } @@ -848,18 +768,6 @@ func (a *App) SetIcon(icon []byte) { } } -func (a *App) SetMenu(menu *Menu) { - a.ApplicationMenu = menu - if a.impl != nil { - a.impl.setApplicationMenu(menu) - } -} -func (a *App) ShowAboutDialog() { - if a.impl != nil { - a.impl.showAboutDialog(a.options.Name, a.options.Description, a.options.Icon) - } -} - func InfoDialog() *MessageDialog { return newMessageDialog(InfoDialogType) } @@ -884,27 +792,6 @@ func SaveFileDialog() *SaveFileDialogStruct { return newSaveFileDialog() } -// NOTE: should use screenManager directly after DPI is implemented in all platforms -// (should also get rid of the error return) -func (a *App) GetScreens() ([]*Screen, error) { - return a.impl.getScreens() - // return a.screenManager.screens, nil -} - -// NOTE: should use screenManager directly after DPI is implemented in all platforms -// (should also get rid of the error return) -func (a *App) GetPrimaryScreen() (*Screen, error) { - return a.impl.getPrimaryScreen() - // return a.screenManager.primaryScreen, nil -} - -func (a *App) Clipboard() *Clipboard { - if a.clipboard == nil { - a.clipboard = newClipboard() - } - return a.clipboard -} - func (a *App) dispatchOnMainThread(fn func()) { // If we are on the main thread, just call the function if a.impl.isOnMainThread() { @@ -920,43 +807,6 @@ func (a *App) dispatchOnMainThread(fn func()) { a.impl.dispatchOnMainThread(id) } -func OpenFileDialogWithOptions(options *OpenFileDialogOptions) *OpenFileDialogStruct { - result := OpenFileDialog() - result.SetOptions(options) - return result -} - -func SaveFileDialogWithOptions(s *SaveFileDialogOptions) *SaveFileDialogStruct { - result := SaveFileDialog() - result.SetOptions(s) - return result -} - -func (a *App) dispatchEventToListeners(event *CustomEvent) { - listeners := a.wailsEventListeners - - for _, window := range a.windows { - if event.IsCancelled() { - return - } - window.DispatchWailsEvent(event) - } - - for _, listener := range listeners { - if event.IsCancelled() { - return - } - listener.DispatchWailsEvent(event) - } -} - -func (a *App) IsDarkMode() bool { - if a.impl == nil { - return false - } - return a.impl.isDarkMode() -} - func (a *App) Hide() { if a.impl != nil { a.impl.hide() @@ -969,84 +819,19 @@ func (a *App) Show() { } } -func (a *App) registerContextMenu(menu *ContextMenu) { - a.contextMenusLock.Lock() - defer a.contextMenusLock.Unlock() - a.contextMenus[menu.name] = menu -} - -func (a *App) unregisterContextMenu(name string) { - a.contextMenusLock.Lock() - defer a.contextMenusLock.Unlock() - delete(a.contextMenus, name) -} - -func (a *App) getContextMenu(name string) (*ContextMenu, bool) { - a.contextMenusLock.Lock() - defer a.contextMenusLock.Unlock() - menu, ok := a.contextMenus[name] - return menu, ok - -} - -func (a *App) OnWindowCreation(callback func(window Window)) { - a.windowCreatedCallbacks = append(a.windowCreatedCallbacks, callback) -} - -func (a *App) GetWindowByName(name string) Window { - a.windowsLock.RLock() - defer a.windowsLock.RUnlock() - for _, window := range a.windows { - if window.Name() == name { - return window - } - } - return nil -} - func (a *App) runOrDeferToAppRun(r runnable) { a.runLock.Lock() - running := a.running - if !running { + + if !a.running { + defer a.runLock.Unlock() // Defer unlocking for panic tolerance. a.pendingRun = append(a.pendingRun, r) + return } + + // Unlock immediately to prevent deadlocks. + // No TOC/TOU risk here because a.running can never switch back to false. a.runLock.Unlock() - - if running { - r.Run() - } -} - -func (a *App) processKeyBinding(acceleratorString string, window *WebviewWindow) bool { - if len(a.keyBindings) == 0 { - return false - } - - a.keyBindingsLock.RLock() - defer a.keyBindingsLock.RUnlock() - - // Check key bindings - callback, ok := a.keyBindings[acceleratorString] - if !ok { - return false - } - - // Execute callback - go callback(window) - - return true -} - -func (a *App) addKeyBinding(acceleratorString string, callback func(window *WebviewWindow)) { - a.keyBindingsLock.Lock() - defer a.keyBindingsLock.Unlock() - a.keyBindings[acceleratorString] = callback -} - -func (a *App) removeKeyBinding(acceleratorString string) { - a.keyBindingsLock.Lock() - defer a.keyBindingsLock.Unlock() - delete(a.keyBindings, acceleratorString) + r.Run() } func (a *App) handleWindowKeyEvent(event *windowKeyEvent) { @@ -1056,47 +841,16 @@ func (a *App) handleWindowKeyEvent(event *windowKeyEvent) { window, ok := a.windows[event.windowId] a.windowsLock.RUnlock() if !ok { - log.Printf("WebviewWindow #%d not found", event.windowId) + a.warning("WebviewWindow #%d not found", event.windowId) return } // Get callback from window window.HandleKeyEvent(event.acceleratorString) } -func (a *App) AssetServerHandler() func(rw http.ResponseWriter, req *http.Request) { - return a.assets.ServeHTTP -} - -func (a *App) BrowserOpenURL(url string) error { - return browser.OpenURL(url) -} - -func (a *App) BrowserOpenFile(path string) error { - return browser.OpenFile(path) -} - -func (a *App) Environment() EnvironmentInfo { - info, _ := operatingsystem.Info() - result := EnvironmentInfo{ - OS: runtime.GOOS, - Arch: runtime.GOARCH, - Debug: a.isDebugMode, - OSInfo: info, - } - result.PlatformInfo = a.platformEnvironment() - return result -} - func (a *App) shouldQuit() bool { if a.options.ShouldQuit != nil { return a.options.ShouldQuit() } return true } - -// OpenFileManager opens the file manager at the specified path, optionally selecting the file. -func (a *App) OpenFileManager(path string, selectFile bool) error { - return InvokeSyncWithError(func() error { - return fileexplorer.OpenFileManager(path, selectFile) - }) -} diff --git a/v3/pkg/application/application_darwin.go b/v3/pkg/application/application_darwin.go index 4a88b9a25..cb38f7e70 100644 --- a/v3/pkg/application/application_darwin.go +++ b/v3/pkg/application/application_darwin.go @@ -57,6 +57,8 @@ static void init(void) { NSDistributedNotificationCenter *center = [NSDistributedNotificationCenter defaultCenter]; [center addObserver:appDelegate selector:@selector(themeChanged:) name:@"AppleInterfaceThemeChangedNotification" object:nil]; + // Register the custom URL scheme handler + StartCustomProtocolHandler(); } static bool isDarkMode(void) { @@ -73,6 +75,32 @@ static bool isDarkMode(void) { return [interfaceStyle isEqualToString:@"Dark"]; } +static char* getAccentColor(void) { + @autoreleasepool { + NSColor *accentColor; + if (@available(macOS 10.14, *)) { + accentColor = [NSColor controlAccentColor]; + } else { + // Fallback to system blue for older macOS versions + accentColor = [NSColor systemBlueColor]; + } + // Convert to RGB color space + NSColor *rgbColor = [accentColor colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (rgbColor == nil) { + rgbColor = accentColor; + } + // Get RGB components + CGFloat red, green, blue, alpha; + [rgbColor getRed:&red green:&green blue:&blue alpha:&alpha]; + // Convert to 0-255 range and format as rgb() string + int r = (int)(red * 255); + int g = (int)(green * 255); + int b = (int)(blue * 255); + NSString *colorString = [NSString stringWithFormat:@"rgb(%d,%d,%d)", r, g, b]; + return strdup([colorString UTF8String]); + } +} + static void setApplicationShouldTerminateAfterLastWindowClosed(bool shouldTerminate) { // Get the NSApp delegate AppDelegate *appDelegate = (AppDelegate*)[NSApp delegate]; @@ -186,6 +214,12 @@ func (m *macosApp) isDarkMode() bool { return bool(C.isDarkMode()) } +func (m *macosApp) getAccentColor() string { + accentColorC := C.getAccentColor() + defer C.free(unsafe.Pointer(accentColorC)) + return C.GoString(accentColorC) +} + func getNativeApplication() *macosApp { return globalApplication.impl.(*macosApp) } @@ -235,7 +269,7 @@ func (m *macosApp) run() error { C.startSingleInstanceListener(cUniqueID) } // Add a hook to the ApplicationDidFinishLaunching event - m.parent.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(*ApplicationEvent) { + m.parent.Event.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(*ApplicationEvent) { C.setApplicationShouldTerminateAfterLastWindowClosed(C.bool(m.parent.options.Mac.ApplicationShouldTerminateAfterLastWindowClosed)) C.setActivationPolicy(C.int(m.parent.options.Mac.ActivationPolicy)) C.activateIgnoringOtherApps() @@ -290,7 +324,7 @@ func processApplicationEvent(eventID C.uint, data unsafe.Pointer) { switch event.Id { case uint(events.Mac.ApplicationDidChangeTheme): - isDark := globalApplication.IsDarkMode() + isDark := globalApplication.Env.IsDarkMode() event.Context().setIsDarkMode(isDark) } applicationEvents <- event @@ -314,10 +348,16 @@ func processMessage(windowID C.uint, message *C.char) { //export processURLRequest func processURLRequest(windowID C.uint, wkUrlSchemeTask unsafe.Pointer) { + window, ok := globalApplication.Window.GetByID(uint(windowID)) + if !ok || window == nil { + globalApplication.debug("could not find window with id: %d", windowID) + return + } + webviewRequests <- &webViewAssetRequest{ Request: webview.NewRequest(wkUrlSchemeTask), windowId: uint(windowID), - windowName: globalApplication.getWindowForID(uint(windowID)).Name(), + windowName: window.Name(), } } @@ -362,7 +402,7 @@ func cleanup() { func (a *App) logPlatformInfo() { info, err := operatingsystem.Info() if err != nil { - a.error("Error getting OS info: %s", err.Error()) + a.error("error getting OS info: %w", err) return } @@ -390,3 +430,16 @@ func HandleOpenFile(filePath *C.char) { ctx: eventContext, } } + +//export HandleCustomProtocol +func HandleCustomProtocol(urlCString *C.char) { + urlString := C.GoString(urlCString) + eventContext := newApplicationEventContext() + eventContext.setURL(urlString) + + // Emit the standard event with the URL string as data + applicationEvents <- &ApplicationEvent{ + Id: uint(events.Common.ApplicationLaunchedWithUrl), + ctx: eventContext, + } +} diff --git a/v3/pkg/application/application_darwin_delegate.h b/v3/pkg/application/application_darwin_delegate.h index 1523356b2..77c30898b 100644 --- a/v3/pkg/application/application_darwin_delegate.h +++ b/v3/pkg/application/application_darwin_delegate.h @@ -13,4 +13,13 @@ extern void HandleOpenFile(char *); -#endif +// Declarations for Apple Event based custom URL handling +extern void HandleCustomProtocol(char*); + +@interface CustomProtocolSchemeHandler : NSObject ++ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent; +@end + +void StartCustomProtocolHandler(void); + +#endif /* appdelegate_h */ diff --git a/v3/pkg/application/application_darwin_delegate.m b/v3/pkg/application/application_darwin_delegate.m index 117968d0a..87504e62b 100644 --- a/v3/pkg/application/application_darwin_delegate.m +++ b/v3/pkg/application/application_darwin_delegate.m @@ -1,6 +1,7 @@ //go:build darwin #import "application_darwin_delegate.h" #import "../events/events_darwin.h" +#import // For Apple Event constants extern bool hasListeners(unsigned int); extern bool shouldQuitApplication(); extern void cleanup(); @@ -41,7 +42,7 @@ extern void handleSecondInstanceData(char * message); return YES; } - (BOOL)applicationShouldHandleReopen:(NSNotification *)notification - hasVisibleWindows:(BOOL)flag { + hasVisibleWindows:(BOOL)flag { // Changed from NSApplication to NSNotification if( hasListeners(EventApplicationShouldHandleReopen) ) { processApplicationEvent(EventApplicationShouldHandleReopen, @{@"hasVisibleWindows": @(flag)}); } @@ -179,3 +180,19 @@ extern void handleSecondInstanceData(char * message); // GENERATED EVENTS END @end +// Implementation for Apple Event based custom URL handling +@implementation CustomProtocolSchemeHandler ++ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + if (urlStr) { + HandleCustomProtocol((char*)[urlStr UTF8String]); + } +} +@end +void StartCustomProtocolHandler(void) { + NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; + [appleEventManager setEventHandler:[CustomProtocolSchemeHandler class] + andSelector:@selector(handleGetURLEvent:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID: kAEGetURL]; +} diff --git a/v3/pkg/application/application_dev.go b/v3/pkg/application/application_dev.go index fe19ceb34..e12033e33 100644 --- a/v3/pkg/application/application_dev.go +++ b/v3/pkg/application/application_dev.go @@ -3,9 +3,10 @@ package application import ( - "github.com/wailsapp/wails/v3/internal/assetserver" "net/http" "time" + + "github.com/wailsapp/wails/v3/internal/assetserver" ) var devMode = false diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index c56b668a1..b92c2c6aa 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -16,9 +16,12 @@ import "C" import ( "fmt" "os" + "slices" "strings" "sync" + "path/filepath" + "github.com/godbus/dbus/v5" "github.com/wailsapp/wails/v3/internal/operatingsystem" "github.com/wailsapp/wails/v3/pkg/events" @@ -27,7 +30,8 @@ import ( func init() { // FIXME: This should be handled appropriately in the individual files most likely. // Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings - if os.Getenv("GDK_BACKEND") == "" && (os.Getenv("XDG_SESSION_TYPE") == "" || os.Getenv("XDG_SESSION_TYPE") == "unspecified" || os.Getenv("XDG_SESSION_TYPE") == "x11") { + if os.Getenv("GDK_BACKEND") == "" && + (os.Getenv("XDG_SESSION_TYPE") == "" || os.Getenv("XDG_SESSION_TYPE") == "unspecified" || os.Getenv("XDG_SESSION_TYPE") == "x11") { _ = os.Setenv("GDK_BACKEND", "x11") } } @@ -88,13 +92,46 @@ func (a *linuxApp) setApplicationMenu(menu *Menu) { if menu == nil { // Create a default menu menu = DefaultApplicationMenu() - globalApplication.ApplicationMenu = menu + globalApplication.applicationMenu = menu } } func (a *linuxApp) run() error { - a.parent.OnApplicationEvent(events.Linux.ApplicationStartup, func(evt *ApplicationEvent) { + if len(os.Args) == 2 { // Case: program + 1 argument + arg1 := os.Args[1] + // Check if the argument is likely a URL from a custom protocol invocation + if strings.Contains(arg1, "://") { + a.parent.info("Application launched with argument, potentially a URL from custom protocol", "url", arg1) + eventContext := newApplicationEventContext() + eventContext.setURL(arg1) + applicationEvents <- &ApplicationEvent{ + Id: uint(events.Common.ApplicationLaunchedWithUrl), + ctx: eventContext, + } + } else { + // Check if the argument matches any file associations + if a.parent.options.FileAssociations != nil { + ext := filepath.Ext(arg1) + if slices.Contains(a.parent.options.FileAssociations, ext) { + a.parent.info("File opened via file association", "file", arg1, "extension", ext) + eventContext := newApplicationEventContext() + eventContext.setOpenedWithFile(arg1) + applicationEvents <- &ApplicationEvent{ + Id: uint(events.Common.ApplicationOpenedWithFile), + ctx: eventContext, + } + return nil + } + } + a.parent.info("Application launched with single argument (not a URL), potential file open?", "arg", arg1) + } + } else if len(os.Args) > 2 { + // Log if multiple arguments are passed + a.parent.info("Application launched with multiple arguments", "args", os.Args[1:]) + } + + a.parent.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(evt *ApplicationEvent) { // TODO: What should happen here? }) a.setupCommonEvents() @@ -136,12 +173,21 @@ func (a *linuxApp) isDarkMode() bool { return strings.Contains(a.theme, "dark") } +func (a *linuxApp) getAccentColor() string { + // Linux doesn't have a unified system accent color API + // Return a default blue color + return "rgb(0,122,255)" +} + func (a *linuxApp) monitorThemeChanges() { go func() { defer handlePanic() conn, err := dbus.ConnectSessionBus() if err != nil { - a.parent.info("[WARNING] Failed to connect to session bus; monitoring for theme changes will not function:", err) + a.parent.info( + "[WARNING] Failed to connect to session bus; monitoring for theme changes will not function:", + err, + ) return } defer conn.Close() @@ -208,7 +254,7 @@ func newPlatformApp(parent *App) *linuxApp { func (a *App) logPlatformInfo() { info, err := operatingsystem.Info() if err != nil { - a.error("Error getting OS info: %s", err.Error()) + a.error("error getting OS info: %w", err) return } diff --git a/v3/pkg/application/application_options.go b/v3/pkg/application/application_options.go index d3f02f3ff..76d77457d 100644 --- a/v3/pkg/application/application_options.go +++ b/v3/pkg/application/application_options.go @@ -31,11 +31,21 @@ type Options struct { // Services allows you to bind Go methods to the frontend. Services []Service + // MarshalError will be called if non-nil + // to marshal to JSON the error values returned by service methods. + // + // MarshalError is not allowed to fail, + // but it may return a nil slice to fall back + // to the default error handling mechanism. + // + // If the returned slice is not nil, it must contain valid JSON. + MarshalError func(error) []byte + // BindAliases allows you to specify alias IDs for your bound methods. // Example: `BindAliases: map[uint32]uint32{1: 1411160069}` states that alias ID 1 maps to the Go method with ID 1411160069. BindAliases map[uint32]uint32 - // Logger i a slog.Logger instance used for logging Wails system messages (not application messages). + // Logger is a slog.Logger instance used for logging Wails system messages (not application messages). // If not defined, a default logger is used. Logger *slog.Logger @@ -60,9 +70,17 @@ type Options struct { // OnShutdown is called when the application is about to terminate. // This is useful for cleanup tasks. - // The shutdown process blocks until this function returns + // The shutdown process blocks until this function returns. OnShutdown func() + // PostShutdown is called after the application + // has finished shutting down, just before process termination. + // This is useful for testing and logging purposes + // on platforms where the Run() method does not return. + // When PostShutdown is called, the application instance is not usable anymore. + // The shutdown process blocks until this function returns. + PostShutdown func() + // ShouldQuit is a function that is called when the user tries to quit the application. // If the function returns true, the application will quit. // If the function returns false, the application will not quit. diff --git a/v3/pkg/application/application_windows.go b/v3/pkg/application/application_windows.go index 4622ee8d3..f72abf963 100644 --- a/v3/pkg/application/application_windows.go +++ b/v3/pkg/application/application_windows.go @@ -3,12 +3,15 @@ package application import ( - "fmt" + "errors" "os" "path/filepath" "slices" + "strings" "sync" + "sync/atomic" "syscall" + "time" "unsafe" "github.com/wailsapp/go-webview2/webviewloader" @@ -18,6 +21,10 @@ import ( "github.com/wailsapp/wails/v3/pkg/w32" ) +var ( + wmTaskbarCreated = w32.RegisterWindowMessage(w32.MustStringToUTF16Ptr("TaskbarCreated")) +) + type windowsApp struct { parent *App @@ -40,12 +47,25 @@ type windowsApp struct { // system theme isCurrentlyDarkMode bool currentWindowID uint + + // Restart taskbar flag + restartingTaskbar atomic.Bool } func (m *windowsApp) isDarkMode() bool { return w32.IsCurrentlyDarkMode() } +func (m *windowsApp) getAccentColor() string { + accentColor, err := w32.GetAccentColor() + if err != nil { + m.parent.error("failed to get accent color: %w", err) + return "rgb(0,122,255)" + } + + return accentColor +} + func (m *windowsApp) isOnMainThread() bool { return m.mainThreadID == w32.GetCurrentThreadId() } @@ -121,7 +141,7 @@ func (m *windowsApp) setApplicationMenu(menu *Menu) { } menu.Update() - m.parent.ApplicationMenu = menu + m.parent.applicationMenu = menu } func (m *windowsApp) run() error { @@ -135,20 +155,35 @@ func (m *windowsApp) run() error { ctx: blankApplicationEventContext, } - // Check if there is 1 parameter passed to the application - // and if the extension matches the options.FileAssociations string - if len(os.Args) == 2 { - arg := os.Args[1] - ext := filepath.Ext(arg) - if slices.Contains(m.parent.options.FileAssociations, ext) { + if len(os.Args) == 2 { // Case: program + 1 argument + arg1 := os.Args[1] + // Check if the argument is likely a URL from a custom protocol invocation + if strings.Contains(arg1, "://") { + m.parent.info("Application launched with argument, potentially a URL from custom protocol", "url", arg1) eventContext := newApplicationEventContext() - eventContext.setOpenedWithFile(arg) - // EmitEvent application started event + eventContext.setURL(arg1) applicationEvents <- &ApplicationEvent{ - Id: uint(events.Common.ApplicationOpenedWithFile), + Id: uint(events.Common.ApplicationLaunchedWithUrl), ctx: eventContext, } + } else { + // If not a URL-like string, check for file association + if m.parent.options.FileAssociations != nil { + ext := filepath.Ext(arg1) + if slices.Contains(m.parent.options.FileAssociations, ext) { + m.parent.info("Application launched with file via file association", "file", arg1) + eventContext := newApplicationEventContext() + eventContext.setOpenedWithFile(arg1) + applicationEvents <- &ApplicationEvent{ + Id: uint(events.Common.ApplicationOpenedWithFile), + ctx: eventContext, + } + } + } } + } else if len(os.Args) > 2 { + // Log if multiple arguments are passed, though typical protocol/file launch is a single arg. + m.parent.info("Application launched with multiple arguments", "args", os.Args[1:]) } _ = m.runMainLoop() @@ -215,12 +250,23 @@ func (m *windowsApp) wndProc(hwnd w32.HWND, msg uint32, wParam, lParam uintptr) if msg == w32.WM_DISPLAYCHANGE || (msg == w32.WM_SETTINGCHANGE && wParam == w32.SPI_SETWORKAREA) { err := m.processAndCacheScreens() if err != nil { - m.parent.error(err.Error()) + m.parent.handleError(err) } } } switch msg { + case wmTaskbarCreated: + if m.restartingTaskbar.Load() { + break + } + m.restartingTaskbar.Store(true) + m.reshowSystrays() + go func() { + // 1 second debounce + time.Sleep(1000) + m.restartingTaskbar.Store(false) + }() case w32.WM_SETTINGCHANGE: settingChanged := w32.UTF16PtrToString((*uint16)(unsafe.Pointer(lParam))) if settingChanged == "ImmersiveColorSet" { @@ -297,6 +343,14 @@ func (m *windowsApp) unregisterWindow(w *windowsWebviewWindow) { } } +func (m *windowsApp) reshowSystrays() { + m.systrayMapLock.Lock() + defer m.systrayMapLock.Unlock() + for _, systray := range m.systrayMap { + systray.reshow() + } +} + func setupDPIAwareness() error { // https://learn.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process // https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows @@ -318,14 +372,14 @@ func setupDPIAwareness() error { return w32.SetProcessDPIAware() } - return fmt.Errorf("no DPI awareness method supported") + return errors.New("no DPI awareness method supported") } func newPlatformApp(app *App) *windowsApp { err := setupDPIAwareness() if err != nil { - app.error(err.Error()) + app.handleError(err) } result := &windowsApp{ @@ -337,7 +391,7 @@ func newPlatformApp(app *App) *windowsApp { err = result.processAndCacheScreens() if err != nil { - app.fatal(err.Error()) + app.handleFatalError(err) } result.init() @@ -349,7 +403,9 @@ func newPlatformApp(app *App) *windowsApp { func (a *App) logPlatformInfo() { var args []any args = append(args, "Go-WebView2Loader", webviewloader.UsingGoWebview2Loader) - webviewVersion, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(a.options.Windows.WebviewBrowserPath) + webviewVersion, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString( + a.options.Windows.WebviewBrowserPath, + ) if err != nil { args = append(args, "WebView2", "Error: "+err.Error()) } else { @@ -364,7 +420,9 @@ func (a *App) logPlatformInfo() { func (a *App) platformEnvironment() map[string]any { result := map[string]any{} - webviewVersion, _ := webviewloader.GetAvailableCoreWebView2BrowserVersionString(a.options.Windows.WebviewBrowserPath) + webviewVersion, _ := webviewloader.GetAvailableCoreWebView2BrowserVersionString( + a.options.Windows.WebviewBrowserPath, + ) result["Go-WebView2Loader"] = webviewloader.UsingGoWebview2Loader result["WebView2"] = webviewVersion return result diff --git a/v3/pkg/application/bindings.go b/v3/pkg/application/bindings.go index e690e7576..431caab62 100644 --- a/v3/pkg/application/bindings.go +++ b/v3/pkg/application/bindings.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "net/http" "reflect" "runtime" "strings" @@ -21,16 +20,22 @@ type CallOptions struct { Args []json.RawMessage `json:"args"` } -type PluginCallOptions struct { - Name string `json:"name"` - Args []json.RawMessage `json:"args"` +type ErrorKind string + +const ( + ReferenceError ErrorKind = "ReferenceError" + TypeError ErrorKind = "TypeError" + RuntimeError ErrorKind = "RuntimeError" +) + +type CallError struct { + Kind ErrorKind `json:"kind"` + Message string `json:"message"` + Cause any `json:"cause,omitempty"` } -var reservedPluginMethods = []string{ - "Name", - "Init", - "Shutdown", - "Exported", +func (e *CallError) Error() string { + return e.Message } // Parameter defines a Go method parameter @@ -61,66 +66,75 @@ func (p *Parameter) IsError() bool { // BoundMethod defines all the data related to a Go method that is // bound to the Wails application type BoundMethod struct { - ID uint32 `json:"id"` - Name string `json:"name"` - Inputs []*Parameter `json:"inputs,omitempty"` - Outputs []*Parameter `json:"outputs,omitempty"` - Comments string `json:"comments,omitempty"` - Method reflect.Value `json:"-"` - TypeName string - PackagePath string + ID uint32 `json:"id"` + Name string `json:"name"` + Inputs []*Parameter `json:"inputs,omitempty"` + Outputs []*Parameter `json:"outputs,omitempty"` + Comments string `json:"comments,omitempty"` + Method reflect.Value `json:"-"` + FQN string + marshalError func(error) []byte needsContext bool } type Bindings struct { + marshalError func(error) []byte boundMethods map[string]*BoundMethod boundByID map[uint32]*BoundMethod methodAliases map[uint32]uint32 } -func NewBindings(instances []Service, aliases map[uint32]uint32) (*Bindings, error) { - app := Get() - b := &Bindings{ +func NewBindings(marshalError func(error) []byte, aliases map[uint32]uint32) *Bindings { + return &Bindings{ + marshalError: wrapErrorMarshaler(marshalError, defaultMarshalError), boundMethods: make(map[string]*BoundMethod), boundByID: make(map[uint32]*BoundMethod), methodAliases: aliases, } - for _, binding := range instances { - handler, ok := binding.Instance().(http.Handler) - if ok && binding.options.Route != "" { - app.assets.AttachServiceHandler(binding.options.Route, handler) - } - err := b.Add(binding.Instance()) - if err != nil { - return nil, err - } - } - return b, nil } -// Add the given named type pointer methods to the Bindings -func (b *Bindings) Add(namedPtr interface{}) error { - methods, err := b.getMethods(namedPtr) +// Add adds the given service to the bindings. +func (b *Bindings) Add(service Service) error { + methods, err := getMethods(service.Instance()) if err != nil { - return fmt.Errorf("cannot bind value to app: %s", err.Error()) + return err + } + + marshalError := wrapErrorMarshaler(service.options.MarshalError, defaultMarshalError) + + // Validate and log methods. + for _, method := range methods { + if _, ok := b.boundMethods[method.FQN]; ok { + return fmt.Errorf("bound method '%s' is already registered. Please note that you can register at most one service of each type; additional instances must be wrapped in dedicated structs", method.FQN) + } + if boundMethod, ok := b.boundByID[method.ID]; ok { + return fmt.Errorf("oh wow, we're sorry about this! Amazingly, a hash collision was detected for method '%s' (it generates the same hash as '%s'). To use this method, please rename it. Sorry :(", method.FQN, boundMethod.FQN) + } + + // Log + attrs := []any{"fqn", method.FQN, "id", method.ID} + if alias, ok := lo.FindKey(b.methodAliases, method.ID); ok { + attrs = append(attrs, "alias", alias) + } + globalApplication.debug("Registering bound method:", attrs...) } for _, method := range methods { - // Add it as a regular method - b.boundMethods[method.String()] = method + // Store composite error marshaler + method.marshalError = marshalError + + // Register method + b.boundMethods[method.FQN] = method b.boundByID[method.ID] = method } + return nil } // Get returns the bound method with the given name func (b *Bindings) Get(options *CallOptions) *BoundMethod { - method, ok := b.boundMethods[options.MethodName] - if !ok { - return nil - } - return method + return b.boundMethods[options.MethodName] } // GetByID returns the bound method with the given ID @@ -131,29 +145,27 @@ func (b *Bindings) GetByID(id uint32) *BoundMethod { id = alias } } - result := b.boundByID[id] - return result + + return b.boundByID[id] } -// GenerateID generates a unique ID for a binding -func (b *Bindings) GenerateID(name string) (uint32, error) { - id, err := hash.Fnv(name) - if err != nil { - return 0, err - } - // Check if we already have it - boundMethod, ok := b.boundByID[id] - if ok { - return 0, fmt.Errorf("oh wow, we're sorry about this! Amazingly, a hash collision was detected for method '%s' (it generates the same hash as '%s'). To continue, please rename it. Sorry :(", name, boundMethod.String()) - } - return id, nil +// internalServiceMethod is a set of methods +// that are handled specially by the binding engine +// and must not be exposed to the frontend. +// +// For simplicity we exclude these by name +// without checking their signatures, +// and so does the binding generator. +var internalServiceMethods = map[string]bool{ + "ServiceName": true, + "ServiceStartup": true, + "ServiceShutdown": true, + "ServeHTTP": true, } -func (b *BoundMethod) String() string { - return fmt.Sprintf("%s.%s.%s", b.PackagePath, b.TypeName, b.Name) -} +var ctxType = reflect.TypeFor[context.Context]() -func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { +func getMethods(value any) ([]*BoundMethod, error) { // Create result placeholder var result []*BoundMethod @@ -180,42 +192,27 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { return nil, fmt.Errorf("%s.%s is a generic type. Generic bound types are not supported", packagePath, namedType.String()) } - ctxType := reflect.TypeFor[context.Context]() - // Process Methods - for i := 0; i < ptrType.NumMethod(); i++ { - methodDef := ptrType.Method(i) - methodName := methodDef.Name - method := namedValue.MethodByName(methodName) + for i := range ptrType.NumMethod() { + methodName := ptrType.Method(i).Name + method := namedValue.Method(i) - if b.internalMethod(methodDef) { + if internalServiceMethods[methodName] { continue } + fqn := fmt.Sprintf("%s.%s.%s", packagePath, typeName, methodName) + // Create new method boundMethod := &BoundMethod{ - Name: methodName, - PackagePath: packagePath, - TypeName: typeName, - Inputs: nil, - Outputs: nil, - Comments: "", - Method: method, + ID: hash.Fnv(fqn), + FQN: fqn, + Name: methodName, + Inputs: nil, + Outputs: nil, + Comments: "", + Method: method, } - var err error - boundMethod.ID, err = b.GenerateID(boundMethod.String()) - if err != nil { - return nil, err - } - - args := []any{"name", boundMethod, "id", boundMethod.ID} - if b.methodAliases != nil { - alias, found := lo.FindKey(b.methodAliases, boundMethod.ID) - if found { - args = append(args, "alias", alias) - } - } - globalApplication.debug("Adding method:", args...) // Iterate inputs methodType := method.Type() @@ -245,40 +242,23 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { result = append(result, boundMethod) } + return result, nil } -func (b *Bindings) internalMethod(def reflect.Method) bool { - // Get the receiver type - receiverType := def.Type.In(0) - - // Create a new instance of the receiver type - instance := reflect.New(receiverType.Elem()).Interface() - - // Check if the instance implements any of our service interfaces - // and if the method matches the interface method - switch def.Name { - case "ServiceName": - if _, ok := instance.(ServiceName); ok { - return true - } - case "ServiceStartup": - if _, ok := instance.(ServiceStartup); ok { - return true - } - case "ServiceShutdown": - if _, ok := instance.(ServiceShutdown); ok { - return true - } - } - - return false +func (b *BoundMethod) String() string { + return b.FQN } var errorType = reflect.TypeFor[error]() -// Call will attempt to call this bound method with the given args -func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnValue interface{}, err error) { +// Call will attempt to call this bound method with the given args. +// If the call succeeds, result will be either a non-error return value (if there is only one) +// or a slice of non-error return values (if there are more than one). +// +// If the arguments are mistyped or the call returns one or more non-nil error values, +// result is nil and err is an instance of *[CallError]. +func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (result any, err error) { // Use a defer statement to capture panics defer handlePanic(handlePanicOptions{skipEnd: 5}) argCount := len(args) @@ -287,7 +267,10 @@ func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnV } if argCount != len(b.Inputs) { - err = fmt.Errorf("%s expects %d arguments, received %d", b.Name, len(b.Inputs), argCount) + err = &CallError{ + Kind: TypeError, + Message: fmt.Sprintf("%s expects %d arguments, got %d", b.FQN, len(b.Inputs), argCount), + } return } @@ -305,7 +288,11 @@ func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnV value := reflect.New(b.Inputs[base+index].ReflectType) err = json.Unmarshal(arg, value.Interface()) if err != nil { - err = fmt.Errorf("could not parse argument #%d: %w", index, err) + err = &CallError{ + Kind: TypeError, + Message: fmt.Sprintf("could not parse argument #%d: %s", index, err), + Cause: json.RawMessage(b.marshalError(err)), + } return } callArgs[base+index] = value.Elem() @@ -322,32 +309,73 @@ func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnV var nonErrorOutputs = make([]any, 0, len(callResults)) var errorOutputs []error - for _, result := range callResults { - if result.Type() == errorType { - if result.IsNil() { + for _, field := range callResults { + if field.Type() == errorType { + if field.IsNil() { continue } if errorOutputs == nil { errorOutputs = make([]error, 0, len(callResults)-len(nonErrorOutputs)) nonErrorOutputs = nil } - errorOutputs = append(errorOutputs, result.Interface().(error)) + errorOutputs = append(errorOutputs, field.Interface().(error)) } else if nonErrorOutputs != nil { - nonErrorOutputs = append(nonErrorOutputs, result.Interface()) + nonErrorOutputs = append(nonErrorOutputs, field.Interface()) } } - if errorOutputs != nil { - err = errors.Join(errorOutputs...) + if len(errorOutputs) > 0 { + info := make([]json.RawMessage, len(errorOutputs)) + for i, err := range errorOutputs { + info[i] = b.marshalError(err) + } + + cerr := &CallError{ + Kind: RuntimeError, + Message: errors.Join(errorOutputs...).Error(), + Cause: info, + } + if len(info) == 1 { + cerr.Cause = info[0] + } + + err = cerr } else if len(nonErrorOutputs) == 1 { - returnValue = nonErrorOutputs[0] + result = nonErrorOutputs[0] } else if len(nonErrorOutputs) > 1 { - returnValue = nonErrorOutputs + result = nonErrorOutputs } return } +// wrapErrorMarshaler returns an error marshaling functions +// that calls the primary marshaler first, +// then falls back to the secondary one. +func wrapErrorMarshaler(primary func(error) []byte, secondary func(error) []byte) func(error) []byte { + if primary == nil { + return secondary + } + + return func(err error) []byte { + result := primary(err) + if result == nil { + result = secondary(err) + } + + return result + } +} + +// defaultMarshalError implements the default error marshaling mechanism. +func defaultMarshalError(err error) []byte { + result, jsonErr := json.Marshal(&err) + if jsonErr != nil { + return nil + } + return result +} + // isPtr returns true if the value given is a pointer. func isPtr(value interface{}) bool { return reflect.ValueOf(value).Kind() == reflect.Ptr diff --git a/v3/pkg/application/bindings_test.go b/v3/pkg/application/bindings_test.go index 2d80467f5..d76a8efe0 100644 --- a/v3/pkg/application/bindings_test.go +++ b/v3/pkg/application/bindings_test.go @@ -44,7 +44,7 @@ func (t *TestService) Variadic(s ...string) []string { return s } -func (t *TestService) PositionalAndVariadic(a int, b ...string) int { +func (t *TestService) PositionalAndVariadic(a int, _ ...string) int { return a } @@ -52,106 +52,103 @@ func (t *TestService) Slice(a []int) []int { return a } -func newArgs(jsonArgs ...string) []json.RawMessage { - args := []json.RawMessage{} - +func newArgs(jsonArgs ...string) (args []json.RawMessage) { for _, j := range jsonArgs { args = append(args, json.RawMessage(j)) } - return args + return } func TestBoundMethodCall(t *testing.T) { - tests := []struct { name string method string args []json.RawMessage - err error + err string expected interface{} }{ { name: "nil", method: "Nil", args: []json.RawMessage{}, - err: nil, + err: "", expected: nil, }, { name: "string", method: "String", args: newArgs(`"foo"`), - err: nil, + err: "", expected: "foo", }, { name: "multiple", method: "Multiple", args: newArgs(`"foo"`, "0", "false"), - err: nil, + err: "", expected: []interface{}{"foo", 0, false}, }, { name: "struct", method: "Struct", args: newArgs(`{ "name": "alice" }`), - err: nil, + err: "", expected: Person{Name: "alice"}, }, { name: "struct, nil error", method: "StructNil", args: newArgs(`{ "name": "alice" }`), - err: nil, + err: "", expected: Person{Name: "alice"}, }, { name: "struct, error", method: "StructError", args: newArgs(`{ "name": "alice" }`), - err: errors.New("error"), + err: "error", expected: nil, }, { name: "invalid argument count", method: "Multiple", args: newArgs(`"foo"`), - err: errors.New("expects 3 arguments, received 1"), + err: "expects 3 arguments, got 1", expected: nil, }, { name: "invalid argument type", method: "String", args: newArgs("1"), - err: errors.New("could not parse"), + err: "could not parse", expected: nil, }, { name: "variadic, no arguments", method: "Variadic", args: newArgs(`[]`), // variadic parameters are passed as arrays - err: nil, + err: "", expected: []string{}, }, { name: "variadic", method: "Variadic", args: newArgs(`["foo", "bar"]`), - err: nil, + err: "", expected: []string{"foo", "bar"}, }, { name: "positional and variadic", method: "PositionalAndVariadic", args: newArgs("42", `[]`), - err: nil, + err: "", expected: 42, }, { name: "slice", method: "Slice", args: newArgs(`[1,2,3]`), - err: nil, + err: "", expected: []int{1, 2, 3}, }, } @@ -159,13 +156,11 @@ func TestBoundMethodCall(t *testing.T) { // init globalApplication _ = application.New(application.Options{}) - bindings, err := application.NewBindings( - []application.Service{ - application.NewService(&TestService{}), - }, make(map[uint32]uint32), - ) + bindings := application.NewBindings(nil, nil) + + err := bindings.Add(application.NewService(&TestService{})) if err != nil { - t.Fatalf("application.NewBindings() error = %v\n", err) + t.Fatalf("bindings.Add() error = %v\n", err) } for _, tt := range tests { @@ -180,13 +175,16 @@ func TestBoundMethodCall(t *testing.T) { } result, err := method.Call(context.TODO(), tt.args) - if tt.err != err && (tt.err == nil || err == nil || !strings.Contains(err.Error(), tt.err.Error())) { - t.Fatalf("error: %v, expected error: %v", err, tt.err) + if (tt.err == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), tt.err)) { + expected := tt.err + if expected == "" { + expected = "nil" + } + t.Fatalf("error: %#v, expected error: %v", err, expected) } if !reflect.DeepEqual(result, tt.expected) { t.Fatalf("result: %v, expected result: %v", result, tt.expected) } - }) } diff --git a/v3/pkg/application/browser_manager.go b/v3/pkg/application/browser_manager.go new file mode 100644 index 000000000..3fe81720a --- /dev/null +++ b/v3/pkg/application/browser_manager.go @@ -0,0 +1,27 @@ +package application + +import ( + "github.com/pkg/browser" +) + +// BrowserManager manages browser-related operations +type BrowserManager struct { + app *App +} + +// newBrowserManager creates a new BrowserManager instance +func newBrowserManager(app *App) *BrowserManager { + return &BrowserManager{ + app: app, + } +} + +// OpenURL opens a URL in the default browser +func (bm *BrowserManager) OpenURL(url string) error { + return browser.OpenURL(url) +} + +// OpenFile opens a file in the default browser +func (bm *BrowserManager) OpenFile(path string) error { + return browser.OpenFile(path) +} diff --git a/v3/pkg/application/clipboard_manager.go b/v3/pkg/application/clipboard_manager.go new file mode 100644 index 000000000..abd0b19c1 --- /dev/null +++ b/v3/pkg/application/clipboard_manager.go @@ -0,0 +1,32 @@ +package application + +// ClipboardManager manages clipboard operations +type ClipboardManager struct { + app *App + clipboard *Clipboard +} + +// newClipboardManager creates a new ClipboardManager instance +func newClipboardManager(app *App) *ClipboardManager { + return &ClipboardManager{ + app: app, + } +} + +// SetText sets text in the clipboard +func (cm *ClipboardManager) SetText(text string) bool { + return cm.getClipboard().SetText(text) +} + +// Text gets text from the clipboard +func (cm *ClipboardManager) Text() (string, bool) { + return cm.getClipboard().Text() +} + +// getClipboard returns the clipboard instance, creating it if needed (lazy initialization) +func (cm *ClipboardManager) getClipboard() *Clipboard { + if cm.clipboard == nil { + cm.clipboard = newClipboard() + } + return cm.clipboard +} diff --git a/v3/pkg/application/context_application_event.go b/v3/pkg/application/context_application_event.go index 468634f22..32f392455 100644 --- a/v3/pkg/application/context_application_event.go +++ b/v3/pkg/application/context_application_event.go @@ -1,19 +1,24 @@ package application +import "log" + var blankApplicationEventContext = &ApplicationEventContext{} const ( - openedFiles = "openedFiles" - filename = "filename" + CONTEXT_OPENED_FILES = "openedFiles" + CONTEXT_FILENAME = "filename" + CONTEXT_URL = "url" ) +// ApplicationEventContext is the context of an application event type ApplicationEventContext struct { // contains filtered or unexported fields data map[string]any } +// OpenedFiles returns the opened files from the event context if it was set func (c ApplicationEventContext) OpenedFiles() []string { - files, ok := c.data[openedFiles] + files, ok := c.data[CONTEXT_OPENED_FILES] if !ok { return nil } @@ -25,7 +30,7 @@ func (c ApplicationEventContext) OpenedFiles() []string { } func (c ApplicationEventContext) setOpenedFiles(files []string) { - c.data[openedFiles] = files + c.data[CONTEXT_OPENED_FILES] = files } func (c ApplicationEventContext) setIsDarkMode(mode bool) { @@ -44,24 +49,31 @@ func (c ApplicationEventContext) getBool(key string) bool { return result } +// IsDarkMode returns true if the event context has a dark mode func (c ApplicationEventContext) IsDarkMode() bool { return c.getBool("isDarkMode") } +// HasVisibleWindows returns true if the event context has a visible window func (c ApplicationEventContext) HasVisibleWindows() bool { return c.getBool("hasVisibleWindows") } -func (c ApplicationEventContext) setData(data map[string]any) { +func (c *ApplicationEventContext) setData(data map[string]any) { c.data = data } -func (c ApplicationEventContext) setOpenedWithFile(filepath string) { - c.data[filename] = filepath +func (c *ApplicationEventContext) setOpenedWithFile(filepath string) { + c.data[CONTEXT_FILENAME] = filepath } +func (c *ApplicationEventContext) setURL(openedWithURL string) { + c.data[CONTEXT_URL] = openedWithURL +} + +// Filename returns the filename from the event context if it was set func (c ApplicationEventContext) Filename() string { - filename, ok := c.data[filename] + filename, ok := c.data[CONTEXT_FILENAME] if !ok { return "" } @@ -72,6 +84,21 @@ func (c ApplicationEventContext) Filename() string { return result } +// URL returns the URL from the event context if it was set +func (c ApplicationEventContext) URL() string { + url, ok := c.data[CONTEXT_URL] + if !ok { + log.Println("URL not found in event context") + return "" + } + result, ok := url.(string) + if !ok { + log.Println("URL not a string in event context") + return "" + } + return result +} + func newApplicationEventContext() *ApplicationEventContext { return &ApplicationEventContext{ data: make(map[string]any), diff --git a/v3/pkg/application/context_menu_manager.go b/v3/pkg/application/context_menu_manager.go new file mode 100644 index 000000000..669e8aca5 --- /dev/null +++ b/v3/pkg/application/context_menu_manager.go @@ -0,0 +1,54 @@ +package application + +// ContextMenuManager manages all context menu operations +type ContextMenuManager struct { + app *App +} + +// newContextMenuManager creates a new ContextMenuManager instance +func newContextMenuManager(app *App) *ContextMenuManager { + return &ContextMenuManager{ + app: app, + } +} + +// New creates a new context menu +func (cmm *ContextMenuManager) New() *ContextMenu { + return &ContextMenu{ + Menu: NewMenu(), + } +} + +// Add adds a context menu (replaces Register for consistency) +func (cmm *ContextMenuManager) Add(name string, menu *ContextMenu) { + cmm.app.contextMenusLock.Lock() + defer cmm.app.contextMenusLock.Unlock() + cmm.app.contextMenus[name] = menu +} + +// Remove removes a context menu by name (replaces Unregister for consistency) +func (cmm *ContextMenuManager) Remove(name string) { + cmm.app.contextMenusLock.Lock() + defer cmm.app.contextMenusLock.Unlock() + delete(cmm.app.contextMenus, name) +} + +// Get retrieves a context menu by name +func (cmm *ContextMenuManager) Get(name string) (*ContextMenu, bool) { + cmm.app.contextMenusLock.RLock() + defer cmm.app.contextMenusLock.RUnlock() + menu, exists := cmm.app.contextMenus[name] + return menu, exists +} + +// GetAll returns all registered context menus as a slice +func (cmm *ContextMenuManager) GetAll() []*ContextMenu { + cmm.app.contextMenusLock.RLock() + defer cmm.app.contextMenusLock.RUnlock() + + result := make([]*ContextMenu, 0, len(cmm.app.contextMenus)) + for _, menu := range cmm.app.contextMenus { + result = append(result, menu) + } + return result +} diff --git a/v3/pkg/application/dialog_manager.go b/v3/pkg/application/dialog_manager.go new file mode 100644 index 000000000..fa49392ae --- /dev/null +++ b/v3/pkg/application/dialog_manager.go @@ -0,0 +1,57 @@ +package application + +// DialogManager manages dialog-related operations +type DialogManager struct { + app *App +} + +// newDialogManager creates a new DialogManager instance +func newDialogManager(app *App) *DialogManager { + return &DialogManager{ + app: app, + } +} + +// OpenFile creates a file dialog for selecting files +func (dm *DialogManager) OpenFile() *OpenFileDialogStruct { + return OpenFileDialog() +} + +// OpenFileWithOptions creates a file dialog with options +func (dm *DialogManager) OpenFileWithOptions(options *OpenFileDialogOptions) *OpenFileDialogStruct { + result := OpenFileDialog() + result.SetOptions(options) + return result +} + +// SaveFile creates a save file dialog +func (dm *DialogManager) SaveFile() *SaveFileDialogStruct { + return SaveFileDialog() +} + +// SaveFileWithOptions creates a save file dialog with options +func (dm *DialogManager) SaveFileWithOptions(options *SaveFileDialogOptions) *SaveFileDialogStruct { + result := SaveFileDialog() + result.SetOptions(options) + return result +} + +// Info creates an information dialog +func (dm *DialogManager) Info() *MessageDialog { + return InfoDialog() +} + +// Question creates a question dialog +func (dm *DialogManager) Question() *MessageDialog { + return QuestionDialog() +} + +// Warning creates a warning dialog +func (dm *DialogManager) Warning() *MessageDialog { + return WarningDialog() +} + +// Error creates an error dialog +func (dm *DialogManager) Error() *MessageDialog { + return ErrorDialog() +} diff --git a/v3/pkg/application/dialogs.go b/v3/pkg/application/dialogs.go index 1e52225ce..2c832d5f2 100644 --- a/v3/pkg/application/dialogs.go +++ b/v3/pkg/application/dialogs.go @@ -291,6 +291,9 @@ func (d *OpenFileDialogStruct) PromptForMultipleSelection() ([]string, error) { } selections, err := InvokeSyncWithResultAndError(d.impl.show) + if err != nil { + return nil, err + } var result []string for filename := range selections { diff --git a/v3/pkg/application/dialogs_linux.go b/v3/pkg/application/dialogs_linux.go index 239ae9d76..5573e739c 100644 --- a/v3/pkg/application/dialogs_linux.go +++ b/v3/pkg/application/dialogs_linux.go @@ -1,7 +1,7 @@ package application func (a *linuxApp) showAboutDialog(title string, message string, icon []byte) { - window := globalApplication.getWindowForID(a.getCurrentWindowID()) + window, _ := globalApplication.Window.GetByID(a.getCurrentWindowID()) var parent uintptr if window != nil { parent, _ = window.(*WebviewWindow).NativeWindowHandle() @@ -24,7 +24,7 @@ type linuxDialog struct { func (m *linuxDialog) show() { windowId := getNativeApplication().getCurrentWindowID() - window := globalApplication.getWindowForID(windowId) + window, _ := globalApplication.Window.GetByID(windowId) var parent uintptr if window != nil { parent, _ = window.(*WebviewWindow).NativeWindowHandle() diff --git a/v3/pkg/application/dialogs_windows.go b/v3/pkg/application/dialogs_windows.go index d944cea5a..ef5b625fc 100644 --- a/v3/pkg/application/dialogs_windows.go +++ b/v3/pkg/application/dialogs_windows.go @@ -42,7 +42,7 @@ func (m *windowsDialog) show() { if m.dialog.window != nil { parentWindow, err = m.dialog.window.NativeWindowHandle() if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } } @@ -50,12 +50,12 @@ func (m *windowsDialog) show() { // 3 is the application icon button, err = w32.MessageBoxWithIcon(parentWindow, message, title, 3, windows.MB_OK|windows.MB_USERICON) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } } else { button, err = windows.MessageBox(windows.HWND(parentWindow), message, title, flags|windows.MB_SYSTEMMODAL) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } } // This maps MessageBox return values to strings @@ -114,7 +114,7 @@ func (m *windowOpenFileDialog) show() (chan string, error) { if m.dialog.window != nil { config.ParentWindowHandle, err = m.dialog.window.NativeWindowHandle() if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } } @@ -242,12 +242,25 @@ func showCfdDialog(newDlg func() (cfd.Dialog, error), isMultiSelect bool) (any, defer func() { err := dlg.Release() if err != nil { - globalApplication.error("Unable to release dialog: " + err.Error()) + globalApplication.error("unable to release dialog: %w", err) } }() if multi, _ := dlg.(cfd.OpenMultipleFilesDialog); multi != nil && isMultiSelect { - return multi.ShowAndGetResults() + paths, err := multi.ShowAndGetResults() + if err != nil { + return nil, err + } + + for i, path := range paths { + paths[i] = filepath.Clean(path) + } + return paths, nil } - return dlg.ShowAndGetResult() + + path, err := dlg.ShowAndGetResult() + if err != nil { + return nil, err + } + return filepath.Clean(path), nil } diff --git a/v3/pkg/application/dialogs_windows_test.go b/v3/pkg/application/dialogs_windows_test.go new file mode 100644 index 000000000..af4dabf75 --- /dev/null +++ b/v3/pkg/application/dialogs_windows_test.go @@ -0,0 +1,57 @@ +//go:build windows + +package application_test + +import ( + "path/filepath" + "testing" + + "github.com/matryer/is" +) + +func TestCleanPath(t *testing.T) { + i := is.New(t) + tests := []struct { + name string + inputPath string + expected string + }{ + { + name: "path with double separators", + inputPath: `C:\\temp\\folder`, + expected: `C:\temp\folder`, + }, + { + name: "path with forward slashes", + inputPath: `C://temp//folder`, + expected: `C:\temp\folder`, + }, + { + name: "path with trailing separator", + inputPath: `C:\\temp\\folder\\`, + expected: `C:\temp\folder`, + }, + { + name: "path with escaped tab character", + inputPath: `C:\\Users\\test\\tab.txt`, + expected: `C:\Users\test\tab.txt`, + }, + { + name: "newline character", + inputPath: `C:\\Users\\test\\newline\\n.txt`, + expected: `C:\Users\test\newline\n.txt`, + }, + { + name: "UNC path with multiple separators", + inputPath: `\\\\\\\\host\\share\\test.txt`, + expected: `\\\\host\share\test.txt`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cleaned := filepath.Clean(tt.inputPath) + i.Equal(cleaned, tt.expected) + }) + } +} diff --git a/v3/pkg/application/environment_manager.go b/v3/pkg/application/environment_manager.go new file mode 100644 index 000000000..d32b6dd89 --- /dev/null +++ b/v3/pkg/application/environment_manager.go @@ -0,0 +1,56 @@ +package application + +import ( + "runtime" + + "github.com/wailsapp/wails/v3/internal/fileexplorer" + "github.com/wailsapp/wails/v3/internal/operatingsystem" +) + +// EnvironmentManager manages environment-related operations +type EnvironmentManager struct { + app *App +} + +// newEnvironmentManager creates a new EnvironmentManager instance +func newEnvironmentManager(app *App) *EnvironmentManager { + return &EnvironmentManager{ + app: app, + } +} + +// Info returns environment information +func (em *EnvironmentManager) Info() EnvironmentInfo { + info, _ := operatingsystem.Info() + result := EnvironmentInfo{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + Debug: em.app.isDebugMode, + OSInfo: info, + } + result.PlatformInfo = em.app.platformEnvironment() + return result +} + +// IsDarkMode returns true if the system is in dark mode +func (em *EnvironmentManager) IsDarkMode() bool { + if em.app.impl == nil { + return false + } + return em.app.impl.isDarkMode() +} + +// GetAccentColor returns the system accent color +func (em *EnvironmentManager) GetAccentColor() string { + if em.app.impl == nil { + return "rgb(0,122,255)" + } + return em.app.impl.getAccentColor() +} + +// OpenFileManager opens the file manager at the specified path, optionally selecting the file +func (em *EnvironmentManager) OpenFileManager(path string, selectFile bool) error { + return InvokeSyncWithError(func() error { + return fileexplorer.OpenFileManager(path, selectFile) + }) +} diff --git a/v3/pkg/application/errors.go b/v3/pkg/application/errors.go index 747142328..d0b7edd6b 100644 --- a/v3/pkg/application/errors.go +++ b/v3/pkg/application/errors.go @@ -3,14 +3,53 @@ package application import ( "fmt" "os" + "strings" ) -func Fatal(message string, args ...interface{}) { - println("*********************** FATAL ***********************") - println("There has been a catastrophic failure in your application.") - println("Please report this error at https://github.com/wailsapp/wails/issues") - println("******************** Error Details ******************") - println(fmt.Sprintf(message, args...)) - println("*********************** FATAL ***********************") +// FatalError instances are passed to the registered error handler +// in case of catastrophic, unrecoverable failures that require immediate termination. +// FatalError wraps the original error value in an informative message. +// The underlying error may be retrieved through the [FatalError.Unwrap] method. +type FatalError struct { + err error + internal bool +} + +// Internal returns true when the error was triggered from wails' internal code. +func (e *FatalError) Internal() bool { + return e.internal +} + +// Unwrap returns the original cause of the fatal error, +// for easy inspection using the [errors.As] API. +func (e *FatalError) Unwrap() error { + return e.err +} + +func (e *FatalError) Error() string { + var buffer strings.Builder + buffer.WriteString("\n\n******************************** FATAL *********************************\n") + buffer.WriteString("* There has been a catastrophic failure in your application. *\n") + if e.internal { + buffer.WriteString("* Please report this error at https://github.com/wailsapp/wails/issues *\n") + } + buffer.WriteString("**************************** Error Details *****************************\n") + buffer.WriteString(e.err.Error()) + buffer.WriteString("************************************************************************\n") + return buffer.String() +} + +func Fatal(message string, args ...any) { + err := &FatalError{ + err: fmt.Errorf(message, args...), + internal: true, + } + + if globalApplication != nil { + globalApplication.handleError(err) + } else { + fmt.Println(err) + } + os.Exit(1) } diff --git a/v3/pkg/application/event_manager.go b/v3/pkg/application/event_manager.go new file mode 100644 index 000000000..1ee191e10 --- /dev/null +++ b/v3/pkg/application/event_manager.go @@ -0,0 +1,155 @@ +package application + +import ( + "slices" + + "github.com/samber/lo" + "github.com/wailsapp/wails/v3/pkg/events" +) + +// EventManager manages event-related operations +type EventManager struct { + app *App +} + +// newEventManager creates a new EventManager instance +func newEventManager(app *App) *EventManager { + return &EventManager{ + app: app, + } +} + +// Emit emits a custom event +func (em *EventManager) Emit(name string, data ...any) { + em.app.customEventProcessor.Emit(&CustomEvent{ + Name: name, + Data: data, + }) +} + +// EmitEvent emits a custom event object (internal use) +func (em *EventManager) EmitEvent(event *CustomEvent) { + em.app.customEventProcessor.Emit(event) +} + +// On registers a listener for custom events +func (em *EventManager) On(name string, callback func(event *CustomEvent)) func() { + return em.app.customEventProcessor.On(name, callback) +} + +// Off removes all listeners for a custom event +func (em *EventManager) Off(name string) { + em.app.customEventProcessor.Off(name) +} + +// OnMultiple registers a listener for custom events that will be called N times +func (em *EventManager) OnMultiple(name string, callback func(event *CustomEvent), counter int) { + em.app.customEventProcessor.OnMultiple(name, callback, counter) +} + +// Reset removes all custom event listeners +func (em *EventManager) Reset() { + em.app.customEventProcessor.OffAll() +} + +// OnApplicationEvent registers a listener for application events +func (em *EventManager) OnApplicationEvent(eventType events.ApplicationEventType, callback func(event *ApplicationEvent)) func() { + eventID := uint(eventType) + em.app.applicationEventListenersLock.Lock() + defer em.app.applicationEventListenersLock.Unlock() + listener := &EventListener{ + callback: callback, + } + em.app.applicationEventListeners[eventID] = append(em.app.applicationEventListeners[eventID], listener) + if em.app.impl != nil { + go func() { + defer handlePanic() + em.app.impl.on(eventID) + }() + } + + return func() { + // lock the map + em.app.applicationEventListenersLock.Lock() + defer em.app.applicationEventListenersLock.Unlock() + // Remove listener + em.app.applicationEventListeners[eventID] = lo.Without(em.app.applicationEventListeners[eventID], listener) + } +} + +// RegisterApplicationEventHook registers an application event hook +func (em *EventManager) RegisterApplicationEventHook(eventType events.ApplicationEventType, callback func(event *ApplicationEvent)) func() { + eventID := uint(eventType) + em.app.applicationEventHooksLock.Lock() + defer em.app.applicationEventHooksLock.Unlock() + thisHook := &eventHook{ + callback: callback, + } + em.app.applicationEventHooks[eventID] = append(em.app.applicationEventHooks[eventID], thisHook) + + return func() { + em.app.applicationEventHooksLock.Lock() + em.app.applicationEventHooks[eventID] = lo.Without(em.app.applicationEventHooks[eventID], thisHook) + em.app.applicationEventHooksLock.Unlock() + } +} + +// Dispatch dispatches an event to listeners (internal use) +func (em *EventManager) dispatch(event *CustomEvent) { + // Snapshot windows under RLock + em.app.windowsLock.RLock() + for _, window := range em.app.windows { + if event.IsCancelled() { + em.app.windowsLock.RUnlock() + return + } + window.DispatchWailsEvent(event) + } + em.app.windowsLock.RUnlock() + + // Snapshot listeners under Lock + em.app.wailsEventListenerLock.Lock() + listeners := slices.Clone(em.app.wailsEventListeners) + em.app.wailsEventListenerLock.Unlock() + + for _, listener := range listeners { + if event.IsCancelled() { + return + } + listener.DispatchWailsEvent(event) + } +} + +// HandleApplicationEvent handles application events (internal use) +func (em *EventManager) handleApplicationEvent(event *ApplicationEvent) { + defer handlePanic() + em.app.applicationEventListenersLock.RLock() + listeners, ok := em.app.applicationEventListeners[event.Id] + em.app.applicationEventListenersLock.RUnlock() + if !ok { + return + } + + // Process Hooks + em.app.applicationEventHooksLock.RLock() + hooks, ok := em.app.applicationEventHooks[event.Id] + em.app.applicationEventHooksLock.RUnlock() + if ok { + for _, thisHook := range hooks { + thisHook.callback(event) + if event.IsCancelled() { + return + } + } + } + + for _, listener := range listeners { + go func() { + if event.IsCancelled() { + return + } + defer handlePanic() + listener.callback(event) + }() + } +} diff --git a/v3/pkg/application/events_common_darwin.go b/v3/pkg/application/events_common_darwin.go index e8c988dae..6fce7fcd4 100644 --- a/v3/pkg/application/events_common_darwin.go +++ b/v3/pkg/application/events_common_darwin.go @@ -13,7 +13,7 @@ func (m *macosApp) setupCommonEvents() { for sourceEvent, targetEvent := range commonApplicationEventMap { sourceEvent := sourceEvent targetEvent := targetEvent - m.parent.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) { + m.parent.Event.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) { event.Id = uint(targetEvent) applicationEvents <- event }) diff --git a/v3/pkg/application/events_common_linux.go b/v3/pkg/application/events_common_linux.go index 812befea8..d16232648 100644 --- a/v3/pkg/application/events_common_linux.go +++ b/v3/pkg/application/events_common_linux.go @@ -13,7 +13,7 @@ func (a *linuxApp) setupCommonEvents() { for sourceEvent, targetEvent := range commonApplicationEventMap { sourceEvent := sourceEvent targetEvent := targetEvent - a.parent.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) { + a.parent.Event.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) { event.Id = uint(targetEvent) applicationEvents <- event }) diff --git a/v3/pkg/application/events_common_windows.go b/v3/pkg/application/events_common_windows.go index 35b4256ba..1c4dd55e6 100644 --- a/v3/pkg/application/events_common_windows.go +++ b/v3/pkg/application/events_common_windows.go @@ -13,7 +13,7 @@ func (m *windowsApp) setupCommonEvents() { for sourceEvent, targetEvent := range commonApplicationEventMap { sourceEvent := sourceEvent targetEvent := targetEvent - m.parent.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) { + m.parent.Event.OnApplicationEvent(sourceEvent, func(event *ApplicationEvent) { event.Id = uint(targetEvent) applicationEvents <- event }) diff --git a/v3/pkg/application/internal/tests/services/common.go b/v3/pkg/application/internal/tests/services/common.go new file mode 100644 index 000000000..9d35da69e --- /dev/null +++ b/v3/pkg/application/internal/tests/services/common.go @@ -0,0 +1,168 @@ +package services + +import ( + "context" + "fmt" + "sync/atomic" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/wailsapp/wails/v3/pkg/application" +) + +type Config struct { + Id int + T *testing.T + Seq *atomic.Int64 + Options application.ServiceOptions + StartupErr bool + ShutdownErr bool +} + +func Configure[T any, P interface { + *T + Configure(Config) +}](srv P, c Config) application.Service { + srv.Configure(c) + return application.NewServiceWithOptions(srv, c.Options) +} + +type Error struct { + Id int +} + +func (e *Error) Error() string { + return fmt.Sprintf("service #%d mock failure", e.Id) +} + +type Startupper struct { + Config + startup int64 +} + +func (s *Startupper) Configure(c Config) { + s.Config = c +} + +func (s *Startupper) Id() int { + return s.Config.Id +} + +func (s *Startupper) StartupSeq() int64 { + return s.startup +} + +func (s *Startupper) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + if s.startup != 0 { + s.T.Errorf("Double startup for service #%d: first at seq=%d, then at seq=%d", s.Id(), s.startup, s.Seq.Load()) + return nil + } + + s.startup = s.Seq.Add(1) + + if diff := cmp.Diff(s.Options, options); diff != "" { + s.T.Errorf("Options mismatch for service #%d (-want +got):\n%s", s.Id(), diff) + } + + if s.StartupErr { + return &Error{Id: s.Id()} + } else { + return nil + } +} + +type Shutdowner struct { + Config + shutdown int64 +} + +func (s *Shutdowner) Configure(c Config) { + s.Config = c +} + +func (s *Shutdowner) Id() int { + return s.Config.Id +} + +func (s *Shutdowner) ShutdownSeq() int64 { + return s.shutdown +} + +func (s *Shutdowner) ServiceShutdown() error { + if s.shutdown != 0 { + s.T.Errorf("Double shutdown for service #%d: first at seq=%d, then at seq=%d", s.Id(), s.shutdown, s.Seq.Load()) + return nil + } + + s.shutdown = s.Seq.Add(1) + + if s.ShutdownErr { + return &Error{Id: s.Id()} + } else { + return nil + } +} + +type StartupShutdowner struct { + Config + startup int64 + shutdown int64 + ctx context.Context +} + +func (s *StartupShutdowner) Configure(c Config) { + s.Config = c +} + +func (s *StartupShutdowner) Id() int { + return s.Config.Id +} + +func (s *StartupShutdowner) StartupSeq() int64 { + return s.startup +} + +func (s *StartupShutdowner) ShutdownSeq() int64 { + return s.shutdown +} + +func (s *StartupShutdowner) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + if s.startup != 0 { + s.T.Errorf("Double startup for service #%d: first at seq=%d, then at seq=%d", s.Id(), s.startup, s.Seq.Load()) + return nil + } + + s.startup = s.Seq.Add(1) + s.ctx = ctx + + if diff := cmp.Diff(s.Options, options); diff != "" { + s.T.Errorf("Options mismatch for service #%d (-want +got):\n%s", s.Id(), diff) + } + + if s.StartupErr { + return &Error{Id: s.Id()} + } else { + return nil + } +} + +func (s *StartupShutdowner) ServiceShutdown() error { + if s.shutdown != 0 { + s.T.Errorf("Double shutdown for service #%d: first at seq=%d, then at seq=%d", s.Id(), s.shutdown, s.Seq.Load()) + return nil + } + + s.shutdown = s.Seq.Add(1) + + select { + case <-s.ctx.Done(): + default: + s.T.Errorf("Service #%d shut down before context cancellation", s.Id()) + } + + if s.ShutdownErr { + return &Error{Id: s.Id()} + } else { + return nil + } +} diff --git a/v3/pkg/application/internal/tests/services/shutdown/shutdown_test.go b/v3/pkg/application/internal/tests/services/shutdown/shutdown_test.go new file mode 100644 index 000000000..ad8df141c --- /dev/null +++ b/v3/pkg/application/internal/tests/services/shutdown/shutdown_test.go @@ -0,0 +1,92 @@ +package shutdown + +import ( + "sync" + "sync/atomic" + "testing" + + "github.com/wailsapp/wails/v3/pkg/application" + apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests" + svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services" + "github.com/wailsapp/wails/v3/pkg/events" +) + +func TestMain(m *testing.M) { + apptest.Main(m) +} + +type ( + Service1 struct{ svctest.Shutdowner } + Service2 struct{ svctest.Shutdowner } + Service3 struct{ svctest.Shutdowner } + Service4 struct{ svctest.Shutdowner } + Service5 struct{ svctest.Shutdowner } + Service6 struct{ svctest.Shutdowner } +) + +func TestServiceShutdown(t *testing.T) { + var seq atomic.Int64 + + services := []application.Service{ + svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}), + svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq}), + svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq}), + svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq}), + svctest.Configure(&Service5{}, svctest.Config{Id: 4, T: t, Seq: &seq}), + svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq}), + } + + app := apptest.New(t, application.Options{ + Services: services[:3], + }) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + app.RegisterService(services[3]) + wg.Done() + }() + go func() { + app.RegisterService(services[4]) + wg.Done() + }() + wg.Wait() + + app.RegisterService(services[5]) + + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) { + app.Quit() + }) + + err := apptest.Run(t, app) + if err != nil { + t.Fatal(err) + } + + if count := seq.Load(); count != int64(len(services)) { + t.Errorf("Wrong shutdown call count: wanted %d, got %d", len(services), count) + } + + validate(t, services[0], 5) + validate(t, services[1], 4) + validate(t, services[2], 2, 3) + validate(t, services[3], 1) + validate(t, services[4], 1) + validate(t, services[5], 0) +} + +func validate(t *testing.T, svc application.Service, prev ...int64) { + id := svc.Instance().(interface{ Id() int }).Id() + seq := svc.Instance().(interface{ ShutdownSeq() int64 }).ShutdownSeq() + + if seq == 0 { + t.Errorf("Service #%d did not shut down", id) + return + } + + for _, p := range prev { + if seq <= p { + t.Errorf("Wrong shutdown sequence number for service #%d: wanted >%d, got %d", id, p, seq) + } + } +} diff --git a/v3/pkg/application/internal/tests/services/shutdownerror/shutdownerror_test.go b/v3/pkg/application/internal/tests/services/shutdownerror/shutdownerror_test.go new file mode 100644 index 000000000..42949b037 --- /dev/null +++ b/v3/pkg/application/internal/tests/services/shutdownerror/shutdownerror_test.go @@ -0,0 +1,123 @@ +package shutdownerror + +import ( + "errors" + "slices" + "sync" + "sync/atomic" + "testing" + + "github.com/wailsapp/wails/v3/pkg/application" + apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests" + svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services" + "github.com/wailsapp/wails/v3/pkg/events" +) + +func TestMain(m *testing.M) { + apptest.Main(m) +} + +type ( + Service1 struct{ svctest.Shutdowner } + Service2 struct{ svctest.Shutdowner } + Service3 struct{ svctest.Shutdowner } + Service4 struct{ svctest.Shutdowner } + Service5 struct{ svctest.Shutdowner } + Service6 struct{ svctest.Shutdowner } +) + +func TestServiceShutdownError(t *testing.T) { + var seq atomic.Int64 + + services := []application.Service{ + svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}), + svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq, ShutdownErr: true}), + svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq}), + svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq}), + svctest.Configure(&Service5{}, svctest.Config{Id: 4, T: t, Seq: &seq, ShutdownErr: true}), + svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq, ShutdownErr: true}), + } + + expectedShutdownErrors := []int{5, 4, 1} + var errCount atomic.Int64 + + var app *application.App + app = apptest.New(t, application.Options{ + Services: services[:3], + ErrorHandler: func(err error) { + var mock *svctest.Error + if !errors.As(err, &mock) { + app.Logger.Error(err.Error()) + return + } + + i := int(errCount.Add(1) - 1) + if i < len(expectedShutdownErrors) && mock.Id == expectedShutdownErrors[i] { + return + } + + cut := min(i, len(expectedShutdownErrors)) + if slices.Contains(expectedShutdownErrors[:cut], mock.Id) { + t.Errorf("Late or duplicate shutdown error for service #%d", mock.Id) + } else if slices.Contains(expectedShutdownErrors[cut:], mock.Id) { + t.Errorf("Early shutdown error for service #%d", mock.Id) + } else { + t.Errorf("Unexpected shutdown error for service #%d", mock.Id) + } + }, + }) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + app.RegisterService(services[3]) + wg.Done() + }() + go func() { + app.RegisterService(services[4]) + wg.Done() + }() + wg.Wait() + + app.RegisterService(services[5]) + + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) { + app.Quit() + }) + + err := apptest.Run(t, app) + if err != nil { + t.Fatal(err) + } + + if ec := errCount.Load(); ec != int64(len(expectedShutdownErrors)) { + t.Errorf("Wrong shutdown error count: wanted %d, got %d", len(expectedShutdownErrors), ec) + } + + if count := seq.Load(); count != int64(len(services)) { + t.Errorf("Wrong shutdown call count: wanted %d, got %d", len(services), count) + } + + validate(t, services[0], 5) + validate(t, services[1], 4) + validate(t, services[2], 2, 3) + validate(t, services[3], 1) + validate(t, services[4], 1) + validate(t, services[5], 0) +} + +func validate(t *testing.T, svc application.Service, prev ...int64) { + id := svc.Instance().(interface{ Id() int }).Id() + seq := svc.Instance().(interface{ ShutdownSeq() int64 }).ShutdownSeq() + + if seq == 0 { + t.Errorf("Service #%d did not shut down", id) + return + } + + for _, p := range prev { + if seq <= p { + t.Errorf("Wrong shutdown sequence number for service #%d: wanted >%d, got %d", id, p, seq) + } + } +} diff --git a/v3/pkg/application/internal/tests/services/startup/startup_test.go b/v3/pkg/application/internal/tests/services/startup/startup_test.go new file mode 100644 index 000000000..bf5c2ac96 --- /dev/null +++ b/v3/pkg/application/internal/tests/services/startup/startup_test.go @@ -0,0 +1,102 @@ +package startup + +import ( + "sync" + "sync/atomic" + "testing" + + "github.com/wailsapp/wails/v3/pkg/application" + apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests" + svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services" + "github.com/wailsapp/wails/v3/pkg/events" +) + +func TestMain(m *testing.M) { + apptest.Main(m) +} + +type ( + Service1 struct{ svctest.Startupper } + Service2 struct{ svctest.Startupper } + Service3 struct{ svctest.Startupper } + Service4 struct{ svctest.Startupper } + Service5 struct{ svctest.Startupper } + Service6 struct{ svctest.Startupper } +) + +func TestServiceStartup(t *testing.T) { + var seq atomic.Int64 + + services := []application.Service{ + svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}), + svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq, Options: application.ServiceOptions{ + Name: "I am service 2", + }}), + svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq, Options: application.ServiceOptions{ + Route: "/mounted/here", + }}), + svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq}), + svctest.Configure(&Service5{}, svctest.Config{Id: 4, Seq: &seq, Options: application.ServiceOptions{ + Name: "I am service 5", + Route: "/mounted/there", + }}), + svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq, Options: application.ServiceOptions{ + Name: "I am service 6", + Route: "/mounted/elsewhere", + }}), + } + + app := apptest.New(t, application.Options{ + Services: services[:3], + }) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + app.RegisterService(services[3]) + wg.Done() + }() + go func() { + app.RegisterService(services[4]) + wg.Done() + }() + wg.Wait() + + app.RegisterService(services[5]) + + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) { + app.Quit() + }) + + err := apptest.Run(t, app) + if err != nil { + t.Fatal(err) + } + + if count := seq.Load(); count != int64(len(services)) { + t.Errorf("Wrong startup call count: wanted %d, got %d", len(services), count) + } + + validate(t, services[0], 0) + validate(t, services[1], 1) + validate(t, services[2], 2) + validate(t, services[3], 3) + validate(t, services[4], 3) + validate(t, services[5], 4, 5) +} + +func validate(t *testing.T, svc application.Service, prev ...int64) { + id := svc.Instance().(interface{ Id() int }).Id() + seq := svc.Instance().(interface{ StartupSeq() int64 }).StartupSeq() + + if seq == 0 { + t.Errorf("Service #%d did not start up", id) + return + } + + for _, p := range prev { + if seq <= p { + t.Errorf("Wrong startup sequence number for service #%d: wanted >%d, got %d", id, p, seq) + } + } +} diff --git a/v3/pkg/application/internal/tests/services/startuperror/startuperror_test.go b/v3/pkg/application/internal/tests/services/startuperror/startuperror_test.go new file mode 100644 index 000000000..a4a3c5bc4 --- /dev/null +++ b/v3/pkg/application/internal/tests/services/startuperror/startuperror_test.go @@ -0,0 +1,114 @@ +package startuperror + +import ( + "errors" + "sync" + "sync/atomic" + "testing" + + "github.com/wailsapp/wails/v3/pkg/application" + apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests" + svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services" + "github.com/wailsapp/wails/v3/pkg/events" +) + +func TestMain(m *testing.M) { + apptest.Main(m) +} + +type ( + Service1 struct{ svctest.Startupper } + Service2 struct{ svctest.Startupper } + Service3 struct{ svctest.Startupper } + Service4 struct{ svctest.Startupper } + Service5 struct{ svctest.Startupper } + Service6 struct{ svctest.Startupper } +) + +func TestServiceStartupError(t *testing.T) { + var seq atomic.Int64 + + services := []application.Service{ + svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}), + svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq}), + svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq}), + svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq, StartupErr: true}), + svctest.Configure(&Service5{}, svctest.Config{Id: 4, T: t, Seq: &seq, StartupErr: true}), + svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq, StartupErr: true}), + } + + app := apptest.New(t, application.Options{ + Services: services[:3], + }) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + app.RegisterService(services[3]) + wg.Done() + }() + go func() { + app.RegisterService(services[4]) + wg.Done() + }() + wg.Wait() + + app.RegisterService(services[5]) + + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) { + t.Errorf("Application started") + app.Quit() + }) + + var mock *svctest.Error + + err := apptest.Run(t, app) + if err != nil { + if !errors.As(err, &mock) { + t.Fatal(err) + } + } + + if mock == nil { + t.Fatal("Wanted startup error for service #3 or #4, got none") + } else if mock.Id != 3 && mock.Id != 4 { + t.Errorf("Wanted startup error for service #3 or #4, got #%d", mock.Id) + } + + if count := seq.Load(); count != 4 { + t.Errorf("Wrong startup call count: wanted %d, got %d", 4, count) + } + + validate(t, services[0], 0) + validate(t, services[1], 1) + validate(t, services[2], 2) + validate(t, services[mock.Id], 3) + + notStarted := 3 + if mock.Id == 3 { + notStarted = 4 + } + + if seq := services[notStarted].Instance().(interface{ StartupSeq() int64 }).StartupSeq(); seq != 0 { + t.Errorf("Service #%d started up unexpectedly at seq=%d", notStarted, seq) + } + if seq := services[5].Instance().(interface{ StartupSeq() int64 }).StartupSeq(); seq != 0 { + t.Errorf("Service #5 started up unexpectedly at seq=%d", seq) + } +} + +func validate(t *testing.T, svc application.Service, prev ...int64) { + id := svc.Instance().(interface{ Id() int }).Id() + seq := svc.Instance().(interface{ StartupSeq() int64 }).StartupSeq() + + if seq == 0 { + t.Errorf("Service #%d did not start up", id) + return + } + + for _, p := range prev { + if seq <= p { + t.Errorf("Wrong startup sequence number for service #%d: wanted >%d, got %d", id, p, seq) + } + } +} diff --git a/v3/pkg/application/internal/tests/services/startupshutdown/startupshutdown_test.go b/v3/pkg/application/internal/tests/services/startupshutdown/startupshutdown_test.go new file mode 100644 index 000000000..5f8cfc365 --- /dev/null +++ b/v3/pkg/application/internal/tests/services/startupshutdown/startupshutdown_test.go @@ -0,0 +1,102 @@ +package startupshutdown + +import ( + "sync" + "sync/atomic" + "testing" + + "github.com/wailsapp/wails/v3/pkg/application" + apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests" + svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services" + "github.com/wailsapp/wails/v3/pkg/events" +) + +func TestMain(m *testing.M) { + apptest.Main(m) +} + +type ( + Service1 struct{ svctest.StartupShutdowner } + Service2 struct{ svctest.StartupShutdowner } + Service3 struct{ svctest.StartupShutdowner } + Service4 struct{ svctest.StartupShutdowner } + Service5 struct{ svctest.StartupShutdowner } + Service6 struct{ svctest.StartupShutdowner } +) + +func TestServiceStartupShutdown(t *testing.T) { + var seq atomic.Int64 + + services := []application.Service{ + svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}), + svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq}), + svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq}), + svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq}), + svctest.Configure(&Service5{}, svctest.Config{Id: 4, T: t, Seq: &seq}), + svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq}), + } + + app := apptest.New(t, application.Options{ + Services: services[:3], + }) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + app.RegisterService(services[3]) + wg.Done() + }() + go func() { + app.RegisterService(services[4]) + wg.Done() + }() + wg.Wait() + + app.RegisterService(services[5]) + + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) { + if count := seq.Load(); count != int64(len(services)) { + t.Errorf("Wrong startup call count: wanted %d, got %d", len(services), count) + } + seq.Store(0) + app.Quit() + }) + + err := apptest.Run(t, app) + if err != nil { + t.Fatal(err) + } + + if count := seq.Load(); count != int64(len(services)) { + t.Errorf("Wrong shutdown call count: wanted %d, got %d", len(services), count) + } + + bound := int64(len(services)) + 1 + validate(t, services[0], bound) + validate(t, services[1], bound) + validate(t, services[2], bound) + validate(t, services[3], bound) + validate(t, services[4], bound) + validate(t, services[5], bound) +} + +func validate(t *testing.T, svc application.Service, bound int64) { + id := svc.Instance().(interface{ Id() int }).Id() + startup := svc.Instance().(interface{ StartupSeq() int64 }).StartupSeq() + shutdown := svc.Instance().(interface{ ShutdownSeq() int64 }).ShutdownSeq() + + if startup == 0 && shutdown == 0 { + t.Errorf("Service #%d did not start nor shut down", id) + return + } else if startup == 0 { + t.Errorf("Service #%d started, but did not shut down", id) + return + } else if shutdown == 0 { + t.Errorf("Service #%d shut down, but did not start", id) + return + } + + if shutdown != bound-startup { + t.Errorf("Wrong sequence numbers for service #%d: wanted either %d..%d or %d..%d, got %d..%d", id, startup, bound-startup, bound-shutdown, shutdown, startup, shutdown) + } +} diff --git a/v3/pkg/application/internal/tests/services/startupshutdownerror/startupshutdownerror_test.go b/v3/pkg/application/internal/tests/services/startupshutdownerror/startupshutdownerror_test.go new file mode 100644 index 000000000..0ca135269 --- /dev/null +++ b/v3/pkg/application/internal/tests/services/startupshutdownerror/startupshutdownerror_test.go @@ -0,0 +1,140 @@ +package startupshutdownerror + +import ( + "errors" + "slices" + "sync" + "sync/atomic" + "testing" + + "github.com/wailsapp/wails/v3/pkg/application" + apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests" + svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services" + "github.com/wailsapp/wails/v3/pkg/events" +) + +func TestMain(m *testing.M) { + apptest.Main(m) +} + +type ( + Service1 struct{ svctest.StartupShutdowner } + Service2 struct{ svctest.StartupShutdowner } + Service3 struct{ svctest.StartupShutdowner } + Service4 struct{ svctest.StartupShutdowner } + Service5 struct{ svctest.StartupShutdowner } + Service6 struct{ svctest.StartupShutdowner } +) + +func TestServiceStartupShutdownError(t *testing.T) { + var seq atomic.Int64 + + services := []application.Service{ + svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}), + svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq, ShutdownErr: true}), + svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq}), + svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq, StartupErr: true, ShutdownErr: true}), + svctest.Configure(&Service5{}, svctest.Config{Id: 4, T: t, Seq: &seq, StartupErr: true, ShutdownErr: true}), + svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq, StartupErr: true, ShutdownErr: true}), + } + + expectedShutdownErrors := []int{1} + var errCount atomic.Int64 + + var app *application.App + app = apptest.New(t, application.Options{ + Services: services[:3], + ErrorHandler: func(err error) { + var mock *svctest.Error + if !errors.As(err, &mock) { + app.Logger.Error(err.Error()) + return + } + + i := int(errCount.Add(1) - 1) + if i < len(expectedShutdownErrors) && mock.Id == expectedShutdownErrors[i] { + return + } + + cut := min(i, len(expectedShutdownErrors)) + if slices.Contains(expectedShutdownErrors[:cut], mock.Id) { + t.Errorf("Late or duplicate shutdown error for service #%d", mock.Id) + } else if slices.Contains(expectedShutdownErrors[cut:], mock.Id) { + t.Errorf("Early shutdown error for service #%d", mock.Id) + } else { + t.Errorf("Unexpected shutdown error for service #%d", mock.Id) + } + }, + }) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + app.RegisterService(services[3]) + wg.Done() + }() + go func() { + app.RegisterService(services[4]) + wg.Done() + }() + wg.Wait() + + app.RegisterService(services[5]) + + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) { + t.Errorf("Application started") + app.Quit() + }) + + var mock *svctest.Error + + err := apptest.Run(t, app) + if err != nil { + if !errors.As(err, &mock) { + t.Fatal(err) + } + } + + if mock == nil { + t.Fatal("Wanted error for service #3 or #4, got none") + } else if mock.Id != 3 && mock.Id != 4 { + t.Errorf("Wanted error for service #3 or #4, got #%d", mock.Id) + } + + if ec := errCount.Load(); ec != int64(len(expectedShutdownErrors)) { + t.Errorf("Wrong shutdown error count: wanted %d, got %d", len(expectedShutdownErrors), ec) + } + + if count := seq.Load(); count != 4+3 { + t.Errorf("Wrong startup+shutdown call count: wanted %d+%d, got %d", 4, 3, count) + } + + validate(t, services[0], true, true) + validate(t, services[1], true, true) + validate(t, services[2], true, true) + validate(t, services[3], mock.Id == 3, false) + validate(t, services[4], mock.Id == 4, false) + validate(t, services[5], false, false) +} + +func validate(t *testing.T, svc application.Service, startup bool, shutdown bool) { + id := svc.Instance().(interface{ Id() int }).Id() + startupSeq := svc.Instance().(interface{ StartupSeq() int64 }).StartupSeq() + shutdownSeq := svc.Instance().(interface{ ShutdownSeq() int64 }).ShutdownSeq() + + if startup != (startupSeq != 0) { + if startupSeq == 0 { + t.Errorf("Service #%d did not start up", id) + } else { + t.Errorf("Unexpected startup for service #%d at seq=%d", id, startupSeq) + } + } + + if shutdown != (shutdownSeq != 0) { + if shutdownSeq == 0 { + t.Errorf("Service #%d did not shut down", id) + } else { + t.Errorf("Unexpected shutdown for service #%d at seq=%d", id, shutdownSeq) + } + } +} diff --git a/v3/pkg/application/internal/tests/utils.go b/v3/pkg/application/internal/tests/utils.go new file mode 100644 index 000000000..ef94ef5ba --- /dev/null +++ b/v3/pkg/application/internal/tests/utils.go @@ -0,0 +1,74 @@ +package tests + +import ( + "errors" + "os" + "runtime" + "testing" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +var appChan chan *application.App = make(chan *application.App, 1) +var errChan chan error = make(chan error, 1) +var endChan chan error = make(chan error, 1) + +func init() { runtime.LockOSThread() } + +func New(t *testing.T, options application.Options) *application.App { + var app *application.App + + app = application.Get() + if app != nil { + return app + } + + if options.Name == "" { + options.Name = t.Name() + } + + errorHandler := options.ErrorHandler + options.ErrorHandler = func(err error) { + if fatal := (*application.FatalError)(nil); errors.As(err, &fatal) { + endChan <- err + select {} // Block forever + } else if errorHandler != nil { + errorHandler(err) + } else { + app.Logger.Error(err.Error()) + } + } + + postShutdown := options.PostShutdown + options.PostShutdown = func() { + if postShutdown != nil { + postShutdown() + } + endChan <- nil + select {} // Block forever + } + + return application.New(options) +} + +func Run(t *testing.T, app *application.App) error { + appChan <- app + select { + case err := <-errChan: + return err + case fatal := <-endChan: + if fatal != nil { + t.Fatal(fatal) + } + return fatal + } +} + +func Main(m *testing.M) { + go func() { + os.Exit(m.Run()) + }() + + errChan <- (<-appChan).Run() + select {} // Block forever +} diff --git a/v3/pkg/application/key_binding_manager.go b/v3/pkg/application/key_binding_manager.go new file mode 100644 index 000000000..490cc1cd3 --- /dev/null +++ b/v3/pkg/application/key_binding_manager.go @@ -0,0 +1,66 @@ +package application + +// KeyBindingManager manages all key binding operations +type KeyBindingManager struct { + app *App +} + +// newKeyBindingManager creates a new KeyBindingManager instance +func newKeyBindingManager(app *App) *KeyBindingManager { + return &KeyBindingManager{ + app: app, + } +} + +// Add adds a key binding +func (kbm *KeyBindingManager) Add(accelerator string, callback func(window *WebviewWindow)) { + kbm.app.keyBindingsLock.Lock() + defer kbm.app.keyBindingsLock.Unlock() + kbm.app.keyBindings[accelerator] = callback +} + +// Remove removes a key binding +func (kbm *KeyBindingManager) Remove(accelerator string) { + kbm.app.keyBindingsLock.Lock() + defer kbm.app.keyBindingsLock.Unlock() + delete(kbm.app.keyBindings, accelerator) +} + +// Process processes a key binding and returns true if handled +func (kbm *KeyBindingManager) Process(accelerator string, window *WebviewWindow) bool { + kbm.app.keyBindingsLock.RLock() + callback, exists := kbm.app.keyBindings[accelerator] + kbm.app.keyBindingsLock.RUnlock() + + if exists && callback != nil { + callback(window) + return true + } + return false +} + +// KeyBinding represents a key binding with its accelerator and callback +type KeyBinding struct { + Accelerator string + Callback func(window *WebviewWindow) +} + +// GetAll returns all registered key bindings as a slice +func (kbm *KeyBindingManager) GetAll() []*KeyBinding { + kbm.app.keyBindingsLock.RLock() + defer kbm.app.keyBindingsLock.RUnlock() + + result := make([]*KeyBinding, 0, len(kbm.app.keyBindings)) + for accelerator, callback := range kbm.app.keyBindings { + result = append(result, &KeyBinding{ + Accelerator: accelerator, + Callback: callback, + }) + } + return result +} + +// HandleWindowKeyEvent handles window key events (internal use) +func (kbm *KeyBindingManager) handleWindowKeyEvent(event *windowKeyEvent) { + kbm.app.handleWindowKeyEvent(event) +} diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index 33006a0c2..f750efdd0 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -334,7 +334,7 @@ func processApplicationEvent(eventID C.uint, data pointer) { switch event.Id { case uint(events.Linux.SystemThemeChanged): - isDark := globalApplication.IsDarkMode() + isDark := globalApplication.Env.IsDarkMode() event.Context().setIsDarkMode(isDark) } applicationEvents <- event @@ -465,7 +465,7 @@ func (a *linuxApp) setIcon(icon []byte) { var gerror *C.GError pixbuf := C.gdk_pixbuf_new_from_stream(stream, nil, &gerror) if gerror != nil { - a.parent.error("Failed to load application icon: " + C.GoString(gerror.message)) + a.parent.error("failed to load application icon: %s", C.GoString(gerror.message)) C.g_error_free(gerror) return } @@ -1003,9 +1003,7 @@ func (w *linuxWebviewWindow) gtkWidget() *C.GtkWidget { return (*C.GtkWidget)(w.window) } -func (w *linuxWebviewWindow) hide() { - // save position - w.lastX, w.lastY = w.position() +func (w *linuxWebviewWindow) windowHide() { C.gtk_widget_hide(w.gtkWidget()) } @@ -1115,12 +1113,11 @@ func (w *linuxWebviewWindow) setSize(width, height int) { C.gint(height)) } -func (w *linuxWebviewWindow) show() { +func (w *linuxWebviewWindow) windowShow() { if w.gtkWidget() == nil { return } C.gtk_widget_show_all(w.gtkWidget()) - //w.setPosition(w.lastX, w.lastY) } func windowIgnoreMouseEvents(window pointer, webview pointer, ignore bool) { @@ -1268,7 +1265,7 @@ func (w *linuxWebviewWindow) setURL(uri string) { //export emit func emit(we *C.WindowEvent) { - window := globalApplication.getWindowForID(uint(we.id)) + window, _ := globalApplication.Window.GetByID(uint(we.id)) if window != nil { windowEvents <- &windowEvent{ WindowID: window.ID(), @@ -1279,7 +1276,7 @@ func emit(we *C.WindowEvent) { //export handleConfigureEvent func handleConfigureEvent(widget *C.GtkWidget, event *C.GdkEventConfigure, data C.uintptr_t) C.gboolean { - window := globalApplication.getWindowForID(uint(data)) + window, _ := globalApplication.Window.GetByID(uint(data)) if window != nil { lw, ok := window.(*WebviewWindow).impl.(*linuxWebviewWindow) if !ok { @@ -1460,7 +1457,7 @@ func onButtonEvent(_ *C.GtkWidget, event *C.GdkEventButton, data C.uintptr_t) C. GdkButtonRelease := C.GDK_BUTTON_RELEASE // 7 windowId := uint(C.uint(data)) - window := globalApplication.getWindowForID(windowId) + window, _ := globalApplication.Window.GetByID(windowId) if window == nil { return C.gboolean(0) } @@ -1497,7 +1494,7 @@ func onMenuButtonEvent(_ *C.GtkWidget, event *C.GdkEventButton, data C.uintptr_t GdkButtonRelease := C.GDK_BUTTON_RELEASE // 7 windowId := uint(C.uint(data)) - window := globalApplication.getWindowForID(windowId) + window, _ := globalApplication.Window.GetByID(windowId) if window == nil { return C.gboolean(0) } @@ -1589,9 +1586,14 @@ func onProcessRequest(request *C.WebKitURISchemeRequest, data C.uintptr_t) { webView := C.webkit_uri_scheme_request_get_web_view(request) windowId := uint(C.get_window_id(unsafe.Pointer(webView))) webviewRequests <- &webViewAssetRequest{ - Request: webview.NewRequest(unsafe.Pointer(request)), - windowId: windowId, - windowName: globalApplication.getWindowForID(windowId).Name(), + Request: webview.NewRequest(unsafe.Pointer(request)), + windowId: windowId, + windowName: func() string { + if window, ok := globalApplication.Window.GetByID(windowId); ok { + return window.Name() + } + return "" + }(), } } @@ -1671,9 +1673,12 @@ func runChooserDialog(window pointer, allowMultiple, createFolders, showHidden b displayStr := C.CString(filter.DisplayName) C.gtk_file_filter_set_name(f, displayStr) C.free(unsafe.Pointer(displayStr)) - patternStr := C.CString(filter.Pattern) - C.gtk_file_filter_add_pattern(f, patternStr) - C.free(unsafe.Pointer(patternStr)) + patterns := strings.Split(filter.Pattern, ";") + for _, pattern := range patterns { + patternStr := C.CString(strings.TrimSpace(pattern)) + C.gtk_file_filter_add_pattern(f, patternStr) + C.free(unsafe.Pointer(patternStr)) + } C.gtk_file_chooser_add_filter((*C.GtkFileChooser)(fc), f) gtkFilters = append(gtkFilters, f) } diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index 258eecc94..c305e75b9 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -824,9 +824,14 @@ func windowNewWebview(parentId uint, gpuPolicy int) pointer { "wails", pointer(purego.NewCallback(func(request uintptr) { webviewRequests <- &webViewAssetRequest{ - Request: webview.NewRequest(request), - windowId: parentId, - windowName: globalApplication.getWindowForID(parentId).Name(), + Request: webview.NewRequest(request), + windowId: parentId, + windowName: func() string { + if window, ok := globalApplication.Window.GetByID(parentId); ok { + return window.Name() + } + return "" + }(), } })), 0, @@ -1206,3 +1211,18 @@ func runSaveFileDialog(dialog *SaveFileDialogStruct) (string, error) { func isOnMainThread() bool { return mainThreadId == gThreadSelf() } + +// linuxWebviewWindow show/hide methods for purego implementation +func (w *linuxWebviewWindow) windowShow() { + if w.window == 0 { + return + } + windowShow(w.window) +} + +func (w *linuxWebviewWindow) windowHide() { + if w.window == 0 { + return + } + windowHide(w.window) +} diff --git a/v3/pkg/application/logger_dev.go b/v3/pkg/application/logger_dev.go index 4952e87bf..e84f49490 100644 --- a/v3/pkg/application/logger_dev.go +++ b/v3/pkg/application/logger_dev.go @@ -11,7 +11,7 @@ import ( "github.com/mattn/go-isatty" ) -func DefaultLogger(level slog.Level) *slog.Logger { +func DefaultLogger(level slog.Leveler) *slog.Logger { return slog.New(tint.NewHandler(os.Stderr, &tint.Options{ TimeFormat: time.Kitchen, NoColor: !isatty.IsTerminal(os.Stderr.Fd()), diff --git a/v3/pkg/application/logger_dev_windows.go b/v3/pkg/application/logger_dev_windows.go index 5a43abc5a..20d20c376 100644 --- a/v3/pkg/application/logger_dev_windows.go +++ b/v3/pkg/application/logger_dev_windows.go @@ -3,15 +3,16 @@ package application import ( - "github.com/lmittmann/tint" - "github.com/mattn/go-colorable" - "github.com/mattn/go-isatty" "log/slog" "os" "time" + + "github.com/lmittmann/tint" + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" ) -func DefaultLogger(level slog.Level) *slog.Logger { +func DefaultLogger(level slog.Leveler) *slog.Logger { return slog.New(tint.NewHandler(colorable.NewColorable(os.Stderr), &tint.Options{ TimeFormat: time.StampMilli, NoColor: !isatty.IsTerminal(os.Stderr.Fd()), diff --git a/v3/pkg/application/logger_prod.go b/v3/pkg/application/logger_prod.go index 3cfbf4036..a748611ce 100644 --- a/v3/pkg/application/logger_prod.go +++ b/v3/pkg/application/logger_prod.go @@ -7,6 +7,6 @@ import ( "log/slog" ) -func DefaultLogger(level slog.Level) *slog.Logger { +func DefaultLogger(level slog.Leveler) *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } diff --git a/v3/pkg/application/menu.go b/v3/pkg/application/menu.go index 935000e20..96a971da1 100644 --- a/v3/pkg/application/menu.go +++ b/v3/pkg/application/menu.go @@ -20,11 +20,11 @@ func NewContextMenu(name string) *ContextMenu { func (m *ContextMenu) Update() { m.Menu.Update() - globalApplication.registerContextMenu(m) + globalApplication.ContextMenu.Add(m.name, m) } func (m *ContextMenu) Destroy() { - globalApplication.unregisterContextMenu(m.name) + globalApplication.ContextMenu.Remove(m.name) } type Menu struct { @@ -215,7 +215,7 @@ func (m *Menu) Prepend(in *Menu) { } func (a *App) NewMenu() *Menu { - return &Menu{} + return a.Menu.New() } func NewMenuFromItems(item *MenuItem, items ...*MenuItem) *Menu { diff --git a/v3/pkg/application/menu_darwin.go b/v3/pkg/application/menu_darwin.go index 3a5fa69e4..a17031489 100644 --- a/v3/pkg/application/menu_darwin.go +++ b/v3/pkg/application/menu_darwin.go @@ -72,19 +72,18 @@ func newMenuImpl(menu *Menu) *macosMenu { } func (m *macosMenu) update() { - if m.nsMenu == nil { - m.nsMenu = C.createNSMenu(C.CString(m.menu.label)) - } else { - C.clearMenu(m.nsMenu) - } - m.processMenu(m.nsMenu, m.menu) + InvokeSync(func() { + if m.nsMenu == nil { + m.nsMenu = C.createNSMenu(C.CString(m.menu.label)) + } else { + C.clearMenu(m.nsMenu) + } + m.processMenu(m.nsMenu, m.menu) + }) } func (m *macosMenu) processMenu(parent unsafe.Pointer, menu *Menu) { for _, item := range menu.items { - if item.hidden { - continue - } switch item.itemType { case submenu: submenu := item.submenu @@ -100,6 +99,9 @@ func (m *macosMenu) processMenu(parent unsafe.Pointer, menu *Menu) { case text, checkbox, radio: menuItem := newMenuItemImpl(item) item.impl = menuItem + if item.hidden { + menuItem.setHidden(true) + } C.addMenuItem(parent, menuItem.nsMenuItem) case separator: C.addMenuSeparator(parent) diff --git a/v3/pkg/application/menu_manager.go b/v3/pkg/application/menu_manager.go new file mode 100644 index 000000000..4c3a36c83 --- /dev/null +++ b/v3/pkg/application/menu_manager.go @@ -0,0 +1,55 @@ +package application + +// MenuManager manages menu-related operations +type MenuManager struct { + app *App +} + +// newMenuManager creates a new MenuManager instance +func newMenuManager(app *App) *MenuManager { + return &MenuManager{ + app: app, + } +} + +// Set sets the application menu +func (mm *MenuManager) Set(menu *Menu) { + mm.SetApplicationMenu(menu) +} + +// SetApplicationMenu sets the application menu +func (mm *MenuManager) SetApplicationMenu(menu *Menu) { + mm.app.applicationMenu = menu + if mm.app.impl != nil { + mm.app.impl.setApplicationMenu(menu) + } +} + +// GetApplicationMenu returns the current application menu +func (mm *MenuManager) GetApplicationMenu() *Menu { + return mm.app.applicationMenu +} + +// New creates a new menu +func (mm *MenuManager) New() *Menu { + return &Menu{} +} + +// ShowAbout shows the about dialog +func (mm *MenuManager) ShowAbout() { + if mm.app.impl != nil { + mm.app.impl.showAboutDialog(mm.app.options.Name, mm.app.options.Description, mm.app.options.Icon) + } +} + +// handleMenuItemClicked handles menu item click events (internal use) +func (mm *MenuManager) handleMenuItemClicked(menuItemID uint) { + defer handlePanic() + + menuItem := getMenuItemByID(menuItemID) + if menuItem == nil { + mm.app.warning("MenuItem #%d not found", menuItemID) + return + } + menuItem.handleClick() +} diff --git a/v3/pkg/application/menu_windows.go b/v3/pkg/application/menu_windows.go index 04a089982..6954cdec1 100644 --- a/v3/pkg/application/menu_windows.go +++ b/v3/pkg/application/menu_windows.go @@ -36,19 +36,23 @@ func (w *windowsMenu) update() { func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) { for _, item := range inputMenu.items { + w.currentMenuID++ + itemID := w.currentMenuID + w.menuMapping[itemID] = item + + menuItemImpl := newMenuItemImpl(item, parentMenu, itemID) + menuItemImpl.parent = inputMenu + item.impl = menuItemImpl + if item.Hidden() { if item.accelerator != nil && item.callback != nil { if w.parentWindow != nil { w.parentWindow.parent.removeMenuBinding(item.accelerator) } else { - globalApplication.removeKeyBinding(item.accelerator.String()) + globalApplication.KeyBinding.Remove(item.accelerator.String()) } } - continue } - w.currentMenuID++ - itemID := w.currentMenuID - w.menuMapping[itemID] = item flags := uint32(w32.MF_STRING) if item.disabled { @@ -76,7 +80,7 @@ func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) { if w.parentWindow != nil { w.parentWindow.parent.addMenuBinding(item.accelerator, item) } else { - globalApplication.addKeyBinding(item.accelerator.String(), func(w *WebviewWindow) { + globalApplication.KeyBinding.Add(item.accelerator.String(), func(w *WebviewWindow) { item.handleClick() }) } @@ -84,6 +88,11 @@ func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) { } var menuText = w32.MustStringToUTF16Ptr(thisText) + // If the item is hidden, don't append + if item.Hidden() { + continue + } + w32.AppendMenu(parentMenu, flags, uintptr(itemID), menuText) if item.bitmap != nil { w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil) diff --git a/v3/pkg/application/menuitem.go b/v3/pkg/application/menuitem.go index ab5371e24..5d11a87c7 100644 --- a/v3/pkg/application/menuitem.go +++ b/v3/pkg/application/menuitem.go @@ -1,8 +1,6 @@ package application import ( - "fmt" - "os" "sync" "sync/atomic" ) @@ -226,15 +224,13 @@ func NewRole(role Role) *MenuItem { result = NewHelpMenuItem() default: - globalApplication.error(fmt.Sprintf("No support for role: %v", role)) - os.Exit(1) + globalApplication.error("no support for role: %v", role) } - if result == nil { - return nil + if result != nil { + result.role = role } - result.role = role return result } @@ -279,7 +275,7 @@ func (m *MenuItem) handleClick() { func (m *MenuItem) SetAccelerator(shortcut string) *MenuItem { accelerator, err := parseAccelerator(shortcut) if err != nil { - globalApplication.error("invalid accelerator. %v", err.Error()) + globalApplication.error("invalid accelerator: %w", err) return m } m.accelerator = accelerator diff --git a/v3/pkg/application/menuitem_dev.go b/v3/pkg/application/menuitem_dev.go index 7eb2a44ae..4ce99fd66 100644 --- a/v3/pkg/application/menuitem_dev.go +++ b/v3/pkg/application/menuitem_dev.go @@ -6,7 +6,7 @@ func NewOpenDevToolsMenuItem() *MenuItem { return NewMenuItem("Open Developer Tools"). SetAccelerator("Alt+Command+I"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.OpenDevTools() } diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go index 34dac1f02..68a3ddd4a 100644 --- a/v3/pkg/application/menuitem_linux.go +++ b/v3/pkg/application/menuitem_linux.go @@ -253,7 +253,7 @@ func newSelectAllMenuItem() *MenuItem { func newAboutMenuItem() *MenuItem { return NewMenuItem("About " + globalApplication.options.Name). OnClick(func(ctx *Context) { - globalApplication.ShowAboutDialog() + globalApplication.Menu.ShowAbout() }) } @@ -261,7 +261,7 @@ func newCloseMenuItem() *MenuItem { return NewMenuItem("Close"). SetAccelerator("CmdOrCtrl+w"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.Close() } @@ -272,7 +272,7 @@ func newReloadMenuItem() *MenuItem { return NewMenuItem("Reload"). SetAccelerator("CmdOrCtrl+r"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.Reload() } @@ -283,7 +283,7 @@ func newForceReloadMenuItem() *MenuItem { return NewMenuItem("Force Reload"). SetAccelerator("CmdOrCtrl+Shift+r"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.ForceReload() } @@ -293,7 +293,7 @@ func newForceReloadMenuItem() *MenuItem { func newToggleFullscreenMenuItem() *MenuItem { result := NewMenuItem("Toggle Full Screen"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.ToggleFullscreen() } @@ -311,7 +311,7 @@ func newZoomResetMenuItem() *MenuItem { return NewMenuItem("Actual Size"). SetAccelerator("CmdOrCtrl+0"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.ZoomReset() } @@ -322,7 +322,7 @@ func newZoomInMenuItem() *MenuItem { return NewMenuItem("Zoom In"). SetAccelerator("CmdOrCtrl+plus"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.ZoomIn() } @@ -333,7 +333,7 @@ func newZoomOutMenuItem() *MenuItem { return NewMenuItem("Zoom Out"). SetAccelerator("CmdOrCtrl+-"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.ZoomOut() } @@ -344,7 +344,7 @@ func newMinimizeMenuItem() *MenuItem { return NewMenuItem("Minimize"). SetAccelerator("CmdOrCtrl+M"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.Minimise() } @@ -354,7 +354,7 @@ func newMinimizeMenuItem() *MenuItem { func newZoomMenuItem() *MenuItem { return NewMenuItem("Zoom"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.Zoom() } @@ -364,7 +364,7 @@ func newZoomMenuItem() *MenuItem { func newFullScreenMenuItem() *MenuItem { return NewMenuItem("Fullscreen"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.Fullscreen() } diff --git a/v3/pkg/application/menuitem_roles.go b/v3/pkg/application/menuitem_roles.go index 4eb4e48d0..f36908884 100644 --- a/v3/pkg/application/menuitem_roles.go +++ b/v3/pkg/application/menuitem_roles.go @@ -36,7 +36,7 @@ func NewUndoMenuItem() *MenuItem { SetAccelerator("CmdOrCtrl+z") if runtime.GOOS != "darwin" { result.OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.undo() } @@ -51,7 +51,7 @@ func NewRedoMenuItem() *MenuItem { SetAccelerator("CmdOrCtrl+Shift+z") if runtime.GOOS != "darwin" { result.OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.redo() } @@ -66,7 +66,7 @@ func NewCutMenuItem() *MenuItem { if runtime.GOOS != "darwin" { result.OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.cut() } @@ -81,7 +81,7 @@ func NewCopyMenuItem() *MenuItem { if runtime.GOOS != "darwin" { result.OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.copy() } @@ -96,7 +96,7 @@ func NewPasteMenuItem() *MenuItem { if runtime.GOOS != "darwin" { result.OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.paste() } @@ -116,7 +116,7 @@ func NewDeleteMenuItem() *MenuItem { if runtime.GOOS != "darwin" { result.OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.delete() } @@ -145,7 +145,7 @@ func NewSelectAllMenuItem() *MenuItem { if runtime.GOOS != "darwin" { result.OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.selectAll() } @@ -161,7 +161,7 @@ func NewAboutMenuItem() *MenuItem { } return NewMenuItem(label). OnClick(func(ctx *Context) { - globalApplication.ShowAboutDialog() + globalApplication.Menu.ShowAbout() }) } @@ -169,7 +169,7 @@ func NewCloseMenuItem() *MenuItem { return NewMenuItem("Close"). SetAccelerator("CmdOrCtrl+w"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.Close() } @@ -180,7 +180,7 @@ func NewReloadMenuItem() *MenuItem { return NewMenuItem("Reload"). SetAccelerator("CmdOrCtrl+r"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.Reload() } @@ -191,7 +191,7 @@ func NewForceReloadMenuItem() *MenuItem { return NewMenuItem("Force Reload"). SetAccelerator("CmdOrCtrl+Shift+r"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.ForceReload() } @@ -202,7 +202,7 @@ func NewToggleFullscreenMenuItem() *MenuItem { result := NewMenuItem("Toggle Full Screen"). SetAccelerator("Ctrl+Command+F"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.ToggleFullscreen() } @@ -218,7 +218,7 @@ func NewZoomResetMenuItem() *MenuItem { return NewMenuItem("Actual Size"). SetAccelerator("CmdOrCtrl+0"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.ZoomReset() } @@ -229,7 +229,7 @@ func NewZoomInMenuItem() *MenuItem { return NewMenuItem("Zoom In"). SetAccelerator("CmdOrCtrl+plus"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.ZoomIn() } @@ -240,7 +240,7 @@ func NewZoomOutMenuItem() *MenuItem { return NewMenuItem("Zoom Out"). SetAccelerator("CmdOrCtrl+-"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.ZoomOut() } @@ -251,7 +251,7 @@ func NewMinimiseMenuItem() *MenuItem { return NewMenuItem("Minimize"). SetAccelerator("CmdOrCtrl+M"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.Minimise() } @@ -261,7 +261,7 @@ func NewMinimiseMenuItem() *MenuItem { func NewZoomMenuItem() *MenuItem { return NewMenuItem("Zoom"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.Zoom() } @@ -271,7 +271,7 @@ func NewZoomMenuItem() *MenuItem { func NewFullScreenMenuItem() *MenuItem { return NewMenuItem("Fullscreen"). OnClick(func(ctx *Context) { - currentWindow := globalApplication.CurrentWindow() + currentWindow := globalApplication.Window.Current() if currentWindow != nil { currentWindow.Fullscreen() } diff --git a/v3/pkg/application/menuitem_windows.go b/v3/pkg/application/menuitem_windows.go index b13ef6f5a..941bf2388 100644 --- a/v3/pkg/application/menuitem_windows.go +++ b/v3/pkg/application/menuitem_windows.go @@ -3,55 +3,41 @@ package application import ( - "github.com/wailsapp/wails/v3/pkg/w32" "unsafe" + + "github.com/wailsapp/wails/v3/pkg/w32" ) type windowsMenuItem struct { parent *Menu menuItem *MenuItem - hMenu w32.HMENU - id int - label string - disabled bool - checked bool - itemType menuItemType - hidden bool - submenu w32.HMENU - itemAfter *MenuItem + hMenu w32.HMENU + id int + label string + disabled bool + checked bool + itemType menuItemType + hidden bool + submenu w32.HMENU } func (m *windowsMenuItem) setHidden(hidden bool) { - m.hidden = hidden - if m.hidden { - // iterate the parent items and find the menu item before us - for i, item := range m.parent.items { - if item == m.menuItem { - if i < len(m.parent.items)-1 { - m.itemAfter = m.parent.items[i+1] - } else { - m.itemAfter = nil - } - break - } - } - // Get the position of this menu item in the parent menu - // m.pos = w32.GetMenuItemPosition(m.hMenu, uint32(m.id)) + if hidden && !m.hidden { + m.hidden = true // Remove from parent menu w32.RemoveMenu(m.hMenu, m.id, w32.MF_BYCOMMAND) - } else { - // Add to parent menu - // Get the position of the item before us + } else if !hidden && m.hidden { + m.hidden = false + // Reinsert into parent menu at correct visible position var pos int - if m.itemAfter != nil { - for i, item := range m.parent.items { - if item == m.itemAfter { - pos = i - 1 - break - } + for _, item := range m.parent.items { + if item == m.menuItem { + break + } + if item.hidden == false { + pos++ } - m.itemAfter = nil } w32.InsertMenuItem(m.hMenu, uint32(pos), true, m.getMenuInfo()) } @@ -121,7 +107,7 @@ func (m *windowsMenuItem) setBitmap(bitmap []byte) { // Set the icon err := w32.SetMenuIcons(m.hMenu, m.id, bitmap, nil) if err != nil { - globalApplication.error("Unable to set bitmap on menu item: %s", err.Error()) + globalApplication.error("unable to set bitmap on menu item: %w", err) return } m.update() diff --git a/v3/pkg/application/messageprocessor.go b/v3/pkg/application/messageprocessor.go index 1ba2368bf..5dd1d0f70 100644 --- a/v3/pkg/application/messageprocessor.go +++ b/v3/pkg/application/messageprocessor.go @@ -3,11 +3,13 @@ package application import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "net/http" "strconv" "sync" + "math" ) // TODO maybe we could use a new struct that has the targetWindow as an attribute so we could get rid of passing the targetWindow @@ -41,19 +43,20 @@ func NewMessageProcessor(logger *slog.Logger) *MessageProcessor { } } -func (m *MessageProcessor) httpError(rw http.ResponseWriter, message string, args ...any) { - m.Error(message, args...) - rw.WriteHeader(http.StatusBadRequest) - _, err := rw.Write([]byte(fmt.Sprintf(message, args...))) +func (m *MessageProcessor) httpError(rw http.ResponseWriter, message string, err error) { + m.Error(message, "error", err) + rw.WriteHeader(http.StatusUnprocessableEntity) + _, err = rw.Write([]byte(err.Error())) if err != nil { - m.Error("Unable to write error message: %s", err) + m.Error("Unable to write error response:", "error", err) } } func (m *MessageProcessor) getTargetWindow(r *http.Request) (Window, string) { windowName := r.Header.Get(webViewRequestHeaderWindowName) if windowName != "" { - return globalApplication.GetWindowByName(windowName), windowName + window, _ := globalApplication.Window.GetByName(windowName) + return window, windowName } windowID := r.Header.Get(webViewRequestHeaderWindowId) if windowID == "" { @@ -61,12 +64,17 @@ func (m *MessageProcessor) getTargetWindow(r *http.Request) (Window, string) { } wID, err := strconv.ParseUint(windowID, 10, 64) if err != nil { - m.Error("Window ID '%s' not parsable: %s", windowID, err) + m.Error("Window ID not parsable:", "id", windowID, "error", err) return nil, windowID } - targetWindow := globalApplication.getWindowForID(uint(wID)) + // Check if wID is within the valid range for uint + if wID > math.MaxUint32 { + m.Error("Window ID out of range for uint:", "id", wID) + return nil, windowID + } + targetWindow, _ := globalApplication.Window.GetByID(uint(wID)) if targetWindow == nil { - m.Error("Window ID %d not found", wID) + m.Error("Window ID not found:", "id", wID) return nil, windowID } return targetWindow, windowID @@ -75,7 +83,7 @@ func (m *MessageProcessor) getTargetWindow(r *http.Request) (Window, string) { func (m *MessageProcessor) ServeHTTP(rw http.ResponseWriter, r *http.Request) { object := r.URL.Query().Get("object") if object == "" { - m.httpError(rw, "Invalid runtime call") + m.httpError(rw, "Invalid runtime call:", errors.New("missing object value")) return } @@ -90,19 +98,19 @@ func (m *MessageProcessor) HandleRuntimeCallWithIDs(rw http.ResponseWriter, r *h }() object, err := strconv.Atoi(r.URL.Query().Get("object")) if err != nil { - m.httpError(rw, "Error decoding object value: "+err.Error()) + m.httpError(rw, "Invalid runtime call:", fmt.Errorf("error decoding object value: %w", err)) return } method, err := strconv.Atoi(r.URL.Query().Get("method")) if err != nil { - m.httpError(rw, "Error decoding method value: "+err.Error()) + m.httpError(rw, "Invalid runtime call:", fmt.Errorf("error decoding method value: %w", err)) return } params := QueryParams(r.URL.Query()) targetWindow, nameOrID := m.getTargetWindow(r) if targetWindow == nil { - m.httpError(rw, fmt.Sprintf("Window '%s' not found", nameOrID)) + m.httpError(rw, "Invalid runtime call:", fmt.Errorf("window '%s' not found", nameOrID)) return } @@ -130,7 +138,7 @@ func (m *MessageProcessor) HandleRuntimeCallWithIDs(rw http.ResponseWriter, r *h case cancelCallRequesst: m.processCallCancelMethod(method, rw, r, targetWindow, params) default: - m.httpError(rw, "Unknown runtime call: %d", object) + m.httpError(rw, "Invalid runtime call:", fmt.Errorf("unknown object %d", object)) } } @@ -150,13 +158,13 @@ func (m *MessageProcessor) json(rw http.ResponseWriter, data any) { if data != nil { jsonPayload, err = json.Marshal(data) if err != nil { - m.Error("Unable to convert data to JSON. Please report this to the Wails team! Error: %s", err) + m.Error("Unable to convert data to JSON. Please report this to the Wails team!", "error", err) return } } _, err = rw.Write(jsonPayload) if err != nil { - m.Error("Unable to write json payload. Please report this to the Wails team! Error: %s", err) + m.Error("Unable to write json payload. Please report this to the Wails team!", "error", err) return } m.ok(rw) @@ -165,7 +173,7 @@ func (m *MessageProcessor) json(rw http.ResponseWriter, data any) { func (m *MessageProcessor) text(rw http.ResponseWriter, data string) { _, err := rw.Write([]byte(data)) if err != nil { - m.Error("Unable to write json payload. Please report this to the Wails team! Error: %s", err) + m.Error("Unable to write json payload. Please report this to the Wails team!", "error", err) return } rw.Header().Set("Content-Type", "text/plain") diff --git a/v3/pkg/application/messageprocessor_application.go b/v3/pkg/application/messageprocessor_application.go index fae7b55a6..fe7cd201f 100644 --- a/v3/pkg/application/messageprocessor_application.go +++ b/v3/pkg/application/messageprocessor_application.go @@ -1,6 +1,7 @@ package application import ( + "fmt" "net/http" ) @@ -28,9 +29,9 @@ func (m *MessageProcessor) processApplicationMethod(method int, rw http.Response globalApplication.Show() m.ok(rw) default: - m.httpError(rw, "Unknown application method: %d", method) + m.httpError(rw, "Invalid application call:", fmt.Errorf("unknown method: %d", method)) + return } - m.Info("Runtime Call:", "method", "Application."+applicationMethodNames[method]) - + m.Info("Runtime call:", "method", "Application."+applicationMethodNames[method]) } diff --git a/v3/pkg/application/messageprocessor_browser.go b/v3/pkg/application/messageprocessor_browser.go index 164d0fdab..469369b13 100644 --- a/v3/pkg/application/messageprocessor_browser.go +++ b/v3/pkg/application/messageprocessor_browser.go @@ -1,8 +1,11 @@ package application import ( - "github.com/pkg/browser" + "errors" + "fmt" "net/http" + + "github.com/pkg/browser" ) const ( @@ -14,10 +17,9 @@ var browserMethods = map[int]string{ } func (m *MessageProcessor) processBrowserMethod(method int, rw http.ResponseWriter, _ *http.Request, _ Window, params QueryParams) { - args, err := params.Args() if err != nil { - m.httpError(rw, "Unable to parse arguments: %s", err.Error()) + m.httpError(rw, "Invalid browser call:", fmt.Errorf("unable to parse arguments: %w", err)) return } @@ -25,19 +27,20 @@ func (m *MessageProcessor) processBrowserMethod(method int, rw http.ResponseWrit case BrowserOpenURL: url := args.String("url") if url == nil { - m.Error("OpenURL: url is required") + m.httpError(rw, "Invalid browser call:", errors.New("missing argument 'url'")) return } + err := browser.OpenURL(*url) if err != nil { - m.Error("OpenURL: %s", err.Error()) + m.httpError(rw, "OpenURL failed:", err) return } + m.ok(rw) - m.Info("Runtime Call:", "method", "Browser."+browserMethods[method], "url", *url) + m.Info("Runtime call:", "method", "Browser."+browserMethods[method], "url", *url) default: - m.httpError(rw, "Unknown browser method: %d", method) + m.httpError(rw, "Invalid browser call:", fmt.Errorf("unknown method: %d", method)) return } - } diff --git a/v3/pkg/application/messageprocessor_call.go b/v3/pkg/application/messageprocessor_call.go index 26ea5188e..dc25d375c 100644 --- a/v3/pkg/application/messageprocessor_call.go +++ b/v3/pkg/application/messageprocessor_call.go @@ -3,6 +3,7 @@ package application import ( "context" "encoding/json" + "errors" "fmt" "net/http" ) @@ -15,33 +16,46 @@ const ( ) func (m *MessageProcessor) callErrorCallback(window Window, message string, callID *string, err error) { - errorMsg := fmt.Sprintf(message, err) - m.Error(errorMsg) - window.CallError(*callID, errorMsg) + m.Error(message, "id", *callID, "error", err) + if cerr := (*CallError)(nil); errors.As(err, &cerr) { + if data, jsonErr := json.Marshal(cerr); jsonErr == nil { + window.CallError(*callID, string(data), true) + return + } else { + m.Error("Unable to convert data to JSON. Please report this to the Wails team!", "id", *callID, "error", jsonErr) + } + } + + window.CallError(*callID, err.Error(), false) } -func (m *MessageProcessor) callCallback(window Window, callID *string, result string, isJSON bool) { +func (m *MessageProcessor) callCallback(window Window, callID *string, result string) { window.CallResponse(*callID, result) } func (m *MessageProcessor) processCallCancelMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) { args, err := params.Args() if err != nil { - m.httpError(rw, "Unable to parse arguments: %s", err.Error()) - return - } - callID := args.String("call-id") - if callID == nil || *callID == "" { - m.Error("call-id is required") + m.httpError(rw, "Invalid binding call:", fmt.Errorf("unable to parse arguments: %w", err)) return } - m.l.Lock() - cancel := m.runningCalls[*callID] - m.l.Unlock() + callID := args.String("call-id") + if callID == nil || *callID == "" { + m.httpError(rw, "Invalid binding call:", errors.New("missing argument 'call-id'")) + return + } + + var cancel func() + func() { + m.l.Lock() + defer m.l.Unlock() + cancel = m.runningCalls[*callID] + }() if cancel != nil { cancel() + m.Info("Binding call canceled:", "id", *callID) } m.ok(rw) } @@ -49,12 +63,13 @@ func (m *MessageProcessor) processCallCancelMethod(method int, rw http.ResponseW func (m *MessageProcessor) processCallMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) { args, err := params.Args() if err != nil { - m.httpError(rw, "Unable to parse arguments: %s", err.Error()) + m.httpError(rw, "Invalid binding call:", fmt.Errorf("unable to parse arguments: %w", err)) return } + callID := args.String("call-id") if callID == nil || *callID == "" { - m.Error("call-id is required") + m.httpError(rw, "Invalid binding call:", errors.New("missing argument 'call-id'")) return } @@ -63,86 +78,118 @@ func (m *MessageProcessor) processCallMethod(method int, rw http.ResponseWriter, var options CallOptions err := params.ToStruct(&options) if err != nil { - m.callErrorCallback(window, "Error parsing call options: %s", callID, err) - return - } - var boundMethod *BoundMethod - if options.MethodName != "" { - boundMethod = globalApplication.bindings.Get(&options) - if boundMethod == nil { - m.callErrorCallback(window, "Error getting binding for method: %s", callID, fmt.Errorf("method '%s' not found", options.MethodName)) - return - } - } else { - boundMethod = globalApplication.bindings.GetByID(options.MethodID) - } - if boundMethod == nil { - m.callErrorCallback(window, "Error getting binding for method: %s", callID, fmt.Errorf("method ID %d not found", options.MethodID)) + m.httpError(rw, "Invalid binding call:", fmt.Errorf("error parsing call options: %w", err)) return } ctx, cancel := context.WithCancel(context.WithoutCancel(r.Context())) + // Schedule cancel in case panics happen before starting the call. + cancelRequired := true + defer func() { + if cancelRequired { + cancel() + } + }() + ambiguousID := false - m.l.Lock() - if m.runningCalls[*callID] != nil { - ambiguousID = true - } else { - m.runningCalls[*callID] = cancel - } - m.l.Unlock() + func() { + m.l.Lock() + defer m.l.Unlock() + + if m.runningCalls[*callID] != nil { + ambiguousID = true + } else { + m.runningCalls[*callID] = cancel + } + }() if ambiguousID { - cancel() - m.callErrorCallback(window, "Error calling method: %s, a method call with the same id is already running", callID, err) + m.httpError(rw, "Invalid binding call:", fmt.Errorf("ambiguous call id: %s", *callID)) return } - // Set the context values for the window - if window != nil { - ctx = context.WithValue(ctx, WindowKey, window) + m.ok(rw) // From now on, failures are reported through the error callback. + + // Log call + var methodRef any = options.MethodName + if options.MethodName == "" { + methodRef = options.MethodID } + m.Info("Binding call started:", "id", *callID, "method", methodRef) go func() { defer handlePanic() defer func() { - cancel() - m.l.Lock() + defer m.l.Unlock() delete(m.runningCalls, *callID) - m.l.Unlock() + }() + defer cancel() + + var boundMethod *BoundMethod + if options.MethodName != "" { + boundMethod = globalApplication.bindings.Get(&options) + if boundMethod == nil { + m.callErrorCallback(window, "Binding call failed:", callID, &CallError{ + Kind: ReferenceError, + Message: fmt.Sprintf("unknown bound method name '%s'", options.MethodName), + }) + return + } + } else { + boundMethod = globalApplication.bindings.GetByID(options.MethodID) + if boundMethod == nil { + m.callErrorCallback(window, "Binding call failed:", callID, &CallError{ + Kind: ReferenceError, + Message: fmt.Sprintf("unknown bound method id %d", options.MethodID), + }) + return + } + } + + // Prepare args for logging. This should never fail since json.Unmarshal succeeded before. + jsonArgs, _ := json.Marshal(options.Args) + var jsonResult []byte + defer func() { + m.Info("Binding call complete:", "id", *callID, "method", boundMethod, "args", string(jsonArgs), "result", string(jsonResult)) }() + // Set the context values for the window + if window != nil { + ctx = context.WithValue(ctx, WindowKey, window) + } + result, err := boundMethod.Call(ctx, options.Args) - if err != nil { - msg := fmt.Sprintf("Error calling method '%v'", boundMethod.Name) - m.callErrorCallback(window, msg+": %s", callID, err) + if cerr := (*CallError)(nil); errors.As(err, &cerr) { + switch cerr.Kind { + case ReferenceError, TypeError: + m.callErrorCallback(window, "Binding call failed:", callID, cerr) + case RuntimeError: + m.callErrorCallback(window, "Bound method returned an error:", callID, cerr) + } return } - var jsonResult = []byte("{}") + if result != nil { // convert result to json jsonResult, err = json.Marshal(result) if err != nil { - m.callErrorCallback(window, "Error converting result to json: %s", callID, err) + m.callErrorCallback(window, "Binding call failed:", callID, &CallError{ + Kind: TypeError, + Message: fmt.Sprintf("error marshaling result: %s", err), + }) return } } - m.callCallback(window, callID, string(jsonResult), true) - var jsonArgs struct { - Args json.RawMessage `json:"args"` - } - err = params.ToStruct(&jsonArgs) - if err != nil { - m.callErrorCallback(window, "Error parsing arguments: %s", callID, err) - return - } - m.Info("Call Binding:", "method", boundMethod, "args", string(jsonArgs.Args), "result", result) + m.callCallback(window, callID, string(jsonResult)) }() - m.ok(rw) - default: - m.httpError(rw, "Unknown call method: %d", method) - } + cancelRequired = false + + default: + m.httpError(rw, "Invalid binding call:", fmt.Errorf("unknown method: %d", method)) + return + } } diff --git a/v3/pkg/application/messageprocessor_clipboard.go b/v3/pkg/application/messageprocessor_clipboard.go index 8ba3b35d9..d77b6459d 100644 --- a/v3/pkg/application/messageprocessor_clipboard.go +++ b/v3/pkg/application/messageprocessor_clipboard.go @@ -1,6 +1,8 @@ package application import ( + "errors" + "fmt" "net/http" ) @@ -15,30 +17,31 @@ var clipboardMethods = map[int]string{ } func (m *MessageProcessor) processClipboardMethod(method int, rw http.ResponseWriter, _ *http.Request, _ Window, params QueryParams) { - args, err := params.Args() if err != nil { - m.httpError(rw, "Unable to parse arguments: %s", err.Error()) + m.httpError(rw, "Invalid clipboard call:", fmt.Errorf("unable to parse arguments: %w", err)) return } + var text string + switch method { case ClipboardSetText: - text := args.String("text") - if text == nil { - m.Error("SetText: text is required") + textp := args.String("text") + if textp == nil { + m.httpError(rw, "Invalid clipboard call:", errors.New("missing argument 'text'")) return } - globalApplication.Clipboard().SetText(*text) + text = *textp + globalApplication.Clipboard.SetText(text) m.ok(rw) - m.Info("Runtime Call:", "method", "Clipboard."+clipboardMethods[method], "text", *text) case ClipboardText: - text, _ := globalApplication.Clipboard().Text() + text, _ = globalApplication.Clipboard.Text() m.text(rw, text) - m.Info("Runtime Call:", "method", "Clipboard."+clipboardMethods[method], "text", text) default: - m.httpError(rw, "Unknown clipboard method: %d", method) + m.httpError(rw, "Invalid clipboard call:", fmt.Errorf("unknown method: %d", method)) return } + m.Info("Runtime call:", "method", "Clipboard."+clipboardMethods[method], "text", text) } diff --git a/v3/pkg/application/messageprocessor_contextmenu.go b/v3/pkg/application/messageprocessor_contextmenu.go index 84b0ea458..c315db15a 100644 --- a/v3/pkg/application/messageprocessor_contextmenu.go +++ b/v3/pkg/application/messageprocessor_contextmenu.go @@ -1,6 +1,7 @@ package application import ( + "fmt" "net/http" ) @@ -29,21 +30,21 @@ var contextmenuMethodNames = map[int]string{ } func (m *MessageProcessor) processContextMenuMethod(method int, rw http.ResponseWriter, _ *http.Request, window Window, params QueryParams) { - switch method { case ContextMenuOpen: var data ContextMenuData err := params.ToStruct(&data) if err != nil { - m.httpError(rw, "error parsing contextmenu message: %s", err.Error()) + m.httpError(rw, "Invalid contextmenu call:", fmt.Errorf("error parsing parameters: %w", err)) return } + window.OpenContextMenu(&data) + m.ok(rw) + m.Info("Runtime call:", "method", "ContextMenu."+contextmenuMethodNames[method], "id", data.Id, "x", data.X, "y", data.Y, "data", data.Data) default: - m.httpError(rw, "Unknown contextmenu method: %d", method) + m.httpError(rw, "Invalid contextmenu call:", fmt.Errorf("unknown method: %d", method)) + return } - - m.Info("Runtime Call:", "method", "ContextMenu."+contextmenuMethodNames[method]) - } diff --git a/v3/pkg/application/messageprocessor_dialog.go b/v3/pkg/application/messageprocessor_dialog.go index 8e0089ddb..772f25524 100644 --- a/v3/pkg/application/messageprocessor_dialog.go +++ b/v3/pkg/application/messageprocessor_dialog.go @@ -2,6 +2,7 @@ package application import ( "encoding/json" + "errors" "fmt" "net/http" "runtime" @@ -26,9 +27,8 @@ var dialogMethodNames = map[int]string{ } func (m *MessageProcessor) dialogErrorCallback(window Window, message string, dialogID *string, err error) { - errorMsg := fmt.Sprintf(message, err) - m.Error(errorMsg) - window.DialogError(*dialogID, errorMsg) + m.Error(message, "error", err) + window.DialogError(*dialogID, err.Error()) } func (m *MessageProcessor) dialogCallback(window Window, dialogID *string, result string, isJSON bool) { @@ -36,15 +36,15 @@ func (m *MessageProcessor) dialogCallback(window Window, dialogID *string, resul } func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) { - args, err := params.Args() if err != nil { - m.httpError(rw, "Unable to parse arguments: %s", err.Error()) + m.httpError(rw, "Invalid dialog call:", fmt.Errorf("unable to parse arguments: %w", err)) return } + dialogID := args.String("dialog-id") if dialogID == nil { - m.Error("dialog-id is required") + m.httpError(rw, "Invalid window call:", errors.New("missing argument 'dialog-id'")) return } @@ -55,7 +55,7 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite var options MessageDialogOptions err := params.ToStruct(&options) if err != nil { - m.dialogErrorCallback(window, "Error parsing dialog options: %s", dialogID, err) + m.httpError(rw, "Invalid dialog call:", fmt.Errorf("error parsing dialog options: %w", err)) return } if len(options.Buttons) == 0 { @@ -91,78 +91,78 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite dialog.AddButtons(options.Buttons) dialog.Show() m.ok(rw) - m.Info("Runtime Call:", "method", methodName, "options", options) + m.Info("Runtime call:", "method", methodName, "options", options) case DialogOpenFile: var options OpenFileDialogOptions err := params.ToStruct(&options) if err != nil { - m.httpError(rw, "Error parsing dialog options: %s", err.Error()) + m.httpError(rw, "Invalid dialog call:", fmt.Errorf("error parsing dialog options: %w", err)) return } var detached = args.Bool("Detached") if detached == nil || !*detached { options.Window = window.(*WebviewWindow) } - dialog := OpenFileDialogWithOptions(&options) + dialog := globalApplication.Dialog.OpenFileWithOptions(&options) go func() { defer handlePanic() if options.AllowsMultipleSelection { files, err := dialog.PromptForMultipleSelection() if err != nil { - m.dialogErrorCallback(window, "Error getting selection: %s", dialogID, err) + m.dialogErrorCallback(window, "Dialog.OpenFile failed", dialogID, fmt.Errorf("error getting selection: %w", err)) return } else { result, err := json.Marshal(files) if err != nil { - m.dialogErrorCallback(window, "Error marshalling files: %s", dialogID, err) + m.dialogErrorCallback(window, "Dialog.OpenFile failed", dialogID, fmt.Errorf("error marshaling files: %w", err)) return } m.dialogCallback(window, dialogID, string(result), true) - m.Info("Runtime Call:", "method", methodName, "result", result) + m.Info("Runtime call:", "method", methodName, "result", result) } } else { file, err := dialog.PromptForSingleSelection() if err != nil { - m.dialogErrorCallback(window, "Error getting selection: %s", dialogID, err) + m.dialogErrorCallback(window, "Dialog.OpenFile failed", dialogID, fmt.Errorf("error getting selection: %w", err)) return } m.dialogCallback(window, dialogID, file, false) - m.Info("Runtime Call:", "method", methodName, "result", file) + m.Info("Runtime call:", "method", methodName, "result", file) } }() m.ok(rw) - m.Info("Runtime Call:", "method", methodName, "options", options) + m.Info("Runtime call:", "method", methodName, "options", options) case DialogSaveFile: var options SaveFileDialogOptions err := params.ToStruct(&options) if err != nil { - m.httpError(rw, "Error parsing dialog options: %s", err.Error()) + m.httpError(rw, "Invalid dialog call:", fmt.Errorf("error parsing dialog options: %w", err)) return } var detached = args.Bool("Detached") if detached == nil || !*detached { options.Window = window.(*WebviewWindow) } - dialog := SaveFileDialogWithOptions(&options) + dialog := globalApplication.Dialog.SaveFileWithOptions(&options) go func() { defer handlePanic() file, err := dialog.PromptForSingleSelection() if err != nil { - m.dialogErrorCallback(window, "Error getting selection: %s", dialogID, err) + m.dialogErrorCallback(window, "Dialog.SaveFile failed", dialogID, fmt.Errorf("error getting selection: %w", err)) return } m.dialogCallback(window, dialogID, file, false) - m.Info("Runtime Call:", "method", methodName, "result", file) + m.Info("Runtime call:", "method", methodName, "result", file) }() m.ok(rw) - m.Info("Runtime Call:", "method", methodName, "options", options) + m.Info("Runtime call:", "method", methodName, "options", options) default: - m.httpError(rw, "Unknown dialog method: %d", method) + m.httpError(rw, "Invalid dialog call:", fmt.Errorf("unknown method: %d", method)) + return } - } diff --git a/v3/pkg/application/messageprocessor_events.go b/v3/pkg/application/messageprocessor_events.go index 03a536864..93ab34064 100644 --- a/v3/pkg/application/messageprocessor_events.go +++ b/v3/pkg/application/messageprocessor_events.go @@ -1,7 +1,10 @@ package application import ( + "fmt" "net/http" + + "github.com/pkg/errors" ) const ( @@ -13,28 +16,26 @@ var eventsMethodNames = map[int]string{ } func (m *MessageProcessor) processEventsMethod(method int, rw http.ResponseWriter, _ *http.Request, window Window, params QueryParams) { - - var event CustomEvent - switch method { case EventsEmit: + var event CustomEvent err := params.ToStruct(&event) if err != nil { - m.httpError(rw, "Error parsing event: %s", err.Error()) + m.httpError(rw, "Invalid events call:", fmt.Errorf("error parsing event: %w", err)) return } if event.Name == "" { - m.httpError(rw, "Event name must be specified") + m.httpError(rw, "Invalid events call:", errors.New("missing event name")) return } + event.Sender = window.Name() - globalApplication.customEventProcessor.Emit(&event) + globalApplication.Event.EmitEvent(&event) + m.ok(rw) + m.Info("Runtime call:", "method", "Events."+eventsMethodNames[method], "name", event.Name, "sender", event.Sender, "data", event.Data, "cancelled", event.IsCancelled()) default: - m.httpError(rw, "Unknown event method: %d", method) + m.httpError(rw, "Invalid events call:", fmt.Errorf("unknown method: %d", method)) return } - - m.Info("Runtime Call:", "method", "Events."+eventsMethodNames[method], "name", event.Name, "sender", event.Sender, "data", event.Data, "cancelled", event.IsCancelled()) - } diff --git a/v3/pkg/application/messageprocessor_params.go b/v3/pkg/application/messageprocessor_params.go index b3030da43..b4f313a3a 100644 --- a/v3/pkg/application/messageprocessor_params.go +++ b/v3/pkg/application/messageprocessor_params.go @@ -141,6 +141,8 @@ func convertNumber[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 result = v case float64: result = T(v) + default: + return nil } return &result } @@ -154,6 +156,7 @@ func (a *Args) UInt8(s string) *uint8 { } return nil } + func (a *Args) UInt(s string) *uint { if a == nil { return nil @@ -169,8 +172,9 @@ func (a *Args) Float64(s string) *float64 { return nil } if val := a.data[s]; val != nil { - result := val.(float64) - return &result + if result, ok := val.(float64); ok { + return &result + } } return nil } @@ -180,8 +184,9 @@ func (a *Args) Bool(s string) *bool { return nil } if val := a.data[s]; val != nil { - result := val.(bool) - return &result + if result, ok := val.(bool); ok { + return &result + } } return nil } diff --git a/v3/pkg/application/messageprocessor_screens.go b/v3/pkg/application/messageprocessor_screens.go index 7339ebd35..d90487d59 100644 --- a/v3/pkg/application/messageprocessor_screens.go +++ b/v3/pkg/application/messageprocessor_screens.go @@ -1,6 +1,7 @@ package application import ( + "fmt" "net/http" ) @@ -17,33 +18,25 @@ var screensMethodNames = map[int]string{ } func (m *MessageProcessor) processScreensMethod(method int, rw http.ResponseWriter, _ *http.Request, _ Window, _ QueryParams) { - switch method { case ScreensGetAll: - screens, err := globalApplication.GetScreens() - if err != nil { - m.Error("GetAll: %s", err.Error()) - return - } + screens := globalApplication.Screen.GetAll() m.json(rw, screens) case ScreensGetPrimary: - screen, err := globalApplication.GetPrimaryScreen() - if err != nil { - m.Error("GetPrimary: %s", err.Error()) - return - } + screen := globalApplication.Screen.GetPrimary() m.json(rw, screen) case ScreensGetCurrent: - screen, err := globalApplication.CurrentWindow().GetScreen() + screen, err := globalApplication.Window.Current().GetScreen() if err != nil { - m.Error("GetCurrent: %s", err.Error()) + m.httpError(rw, "Window.GetScreen failed:", err) return } m.json(rw, screen) default: - m.httpError(rw, "Unknown screens method: %d", method) + m.httpError(rw, "Invalid screens call:", fmt.Errorf("unknown method: %d", method)) + return } - m.Info("Runtime Call:", "method", "Screens."+screensMethodNames[method]) + m.Info("Runtime call:", "method", "Screens."+screensMethodNames[method]) } diff --git a/v3/pkg/application/messageprocessor_system.go b/v3/pkg/application/messageprocessor_system.go index 6375176e0..e1a1e771c 100644 --- a/v3/pkg/application/messageprocessor_system.go +++ b/v3/pkg/application/messageprocessor_system.go @@ -1,6 +1,7 @@ package application import ( + "fmt" "net/http" ) @@ -15,16 +16,15 @@ var systemMethodNames = map[int]string{ } func (m *MessageProcessor) processSystemMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) { - switch method { case SystemIsDarkMode: - m.json(rw, globalApplication.IsDarkMode()) + m.json(rw, globalApplication.Env.IsDarkMode()) case Environment: - m.json(rw, globalApplication.Environment()) + m.json(rw, globalApplication.Env.Info()) default: - m.httpError(rw, "Unknown system method: %d", method) + m.httpError(rw, "Invalid system call:", fmt.Errorf("unknown method: %d", method)) + return } - m.Info("Runtime Call:", "method", "System."+systemMethodNames[method]) - + m.Info("Runtime call:", "method", "System."+systemMethodNames[method]) } diff --git a/v3/pkg/application/messageprocessor_window.go b/v3/pkg/application/messageprocessor_window.go index 584fe22f6..6b9f2a9b2 100644 --- a/v3/pkg/application/messageprocessor_window.go +++ b/v3/pkg/application/messageprocessor_window.go @@ -1,6 +1,8 @@ package application import ( + "errors" + "fmt" "net/http" ) @@ -45,14 +47,15 @@ const ( WindowSize = 37 WindowToggleFullscreen = 38 WindowToggleMaximise = 39 - WindowUnFullscreen = 40 - WindowUnMaximise = 41 - WindowUnMinimise = 42 - WindowWidth = 43 - WindowZoom = 44 - WindowZoomIn = 45 - WindowZoomOut = 46 - WindowZoomReset = 47 + WindowToggleFrameless = 40 + WindowUnFullscreen = 41 + WindowUnMaximise = 42 + WindowUnMinimise = 43 + WindowWidth = 44 + WindowZoom = 45 + WindowZoomIn = 46 + WindowZoomOut = 47 + WindowZoomReset = 48 ) var windowMethodNames = map[int]string{ @@ -96,6 +99,7 @@ var windowMethodNames = map[int]string{ WindowSize: "Size", WindowToggleFullscreen: "ToggleFullscreen", WindowToggleMaximise: "ToggleMaximise", + WindowToggleFrameless: "ToggleFrameless", WindowUnFullscreen: "UnFullscreen", WindowUnMaximise: "UnMaximise", WindowUnMinimise: "UnMinimise", @@ -106,11 +110,16 @@ var windowMethodNames = map[int]string{ WindowZoomReset: "ZoomReset", } -func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWriter, _ *http.Request, window Window, params QueryParams) { - +func (m *MessageProcessor) processWindowMethod( + method int, + rw http.ResponseWriter, + _ *http.Request, + window Window, + params QueryParams, +) { args, err := params.Args() if err != nil { - m.httpError(rw, "Unable to parse arguments: %s", err.Error()) + m.httpError(rw, "Invalid window call:", fmt.Errorf("unable to parse arguments: %w", err)) return } @@ -145,7 +154,7 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite case WindowGetScreen: screen, err := window.GetScreen() if err != nil { - m.httpError(rw, err.Error()) + m.httpError(rw, "Window.GetScreen failed:", err) return } m.json(rw, screen) @@ -197,18 +206,24 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite case WindowSetPosition: x := args.Int("x") if x == nil { - m.Error("Invalid SetPosition Message: 'x' value required") + m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'x'")) + return } y := args.Int("y") if y == nil { - m.Error("Invalid SetPosition Message: 'y' value required") + m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'y'")) + return } window.SetPosition(*x, *y) m.ok(rw) case WindowSetAlwaysOnTop: alwaysOnTop := args.Bool("alwaysOnTop") if alwaysOnTop == nil { - m.Error("Invalid SetAlwaysOnTop Message: 'alwaysOnTop' value required") + m.httpError( + rw, + "Invalid window call:", + errors.New("missing or invalid argument 'alwaysOnTop'"), + ) return } window.SetAlwaysOnTop(*alwaysOnTop) @@ -216,22 +231,22 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite case WindowSetBackgroundColour: r := args.UInt8("r") if r == nil { - m.Error("Invalid SetBackgroundColour Message: 'r' value required") + m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'r'")) return } g := args.UInt8("g") if g == nil { - m.Error("Invalid SetBackgroundColour Message: 'g' value required") + m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'g'")) return } b := args.UInt8("b") if b == nil { - m.Error("Invalid SetBackgroundColour Message: 'b' value required") + m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'b'")) return } a := args.UInt8("a") if a == nil { - m.Error("Invalid SetBackgroundColour Message: 'a' value required") + m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'a'")) return } window.SetBackgroundColour(RGBA{ @@ -244,7 +259,11 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite case WindowSetFrameless: frameless := args.Bool("frameless") if frameless == nil { - m.Error("Invalid SetFrameless Message: 'frameless' value required") + m.httpError( + rw, + "Invalid window call:", + errors.New("missing or invalid argument 'frameless'"), + ) return } window.SetFrameless(*frameless) @@ -252,40 +271,66 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite case WindowSetMaxSize: width := args.Int("width") if width == nil { - m.Error("Invalid SetMaxSize Message: 'width' value required") + m.httpError( + rw, + "Invalid window call:", + errors.New("missing or invalid argument 'width'"), + ) + return } height := args.Int("height") if height == nil { - m.Error("Invalid SetMaxSize Message: 'height' value required") + m.httpError( + rw, + "Invalid window call:", + errors.New("missing or invalid argument 'height'"), + ) + return } window.SetMaxSize(*width, *height) m.ok(rw) case WindowSetMinSize: width := args.Int("width") if width == nil { - m.Error("Invalid SetMinSize Message: 'width' value required") + m.httpError( + rw, + "Invalid window call:", + errors.New("missing or invalid argument 'width'"), + ) + return } height := args.Int("height") if height == nil { - m.Error("Invalid SetMinSize Message: 'height' value required") + m.httpError( + rw, + "Invalid window call:", + errors.New("missing or invalid argument 'height'"), + ) + return } window.SetMinSize(*width, *height) m.ok(rw) case WindowSetRelativePosition: x := args.Int("x") if x == nil { - m.Error("Invalid SetRelativePosition Message: 'x' value required") + m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'x'")) + return } y := args.Int("y") if y == nil { - m.Error("Invalid SetRelativePosition Message: 'y' value required") + m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'y'")) + return } window.SetRelativePosition(*x, *y) m.ok(rw) case WindowSetResizable: resizable := args.Bool("resizable") if resizable == nil { - m.Error("Invalid SetResizable Message: 'resizable' value required") + m.httpError( + rw, + "Invalid window call:", + errors.New("missing or invalid argument 'resizable'"), + ) return } window.SetResizable(*resizable) @@ -293,18 +338,28 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite case WindowSetSize: width := args.Int("width") if width == nil { - m.Error("Invalid SetSize Message: 'width' value required") + m.httpError( + rw, + "Invalid window call:", + errors.New("missing or invalid argument 'width'"), + ) + return } height := args.Int("height") if height == nil { - m.Error("Invalid SetSize Message: 'height' value required") + m.httpError( + rw, + "Invalid window call:", + errors.New("missing or invalid argument 'height'"), + ) + return } window.SetSize(*width, *height) m.ok(rw) case WindowSetTitle: title := args.String("title") if title == nil { - m.Error("Invalid SetTitle Message: 'title' value required") + m.httpError(rw, "Invalid window call:", errors.New("missing argument 'title'")) return } window.SetTitle(*title) @@ -312,7 +367,11 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite case WindowSetZoom: zoom := args.Float64("zoom") if zoom == nil { - m.Error("Invalid SetZoom Message: 'zoom' value required") + m.httpError( + rw, + "Invalid window call:", + errors.New("missing or invalid argument 'zoom'"), + ) return } window.SetZoom(*zoom) @@ -335,6 +394,9 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite case WindowToggleMaximise: window.ToggleMaximise() m.ok(rw) + case WindowToggleFrameless: + window.ToggleFrameless() + m.ok(rw) case WindowUnFullscreen: window.UnFullscreen() m.ok(rw) @@ -360,8 +422,9 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite window.ZoomReset() m.ok(rw) default: - m.httpError(rw, "Unknown window method id: %d", method) + m.httpError(rw, "Invalid window call:", fmt.Errorf("unknown method %d", method)) + return } - m.Info("Runtime Call:", "method", "Window."+windowMethodNames[method]) + m.Info("Runtime call:", "method", "Window."+windowMethodNames[method]) } diff --git a/v3/pkg/application/panic_handler.go b/v3/pkg/application/panic_handler.go index f1c520b5a..53f42a309 100644 --- a/v3/pkg/application/panic_handler.go +++ b/v3/pkg/application/panic_handler.go @@ -73,10 +73,8 @@ func handlePanic(options ...handlePanicOptions) bool { } // Get the error - var err error - if errPanic, ok := e.(error); ok { - err = errPanic - } else { + err, ok := e.(error) + if !ok { err = fmt.Errorf("%v", e) } @@ -102,6 +100,5 @@ func processPanic(panicDetails *PanicDetails) { } func defaultPanicHandler(panicDetails *PanicDetails) { - errorMessage := fmt.Sprintf("panic error: %s\n%s", panicDetails.Error.Error(), panicDetails.StackTrace) - globalApplication.fatal(errorMessage) + globalApplication.fatal("panic error: %w\n%s", panicDetails.Error, panicDetails.StackTrace) } diff --git a/v3/pkg/application/popupmenu_windows.go b/v3/pkg/application/popupmenu_windows.go index 29bc9dafb..f69abeaa8 100644 --- a/v3/pkg/application/popupmenu_windows.go +++ b/v3/pkg/application/popupmenu_windows.go @@ -1,8 +1,8 @@ package application import ( - "fmt" "github.com/wailsapp/wails/v3/pkg/w32" + "unsafe" ) const ( @@ -61,6 +61,14 @@ func (p *Win32Menu) newMenu() w32.HMENU { func (p *Win32Menu) buildMenu(parentMenu w32.HMENU, inputMenu *Menu) { currentRadioGroup := RadioGroup{} for _, item := range inputMenu.items { + p.currentMenuID++ + itemID := p.currentMenuID + p.menuMapping[itemID] = item + + menuItemImpl := newMenuItemImpl(item, parentMenu, itemID) + menuItemImpl.parent = inputMenu + item.impl = menuItemImpl + if item.Hidden() { if item.accelerator != nil { if p.parentWindow != nil { @@ -68,17 +76,10 @@ func (p *Win32Menu) buildMenu(parentMenu w32.HMENU, inputMenu *Menu) { p.parentWindow.parent.removeMenuBinding(item.accelerator) } else { // Remove the global keybindings - globalApplication.removeKeyBinding(item.accelerator.String()) + globalApplication.KeyBinding.Remove(item.accelerator.String()) } } - continue } - p.currentMenuID++ - itemID := p.currentMenuID - p.menuMapping[itemID] = item - - menuItemImpl := newMenuItemImpl(item, parentMenu, itemID) - menuItemImpl.parent = inputMenu flags := uint32(w32.MF_STRING) if item.disabled { @@ -125,24 +126,28 @@ func (p *Win32Menu) buildMenu(parentMenu w32.HMENU, inputMenu *Menu) { if p.parentWindow != nil { p.parentWindow.parent.addMenuBinding(item.accelerator, item) } else { - globalApplication.addKeyBinding(item.accelerator.String(), func(w *WebviewWindow) { + globalApplication.KeyBinding.Add(item.accelerator.String(), func(w *WebviewWindow) { item.handleClick() }) } } } + + // If the item is hidden, don't append + if item.Hidden() { + continue + } + ok := w32.AppendMenu(parentMenu, flags, uintptr(itemID), w32.MustStringToUTF16Ptr(menuText)) if !ok { - globalApplication.fatal(fmt.Sprintf("Error adding menu item: %s", menuText)) + globalApplication.fatal("error adding menu item '%s'", menuText) } if item.bitmap != nil { err := w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil) if err != nil { - globalApplication.fatal(fmt.Sprintf("Error setting menu icons: %s", err.Error())) + globalApplication.fatal("error setting menu icons: %w", err) } } - - item.impl = menuItemImpl } if len(currentRadioGroup) > 0 { for _, radioMember := range currentRadioGroup { @@ -192,7 +197,26 @@ func (p *Win32Menu) ShowAt(x int, y int) { p.onMenuOpen() } - if !w32.TrackPopupMenuEx(p.menu, w32.TPM_LEFTALIGN, int32(x), int32(y-5), p.parent, nil) { + // Get screen dimensions to determine menu positioning + monitor := w32.MonitorFromWindow(p.parent, w32.MONITOR_DEFAULTTONEAREST) + var monitorInfo w32.MONITORINFO + monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo)) + if !w32.GetMonitorInfo(monitor, &monitorInfo) { + globalApplication.fatal("GetMonitorInfo failed") + } + + // Set flags to always position the menu above the cursor + menuFlags := uint32(w32.TPM_LEFTALIGN | w32.TPM_BOTTOMALIGN) + + // Check if we're close to the right edge of the screen + // If so, right-align the menu with some padding + if x > int(monitorInfo.RcWork.Right)-200 { // Assuming 200px as a reasonable menu width + menuFlags = uint32(w32.TPM_RIGHTALIGN | w32.TPM_BOTTOMALIGN) + // Add a small padding (10px) from the right edge + x = int(monitorInfo.RcWork.Right) - 10 + } + + if !w32.TrackPopupMenuEx(p.menu, menuFlags, int32(x), int32(y), p.parent, nil) { globalApplication.fatal("TrackPopupMenu failed") } diff --git a/v3/pkg/application/roles.go b/v3/pkg/application/roles.go index 74c49c393..78bbf2c38 100644 --- a/v3/pkg/application/roles.go +++ b/v3/pkg/application/roles.go @@ -156,7 +156,7 @@ func NewWindowMenu() *MenuItem { func NewHelpMenu() *MenuItem { menu := NewMenu() menu.Add("Learn More").OnClick(func(ctx *Context) { - globalApplication.CurrentWindow().SetURL("https://wails.io") + globalApplication.Window.Current().SetURL("https://wails.io") }) subMenu := NewSubMenuItem("Help") subMenu.submenu = menu diff --git a/v3/pkg/application/screen_windows.go b/v3/pkg/application/screen_windows.go index 45914badd..b6328f809 100644 --- a/v3/pkg/application/screen_windows.go +++ b/v3/pkg/application/screen_windows.go @@ -3,7 +3,7 @@ package application import ( - "fmt" + "errors" "strconv" "github.com/wailsapp/wails/v3/pkg/w32" @@ -50,7 +50,7 @@ func (m *windowsApp) processAndCacheScreens() error { }) } - err = m.parent.screenManager.LayoutScreens(screens) + err = m.parent.Screen.LayoutScreens(screens) if err != nil { return err } @@ -60,12 +60,12 @@ func (m *windowsApp) processAndCacheScreens() error { // NOTE: should be moved to *App after DPI is implemented in all platforms func (m *windowsApp) getScreens() ([]*Screen, error) { - return m.parent.screenManager.screens, nil + return m.parent.Screen.screens, nil } // NOTE: should be moved to *App after DPI is implemented in all platforms func (m *windowsApp) getPrimaryScreen() (*Screen, error) { - return m.parent.screenManager.primaryScreen, nil + return m.parent.Screen.primaryScreen, nil } func getScreenForWindow(window *windowsWebviewWindow) (*Screen, error) { @@ -75,12 +75,12 @@ func getScreenForWindow(window *windowsWebviewWindow) (*Screen, error) { func getScreenForWindowHwnd(hwnd w32.HWND) (*Screen, error) { hMonitor := w32.MonitorFromWindow(hwnd, w32.MONITOR_DEFAULTTONEAREST) screenID := hMonitorToScreenID(hMonitor) - for _, screen := range globalApplication.screenManager.screens { + for _, screen := range globalApplication.Screen.screens { if screen.ID == screenID { return screen, nil } } - return nil, fmt.Errorf("screen not found for window") + return nil, errors.New("screen not found for window") } func hMonitorToScreenID(hMonitor uintptr) string { diff --git a/v3/pkg/application/screenmanager.go b/v3/pkg/application/screenmanager.go index 70e2b0565..6caa1cd33 100644 --- a/v3/pkg/application/screenmanager.go +++ b/v3/pkg/application/screenmanager.go @@ -1,7 +1,7 @@ package application import ( - "fmt" + "errors" "math" "sort" ) @@ -10,10 +10,18 @@ import ( // Chromium License: https://chromium.googlesource.com/chromium/src/+/HEAD/LICENSE type ScreenManager struct { + app *App screens []*Screen primaryScreen *Screen } +// newScreenManager creates a new ScreenManager instance +func newScreenManager(app *App) *ScreenManager { + return &ScreenManager{ + app: app, + } +} + type Screen struct { ID string // A unique identifier for the display Name string // The name of the display @@ -363,7 +371,7 @@ func (s *Screen) physicalToDipRect(physicalRect Rect) Rect { // for future coordinate transformation between the physical and logical (DIP) space func (m *ScreenManager) LayoutScreens(screens []*Screen) error { if screens == nil || len(screens) == 0 { - return fmt.Errorf("screens parameter is nil or empty") + return errors.New("screens parameter is nil or empty") } m.screens = screens @@ -375,11 +383,11 @@ func (m *ScreenManager) LayoutScreens(screens []*Screen) error { return nil } -func (m *ScreenManager) Screens() []*Screen { +func (m *ScreenManager) GetAll() []*Screen { return m.screens } -func (m *ScreenManager) PrimaryScreen() *Screen { +func (m *ScreenManager) GetPrimary() *Screen { return m.primaryScreen } @@ -397,9 +405,9 @@ func (m *ScreenManager) calculateScreensDipCoordinates() error { } } if m.primaryScreen == nil { - return fmt.Errorf("no primary screen found") + return errors.New("no primary screen found") } else if len(remainingScreens) != len(m.screens)-1 { - return fmt.Errorf("invalid primary screen found") + return errors.New("invalid primary screen found") } // Build screens tree using the primary screen as root @@ -835,33 +843,33 @@ func (m *ScreenManager) ScreenNearestDipRect(dipRect Rect) *Screen { // Exported application-level methods for internal convenience and availability to application devs func DipToPhysicalPoint(dipPoint Point) Point { - return globalApplication.screenManager.DipToPhysicalPoint(dipPoint) + return globalApplication.Screen.DipToPhysicalPoint(dipPoint) } func PhysicalToDipPoint(physicalPoint Point) Point { - return globalApplication.screenManager.PhysicalToDipPoint(physicalPoint) + return globalApplication.Screen.PhysicalToDipPoint(physicalPoint) } func DipToPhysicalRect(dipRect Rect) Rect { - return globalApplication.screenManager.DipToPhysicalRect(dipRect) + return globalApplication.Screen.DipToPhysicalRect(dipRect) } func PhysicalToDipRect(physicalRect Rect) Rect { - return globalApplication.screenManager.PhysicalToDipRect(physicalRect) + return globalApplication.Screen.PhysicalToDipRect(physicalRect) } func ScreenNearestPhysicalPoint(physicalPoint Point) *Screen { - return globalApplication.screenManager.ScreenNearestPhysicalPoint(physicalPoint) + return globalApplication.Screen.ScreenNearestPhysicalPoint(physicalPoint) } func ScreenNearestDipPoint(dipPoint Point) *Screen { - return globalApplication.screenManager.ScreenNearestDipPoint(dipPoint) + return globalApplication.Screen.ScreenNearestDipPoint(dipPoint) } func ScreenNearestPhysicalRect(physicalRect Rect) *Screen { - return globalApplication.screenManager.ScreenNearestPhysicalRect(physicalRect) + return globalApplication.Screen.ScreenNearestPhysicalRect(physicalRect) } func ScreenNearestDipRect(dipRect Rect) *Screen { - return globalApplication.screenManager.ScreenNearestDipRect(dipRect) + return globalApplication.Screen.ScreenNearestDipRect(dipRect) } diff --git a/v3/pkg/application/screenmanager_test.go b/v3/pkg/application/screenmanager_test.go index a7f182901..1e58e3fd1 100644 --- a/v3/pkg/application/screenmanager_test.go +++ b/v3/pkg/application/screenmanager_test.go @@ -305,7 +305,7 @@ func TestScreenManager_ScreensLayout(t *testing.T) { err := sm.LayoutScreens(layout.screens) is.NoErr(err) - screens := sm.Screens() + screens := sm.GetAll() is.Equal(len(screens), 2) // 2 screens is.Equal(screens[0].PhysicalBounds, application.Rect{X: 0, Y: 0, Width: 1200, Height: 1200}) // Parent physical bounds is.Equal(screens[0].Bounds, screens[0].PhysicalBounds) // Parent no scaling @@ -324,7 +324,7 @@ func TestScreenManager_ScreensLayout(t *testing.T) { err := sm.LayoutScreens(layout.screens) is.NoErr(err) - screens := sm.Screens() + screens := sm.GetAll() is.Equal(len(screens), 2) // 2 screens is.Equal(screens[0].PhysicalBounds, application.Rect{X: 0, Y: 0, Width: 1200, Height: 1200}) // Parent physical bounds is.Equal(screens[0].Bounds, application.Rect{X: 0, Y: 0, Width: 800, Height: 800}) // Parent DIP bounds @@ -376,7 +376,7 @@ func TestScreenManager_PrimaryScreen(t *testing.T) { for _, layout := range exampleLayouts() { err := sm.LayoutScreens(layout.screens) is.NoErr(err) - is.Equal(sm.PrimaryScreen(), layout.screens[0]) // Primary screen + is.Equal(sm.GetPrimary(), layout.screens[0]) // Primary screen } layout := parseLayout(ScreensLayout{screens: []ScreenDef{ @@ -387,7 +387,7 @@ func TestScreenManager_PrimaryScreen(t *testing.T) { layout.screens[0], layout.screens[1] = layout.screens[1], layout.screens[0] err := sm.LayoutScreens(layout.screens) is.NoErr(err) - is.Equal(sm.PrimaryScreen(), layout.screens[1]) // Primary screen + is.Equal(sm.GetPrimary(), layout.screens[1]) // Primary screen layout.screens[1].IsPrimary = false err = sm.LayoutScreens(layout.screens) @@ -403,7 +403,7 @@ func TestScreenManager_EdgeAlign(t *testing.T) { for _, layout := range exampleLayouts() { err := sm.LayoutScreens(layout.screens) is.NoErr(err) - for _, screen := range sm.Screens() { + for _, screen := range sm.GetAll() { ptOriginDip := screen.Bounds.Origin() ptOriginPhysical := screen.PhysicalBounds.Origin() ptCornerDip := screen.Bounds.InsideCorner() @@ -436,7 +436,7 @@ func TestScreenManager_ProbePoints(t *testing.T) { for _, layout := range exampleLayouts() { err := sm.LayoutScreens(layout.screens) is.NoErr(err) - for _, screen := range sm.Screens() { + for _, screen := range sm.GetAll() { for i := 0; i <= 1; i++ { isDip := (i == 0) @@ -500,7 +500,7 @@ func TestScreenManager_TransformationDrift(t *testing.T) { for _, layout := range exampleLayouts() { err := sm.LayoutScreens(layout.screens) is.NoErr(err) - for _, screen := range sm.Screens() { + for _, screen := range sm.GetAll() { rectPhysicalOriginal := application.Rect{ X: screen.PhysicalBounds.X + 100, Y: screen.PhysicalBounds.Y + 100, diff --git a/v3/pkg/application/services.go b/v3/pkg/application/services.go index 02ac6f049..582d135b0 100644 --- a/v3/pkg/application/services.go +++ b/v3/pkg/application/services.go @@ -27,6 +27,16 @@ type ServiceOptions struct { // it will be mounted on the internal asset server // at the prefix specified by Route. Route string + + // MarshalError will be called if non-nil + // to marshal to JSON the error values returned by this service's methods. + // + // MarshalError is not allowed to fail, + // but it may return a nil slice to fall back + // to the globally configured error handler. + // + // If the returned slice is not nil, it must contain valid JSON. + MarshalError func(error) []byte } // DefaultServiceOptions specifies the default values of service options, @@ -72,8 +82,17 @@ type ServiceName interface { // The context will be valid as long as the application is running, // and will be canceled right before shutdown. // -// If the return value is non-nil, it is logged along with the service name, -// the startup process aborts and the application quits. +// Services are guaranteed to receive the startup notification +// in the exact order in which they were either +// listed in [Options.Services] or registered with [App.RegisterService], +// with those from [Options.Services] coming first. +// +// If the return value is non-nil, the startup process aborts +// and [App.Run] returns the error wrapped with [fmt.Errorf] +// in a user-friendly message comprising the service name. +// The original error can be retrieved either by calling the Unwrap method +// or through the [errors.As] API. +// // When that happens, service instances that have been already initialised // receive a shutdown notification. type ServiceStartup interface { @@ -83,17 +102,33 @@ type ServiceStartup interface { // ServiceShutdown is an *optional* method that may be implemented by service instances. // // This method will be called during application shutdown. It can be used for cleaning up resources. +// If a service has received a startup notification, +// then it is guaranteed to receive a shutdown notification too, +// except in case of unhandled panics during shutdown. // -// If the return value is non-nil, it is logged along with the service name. +// Services receive shutdown notifications in reverse registration order, +// after all user-provided shutdown hooks have run (see [App.OnShutdown]). +// +// If the return value is non-nil, it is passed to the application's +// configured error handler at [Options.ErrorHandler], +// wrapped with [fmt.Errorf] in a user-friendly message comprising the service name. +// The default behaviour is to log the error along with the service name. +// The original error can be retrieved either by calling the Unwrap method +// or through the [errors.As] API. type ServiceShutdown interface { ServiceShutdown() error } -func getServiceName(service any) string { - // First check it conforms to ServiceName interface - if serviceName, ok := service.(ServiceName); ok { - return serviceName.ServiceName() +func getServiceName(service Service) string { + if service.options.Name != "" { + return service.options.Name } - // Next, get the name from the type - return reflect.TypeOf(service).String() + + // Check if the service implements the ServiceName interface + if s, ok := service.Instance().(ServiceName); ok { + return s.ServiceName() + } + + // Finally, get the name from the type. + return reflect.TypeOf(service.Instance()).Elem().String() } diff --git a/v3/pkg/application/single_instance_linux.go b/v3/pkg/application/single_instance_linux.go index c2a306eeb..28c9e5483 100644 --- a/v3/pkg/application/single_instance_linux.go +++ b/v3/pkg/application/single_instance_linux.go @@ -3,12 +3,13 @@ package application import ( - "fmt" - "github.com/godbus/dbus/v5" + "errors" "os" "strings" "sync" "syscall" + + "github.com/godbus/dbus/v5" ) type dbusHandler func(string) @@ -36,7 +37,7 @@ func newPlatformLock(manager *singleInstanceManager) (platformLock, error) { func (l *linuxLock) acquire(uniqueID string) error { if uniqueID == "" { - return fmt.Errorf("UniqueID is required for single instance lock") + return errors.New("UniqueID is required for single instance lock") } id := "wails_app_" + strings.ReplaceAll(strings.ReplaceAll(uniqueID, "-", "_"), ".", "_") @@ -56,11 +57,11 @@ func (l *linuxLock) acquire(uniqueID string) error { secondInstanceBuffer <- message }) - err := conn.Export(f, dbus.ObjectPath(l.dbusPath), l.dbusName) - if err != nil { - globalApplication.error(err.Error()) - } + err = conn.Export(f, dbus.ObjectPath(l.dbusPath), l.dbusName) }) + if err != nil { + return err + } reply, err := conn.RequestName(l.dbusName, dbus.NameFlagDoNotQueue) if err != nil { diff --git a/v3/pkg/application/single_instance_windows.go b/v3/pkg/application/single_instance_windows.go index 9f92b4eb7..b92b2749a 100644 --- a/v3/pkg/application/single_instance_windows.go +++ b/v3/pkg/application/single_instance_windows.go @@ -4,11 +4,11 @@ package application import ( "errors" - "fmt" - "github.com/wailsapp/wails/v3/pkg/w32" - "golang.org/x/sys/windows" "syscall" "unsafe" + + "github.com/wailsapp/wails/v3/pkg/w32" + "golang.org/x/sys/windows" ) var ( @@ -33,7 +33,7 @@ func newPlatformLock(manager *singleInstanceManager) (platformLock, error) { func (l *windowsLock) acquire(uniqueID string) error { if uniqueID == "" { - return fmt.Errorf("UniqueID is required for single instance lock") + return errors.New("UniqueID is required for single instance lock") } l.uniqueID = uniqueID diff --git a/v3/pkg/application/system_tray_manager.go b/v3/pkg/application/system_tray_manager.go new file mode 100644 index 000000000..16bdf5153 --- /dev/null +++ b/v3/pkg/application/system_tray_manager.go @@ -0,0 +1,44 @@ +package application + +// SystemTrayManager manages system tray-related operations +type SystemTrayManager struct { + app *App +} + +// newSystemTrayManager creates a new SystemTrayManager instance +func newSystemTrayManager(app *App) *SystemTrayManager { + return &SystemTrayManager{ + app: app, + } +} + +// New creates a new system tray +func (stm *SystemTrayManager) New() *SystemTray { + id := stm.getNextID() + newSystemTray := newSystemTray(id) + + stm.app.systemTraysLock.Lock() + stm.app.systemTrays[id] = newSystemTray + stm.app.systemTraysLock.Unlock() + + stm.app.runOrDeferToAppRun(newSystemTray) + + return newSystemTray +} + +// getNextID generates the next system tray ID (internal use) +func (stm *SystemTrayManager) getNextID() uint { + stm.app.systemTrayIDLock.Lock() + defer stm.app.systemTrayIDLock.Unlock() + stm.app.systemTrayID++ + return stm.app.systemTrayID +} + +// destroy destroys a system tray (internal use) +func (stm *SystemTrayManager) destroy(tray *SystemTray) { + // Remove the system tray from the app.systemTrays map + stm.app.systemTraysLock.Lock() + delete(stm.app.systemTrays, tray.id) + stm.app.systemTraysLock.Unlock() + tray.destroy() +} diff --git a/v3/pkg/application/systemtray.go b/v3/pkg/application/systemtray.go index f3daa1e51..beb61d0d9 100644 --- a/v3/pkg/application/systemtray.go +++ b/v3/pkg/application/systemtray.go @@ -1,7 +1,7 @@ package application import ( - "fmt" + "errors" "runtime" "sync" "time" @@ -123,7 +123,7 @@ func (s *SystemTray) Run() { func (s *SystemTray) PositionWindow(window *WebviewWindow, offset int) error { if s.impl == nil { - return fmt.Errorf("system tray not running") + return errors.New("system tray not running") } return InvokeSyncWithError(func() error { return s.impl.positionWindow(window, offset) @@ -197,7 +197,7 @@ func (s *SystemTray) SetTooltip(tooltip string) { } func (s *SystemTray) Destroy() { - globalApplication.destroySystemTray(s) + globalApplication.SystemTray.destroy(s) } func (s *SystemTray) destroy() { diff --git a/v3/pkg/application/systemtray_darwin.go b/v3/pkg/application/systemtray_darwin.go index 677abbbce..452ce9aba 100644 --- a/v3/pkg/application/systemtray_darwin.go +++ b/v3/pkg/application/systemtray_darwin.go @@ -30,9 +30,9 @@ static void systemTrayHide(void* nsStatusItem) { */ import "C" import ( + "errors" "unsafe" - "fmt" "github.com/leaanthony/go-ansi-parser" ) @@ -125,7 +125,7 @@ func (s *macosSystemTray) getScreen() (*Screen, error) { } return result, nil } - return nil, fmt.Errorf("no screen available") + return nil, errors.New("no screen available") } func (s *macosSystemTray) bounds() (*Rect, error) { diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index 0bf25b6a0..449753851 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -9,13 +9,14 @@ package application import "C" import ( "fmt" + "os" + "github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5/introspect" "github.com/godbus/dbus/v5/prop" "github.com/wailsapp/wails/v3/internal/dbus/menu" "github.com/wailsapp/wails/v3/internal/dbus/notifier" "github.com/wailsapp/wails/v3/pkg/icons" - "os" ) const ( @@ -178,7 +179,7 @@ func (s *linuxSystemTray) refresh() { s.menuVersion++ if err := s.menuProps.Set("com.canonical.dbusmenu", "Version", dbus.MakeVariant(s.menuVersion)); err != nil { - globalApplication.error("systray error: failed to update menu version: %v", err) + globalApplication.error("systray error: failed to update menu version: %w", err) return } if err := menu.Emit(s.conn, &menu.Dbusmenu_LayoutUpdatedSignal{ @@ -187,7 +188,7 @@ func (s *linuxSystemTray) refresh() { Revision: s.menuVersion, }, }); err != nil { - globalApplication.error("systray error: failed to emit layout updated signal: %v", err) + globalApplication.error("systray error: failed to emit layout updated signal: %w", err) } } @@ -270,34 +271,34 @@ func (s *linuxSystemTray) bounds() (*Rect, error) { func (s *linuxSystemTray) run() { conn, err := dbus.SessionBus() if err != nil { - globalApplication.error("systray error: failed to connect to DBus: %v\n", err) + globalApplication.error("systray error: failed to connect to DBus: %w\n", err) return } err = notifier.ExportStatusNotifierItem(conn, itemPath, s) if err != nil { - globalApplication.error("systray error: failed to export status notifier item: %v\n", err) + globalApplication.error("systray error: failed to export status notifier item: %w\n", err) } err = menu.ExportDbusmenu(conn, menuPath, s) if err != nil { - globalApplication.error("systray error: failed to export status notifier menu: %v", err) + globalApplication.error("systray error: failed to export status notifier menu: %w", err) return } name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) // register id 1 for this process _, err = conn.RequestName(name, dbus.NameFlagDoNotQueue) if err != nil { - globalApplication.error("systray error: failed to request name: %s\n", err) + globalApplication.error("systray error: failed to request name: %w", err) // it's not critical error: continue } props, err := prop.Export(conn, itemPath, s.createPropSpec()) if err != nil { - globalApplication.error("systray error: failed to export notifier item properties to bus: %s\n", err) + globalApplication.error("systray error: failed to export notifier item properties to bus: %w", err) return } menuProps, err := prop.Export(conn, menuPath, s.createMenuPropSpec()) if err != nil { - globalApplication.error("systray error: failed to export notifier menu properties to bus: %s\n", err) + globalApplication.error("systray error: failed to export notifier menu properties to bus: %w", err) return } @@ -315,7 +316,7 @@ func (s *linuxSystemTray) run() { } err = conn.Export(introspect.NewIntrospectable(&node), itemPath, "org.freedesktop.DBus.Introspectable") if err != nil { - globalApplication.error("systray error: failed to export node introspection: %s\n", err) + globalApplication.error("systray error: failed to export node introspection: %w", err) return } menuNode := introspect.Node{ @@ -329,7 +330,7 @@ func (s *linuxSystemTray) run() { err = conn.Export(introspect.NewIntrospectable(&menuNode), menuPath, "org.freedesktop.DBus.Introspectable") if err != nil { - globalApplication.error("systray error: failed to export menu node introspection: %s\n", err) + globalApplication.error("systray error: failed to export menu node introspection: %w", err) return } s.setLabel(s.label) @@ -344,7 +345,7 @@ func (s *linuxSystemTray) run() { dbus.WithMatchMember("NameOwnerChanged"), dbus.WithMatchArg(0, "org.kde.StatusNotifierWatcher"), ); err != nil { - globalApplication.error("systray error: failed to register signal matching: %v\n", err) + globalApplication.error("systray error: failed to register signal matching: %w", err) return } @@ -388,7 +389,7 @@ func (s *linuxSystemTray) setIcon(icon []byte) { iconPx, err := iconToPX(icon) if err != nil { - globalApplication.error("systray error: failed to convert icon to PX: %s\n", err) + globalApplication.error("systray error: failed to convert icon to PX: %w", err) return } s.props.SetMust("org.kde.StatusNotifierItem", "IconPixmap", []PX{iconPx}) @@ -402,7 +403,7 @@ func (s *linuxSystemTray) setIcon(icon []byte) { Body: ¬ifier.StatusNotifierItem_NewIconSignalBody{}, }) if err != nil { - globalApplication.error("systray error: failed to emit new icon signal: %s\n", err) + globalApplication.error("systray error: failed to emit new icon signal: %w", err) return } } @@ -445,7 +446,7 @@ func (s *linuxSystemTray) setLabel(label string) { s.label = label if err := s.props.Set("org.kde.StatusNotifierItem", "Title", dbus.MakeVariant(label)); err != nil { - globalApplication.error("systray error: failed to set Title prop: %s\n", err) + globalApplication.error("systray error: failed to set Title prop: %w", err) return } @@ -457,7 +458,7 @@ func (s *linuxSystemTray) setLabel(label string) { Path: itemPath, Body: ¬ifier.StatusNotifierItem_NewTitleSignalBody{}, }); err != nil { - globalApplication.error("systray error: failed to emit new title signal: %s", err) + globalApplication.error("systray error: failed to emit new title signal: %w", err) return } @@ -591,7 +592,7 @@ func (s *linuxSystemTray) register() bool { obj := s.conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher") call := obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, itemPath) if call.Err != nil { - globalApplication.error("systray error: failed to register: %v\n", call.Err) + globalApplication.error("systray error: failed to register: %w", call.Err) return false } diff --git a/v3/pkg/application/systemtray_windows.go b/v3/pkg/application/systemtray_windows.go index 8b0943812..ca30cafce 100644 --- a/v3/pkg/application/systemtray_windows.go +++ b/v3/pkg/application/systemtray_windows.go @@ -3,12 +3,13 @@ package application import ( - "fmt" - "github.com/wailsapp/wails/v3/pkg/icons" + "errors" "syscall" "time" "unsafe" + "github.com/wailsapp/wails/v3/pkg/icons" + "github.com/samber/lo" "github.com/wailsapp/wails/v3/pkg/events" @@ -61,7 +62,8 @@ func (s *windowsSystemTray) positionWindow(window *WebviewWindow, offset int) er // systray icons in windows can either be in the taskbar // or in a flyout menu. - iconIsInTrayBounds, err := s.iconIsInTrayBounds() + var iconIsInTrayBounds bool + iconIsInTrayBounds, err = s.iconIsInTrayBounds() if err != nil { return err } @@ -120,7 +122,7 @@ func (s *windowsSystemTray) bounds() (*Rect, error) { monitor := w32.MonitorFromWindow(s.hwnd, w32.MONITOR_DEFAULTTONEAREST) if monitor == 0 { - return nil, fmt.Errorf("failed to get monitor") + return nil, errors.New("failed to get monitor") } return &Rect{ @@ -186,7 +188,7 @@ func (s *windowsSystemTray) run() { for retries := range 6 { if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) { if retries == 5 { - globalApplication.fatal("Failed to register system tray icon: %v", syscall.GetLastError()) + globalApplication.fatal("failed to register system tray icon: %w", syscall.GetLastError()) } time.Sleep(500 * time.Millisecond) @@ -211,11 +213,22 @@ func (s *windowsSystemTray) run() { s.darkModeIcon = lo.Must(w32.CreateSmallHIconFromImage(icons.SystrayDark)) } + // Use custom icons if provided if s.parent.icon != nil { - s.lightModeIcon = lo.Must(w32.CreateSmallHIconFromImage(s.parent.icon)) + // Create a new icon and destroy the old one + newIcon := lo.Must(w32.CreateSmallHIconFromImage(s.parent.icon)) + if s.lightModeIcon != 0 && s.lightModeIcon != defaultIcon { + w32.DestroyIcon(s.lightModeIcon) + } + s.lightModeIcon = newIcon } if s.parent.darkModeIcon != nil { - s.darkModeIcon = lo.Must(w32.CreateSmallHIconFromImage(s.parent.darkModeIcon)) + // Create a new icon and destroy the old one + newIcon := lo.Must(w32.CreateSmallHIconFromImage(s.parent.darkModeIcon)) + if s.darkModeIcon != 0 && s.darkModeIcon != defaultIcon && s.darkModeIcon != s.lightModeIcon { + w32.DestroyIcon(s.darkModeIcon) + } + s.darkModeIcon = newIcon } s.uid = nid.UID @@ -245,7 +258,7 @@ func (s *windowsSystemTray) run() { s.updateIcon() // Listen for dark mode changes - globalApplication.OnApplicationEvent(events.Windows.SystemThemeChanged, func(event *ApplicationEvent) { + globalApplication.Event.OnApplicationEvent(events.Windows.SystemThemeChanged, func(event *ApplicationEvent) { s.updateIcon() }) @@ -264,6 +277,9 @@ func (s *windowsSystemTray) updateIcon() { return } + // Store the old icon to destroy it after updating + oldIcon := s.currentIcon + s.currentIcon = newIcon nid := s.newNotifyIconData() nid.UFlags = w32.NIF_ICON @@ -274,6 +290,11 @@ func (s *windowsSystemTray) updateIcon() { if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) { panic(syscall.GetLastError()) } + + // Destroy the old icon handle if it exists and is not one of our default icons + if oldIcon != 0 && oldIcon != s.lightModeIcon && oldIcon != s.darkModeIcon { + w32.DestroyIcon(oldIcon) + } } func (s *windowsSystemTray) newNotifyIconData() w32.NOTIFYICONDATA { @@ -287,6 +308,10 @@ func (s *windowsSystemTray) newNotifyIconData() w32.NOTIFYICONDATA { func (s *windowsSystemTray) setIcon(icon []byte) { var err error + // Destroy the previous light mode icon if it exists + if s.lightModeIcon != 0 { + w32.DestroyIcon(s.lightModeIcon) + } s.lightModeIcon, err = w32.CreateSmallHIconFromImage(icon) if err != nil { panic(syscall.GetLastError()) @@ -300,6 +325,10 @@ func (s *windowsSystemTray) setIcon(icon []byte) { func (s *windowsSystemTray) setDarkModeIcon(icon []byte) { var err error + // Destroy the previous dark mode icon if it exists + if s.darkModeIcon != 0 { + w32.DestroyIcon(s.darkModeIcon) + } s.darkModeIcon, err = w32.CreateSmallHIconFromImage(icon) if err != nil { panic(syscall.GetLastError()) @@ -421,6 +450,17 @@ func (s *windowsSystemTray) destroy() { if !w32.ShellNotifyIcon(w32.NIM_DELETE, &nid) { globalApplication.debug(syscall.GetLastError().Error()) } + + // Clean up icon handles + if s.lightModeIcon != 0 { + w32.DestroyIcon(s.lightModeIcon) + s.lightModeIcon = 0 + } + if s.darkModeIcon != 0 && s.darkModeIcon != s.lightModeIcon { + w32.DestroyIcon(s.darkModeIcon) + s.darkModeIcon = 0 + } + s.currentIcon = 0 } func (s *windowsSystemTray) Show() { @@ -430,3 +470,19 @@ func (s *windowsSystemTray) Show() { func (s *windowsSystemTray) Hide() { // No-op } + +func (s *windowsSystemTray) reshow() { + // Add icons back to systray + nid := w32.NOTIFYICONDATA{ + HWnd: s.hwnd, + UID: uint32(s.parent.id), + UFlags: w32.NIF_ICON | w32.NIF_MESSAGE, + HIcon: s.currentIcon, + UCallbackMessage: WM_USER_SYSTRAY, + } + nid.CbSize = uint32(unsafe.Sizeof(nid)) + // Show the icon + if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) { + panic(syscall.GetLastError()) + } +} diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 1a37643bb..67d125074 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -1,13 +1,14 @@ package application import ( - "encoding/json" "errors" "fmt" "runtime" "slices" "strings" "sync" + "sync/atomic" + "text/template" "github.com/leaanthony/u" @@ -108,6 +109,7 @@ type ( showMenuBar() hideMenuBar() toggleMenuBar() + setMenu(menu *Menu) } ) @@ -164,13 +166,29 @@ type WebviewWindow struct { // pendingJS holds JS that was sent to the window before the runtime was loaded pendingJS []string - // unconditionallyClose marks the window to be unconditionally closed - unconditionallyClose bool + // unconditionallyClose marks the window to be unconditionally closed (atomic) + unconditionallyClose uint32 +} + +func (w *WebviewWindow) SetMenu(menu *Menu) { + switch runtime.GOOS { + case "darwin": + return + case "windows": + w.options.Windows.Menu = menu + case "linux": + w.options.Linux.Menu = menu + } + if w.impl != nil { + InvokeSync(func() { + w.impl.setMenu(menu) + }) + } } // EmitEvent emits an event from the window func (w *WebviewWindow) EmitEvent(name string, data ...any) { - globalApplication.emitEvent(&CustomEvent{ + globalApplication.Event.EmitEvent(&CustomEvent{ Name: name, Data: data, Sender: w.Name(), @@ -191,7 +209,7 @@ func getWindowID() uint { // Use onApplicationEvent to register a callback for an application event from a window. // This will handle tidying up the callback when the window is destroyed func (w *WebviewWindow) onApplicationEvent(eventType events.ApplicationEventType, callback func(*ApplicationEvent)) { - cancelFn := globalApplication.OnApplicationEvent(eventType, callback) + cancelFn := globalApplication.Event.OnApplicationEvent(eventType, callback) w.addCancellationFunction(cancelFn) } @@ -256,10 +274,10 @@ func NewWindow(options WebviewWindowOptions) *WebviewWindow { // Listen for window closing events and de result.OnWindowEvent(events.Common.WindowClosing, func(event *WindowEvent) { - result.unconditionallyClose = true + atomic.StoreUint32(&result.unconditionallyClose, 1) InvokeSync(result.markAsDestroyed) InvokeSync(result.impl.close) - globalApplication.deleteWindowByID(result.id) + globalApplication.Window.Remove(result.id) }) // Process keybindings @@ -270,13 +288,15 @@ func NewWindow(options WebviewWindowOptions) *WebviewWindow { return result } -func processKeyBindingOptions(keyBindings map[string]func(window *WebviewWindow)) map[string]func(window *WebviewWindow) { +func processKeyBindingOptions( + keyBindings map[string]func(window *WebviewWindow), +) map[string]func(window *WebviewWindow) { result := make(map[string]func(window *WebviewWindow)) for key, callback := range keyBindings { // Parse the key to an accelerator acc, err := parseAccelerator(key) if err != nil { - globalApplication.error("Invalid keybinding: %s", err.Error()) + globalApplication.error("invalid keybinding: %w", err) continue } result[acc.String()] = callback @@ -291,40 +311,53 @@ func (w *WebviewWindow) addCancellationFunction(canceller func()) { w.cancellers = append(w.cancellers, canceller) } -// formatJS ensures the 'data' provided marshals to valid json or panics -func (w *WebviewWindow) formatJS(f string, callID string, data string) string { - j, err := json.Marshal(data) - if err != nil { - panic(err) - } - return fmt.Sprintf(f, callID, j) -} - -func (w *WebviewWindow) CallError(callID string, result string) { +func (w *WebviewWindow) CallError(callID string, result string, isJSON bool) { if w.impl != nil { - w.impl.execJS(w.formatJS("_wails.callErrorHandler('%s', %s);", callID, result)) + w.impl.execJS( + fmt.Sprintf( + "_wails.callErrorHandler('%s', '%s', %t);", + callID, + template.JSEscapeString(result), + isJSON, + ), + ) } } func (w *WebviewWindow) CallResponse(callID string, result string) { if w.impl != nil { - w.impl.execJS(w.formatJS("_wails.callResultHandler('%s', %s, true);", callID, result)) + w.impl.execJS( + fmt.Sprintf( + "_wails.callResultHandler('%s', '%s', true);", + callID, + template.JSEscapeString(result), + ), + ) } } func (w *WebviewWindow) DialogError(dialogID string, result string) { if w.impl != nil { - w.impl.execJS(w.formatJS("_wails.dialogErrorCallback('%s', %s);", dialogID, result)) + w.impl.execJS( + fmt.Sprintf( + "_wails.dialogErrorCallback('%s', '%s');", + dialogID, + template.JSEscapeString(result), + ), + ) } } func (w *WebviewWindow) DialogResponse(dialogID string, result string, isJSON bool) { if w.impl != nil { - if isJSON { - w.impl.execJS(w.formatJS("_wails.dialogResultCallback('%s', %s, true);", dialogID, result)) - } else { - w.impl.execJS(fmt.Sprintf("_wails.dialogResultCallback('%s', '%s', false);", dialogID, result)) - } + w.impl.execJS( + fmt.Sprintf( + "_wails.dialogResultCallback('%s', '%s', %t);", + dialogID, + template.JSEscapeString(result), + isJSON, + ), + ) } } @@ -690,7 +723,7 @@ func (w *WebviewWindow) HandleMessage(message string) { InvokeSync(func() { err := w.startDrag() if err != nil { - w.Error("Failed to start drag: %s", err) + w.Error("failed to start drag: %w", err) } }) } @@ -698,12 +731,12 @@ func (w *WebviewWindow) HandleMessage(message string) { if !w.IsFullscreen() { sl := strings.Split(message, ":") if len(sl) != 3 { - w.Error("Unknown message returned from dispatcher", "message", message) + w.Error("unknown message returned from dispatcher: %s", message) return } err := w.startResize(sl[2]) if err != nil { - w.Error(err.Error()) + w.Error("%w", err) } } case message == "wails:runtime:ready": @@ -714,7 +747,7 @@ func (w *WebviewWindow) HandleMessage(message string) { w.ExecJS(js) } default: - w.Error("Unknown message sent via 'invoke' on frontend: %v", message) + w.Error("unknown message sent via 'invoke' on frontend: %v", message) } } @@ -737,7 +770,10 @@ func (w *WebviewWindow) Center() { } // OnWindowEvent registers a callback for the given window event -func (w *WebviewWindow) OnWindowEvent(eventType events.WindowEventType, callback func(event *WindowEvent)) func() { +func (w *WebviewWindow) OnWindowEvent( + eventType events.WindowEventType, + callback func(event *WindowEvent), +) func() { eventID := uint(eventType) windowEventListener := &WindowEventListener{ callback: callback, @@ -758,7 +794,10 @@ func (w *WebviewWindow) OnWindowEvent(eventType events.WindowEventType, callback } // RegisterHook registers a hook for the given window event -func (w *WebviewWindow) RegisterHook(eventType events.WindowEventType, callback func(event *WindowEvent)) func() { +func (w *WebviewWindow) RegisterHook( + eventType events.WindowEventType, + callback func(event *WindowEvent), +) func() { eventID := uint(eventType) w.eventHooksLock.Lock() defer w.eventHooksLock.Unlock() @@ -967,6 +1006,16 @@ func (w *WebviewWindow) ToggleMaximise() { }) } +// ToggleFrameless toggles the window between frameless and normal +func (w *WebviewWindow) ToggleFrameless() { + if w.impl == nil || w.isDestroyed() { + return + } + InvokeSync(func() { + w.SetFrameless(!w.options.Frameless) + }) +} + func (w *WebviewWindow) OpenDevTools() { if w.impl == nil || w.isDestroyed() { return @@ -1162,10 +1211,8 @@ func (w *WebviewWindow) Info(message string, args ...any) { } func (w *WebviewWindow) Error(message string, args ...any) { - var messageArgs []interface{} - messageArgs = append(messageArgs, args...) - messageArgs = append(messageArgs, "sender", w.Name()) - globalApplication.error(message, messageArgs...) + args = append([]any{w.Name()}, args...) + globalApplication.error("in window '%s': "+message, args...) } func (w *WebviewWindow) HandleDragAndDropMessage(filenames []string) { @@ -1180,9 +1227,9 @@ func (w *WebviewWindow) HandleDragAndDropMessage(filenames []string) { func (w *WebviewWindow) OpenContextMenu(data *ContextMenuData) { // try application level context menu - menu, ok := globalApplication.getContextMenu(data.Id) + menu, ok := globalApplication.ContextMenu.Get(data.Id) if !ok { - w.Error("No context menu found for id: %s", data.Id) + w.Error("no context menu found for id: %s", data.Id) return } menu.setContextData(data) @@ -1261,7 +1308,7 @@ func (w *WebviewWindow) processKeyBinding(acceleratorString string) bool { } } - return globalApplication.processKeyBinding(acceleratorString, w) + return globalApplication.KeyBinding.Process(acceleratorString, w) } func (w *WebviewWindow) HandleKeyEvent(acceleratorString string) { diff --git a/v3/pkg/application/webview_window_close_darwin.go b/v3/pkg/application/webview_window_close_darwin.go new file mode 100644 index 000000000..d09833e93 --- /dev/null +++ b/v3/pkg/application/webview_window_close_darwin.go @@ -0,0 +1,26 @@ +//go:build darwin + +package application + +/* +#include +*/ +import "C" +import "sync/atomic" + +//export windowShouldUnconditionallyClose +func windowShouldUnconditionallyClose(windowId C.uint) C.bool { + window, _ := globalApplication.Window.GetByID(uint(windowId)) + if window == nil { + globalApplication.debug("windowShouldUnconditionallyClose: window not found", "windowId", windowId) + return C.bool(false) + } + webviewWindow, ok := window.(*WebviewWindow) + if !ok { + globalApplication.debug("windowShouldUnconditionallyClose: window is not WebviewWindow", "windowId", windowId) + return C.bool(false) + } + unconditionallyClose := atomic.LoadUint32(&webviewWindow.unconditionallyClose) != 0 + globalApplication.debug("windowShouldUnconditionallyClose check", "windowId", windowId, "unconditionallyClose", unconditionallyClose) + return C.bool(unconditionallyClose) +} diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go index 2018ff098..eb3f83769 100644 --- a/v3/pkg/application/webview_window_darwin.go +++ b/v3/pkg/application/webview_window_darwin.go @@ -600,6 +600,7 @@ void windowSetShadow(void* nsWindow, bool hasShadow) { } + // windowClose closes the current window static void windowClose(void *window) { [(WebviewWindow*)window close]; @@ -815,6 +816,7 @@ static void setIgnoreMouseEvents(void *nsWindow, bool ignore) { import "C" import ( "sync" + "sync/atomic" "unsafe" "github.com/wailsapp/wails/v3/internal/assetserver" @@ -832,7 +834,7 @@ func (w *macosWebviewWindow) handleKeyEvent(acceleratorString string) { // Parse acceleratorString accelerator, err := parseAccelerator(acceleratorString) if err != nil { - globalApplication.error("unable to parse accelerator: %s", err.Error()) + globalApplication.error("unable to parse accelerator: %w", err) return } w.parent.processKeyBinding(accelerator.String()) @@ -907,6 +909,7 @@ func (w *macosWebviewWindow) show() { } func (w *macosWebviewWindow) hide() { + globalApplication.debug("Window hiding", "windowId", w.parent.id, "title", w.parent.options.Title) C.windowHide(w.nsWindow) } @@ -955,7 +958,11 @@ func (w *macosWebviewWindow) windowZoom() { } func (w *macosWebviewWindow) close() { + globalApplication.debug("Window close() called - setting unconditionallyClose flag", "windowId", w.parent.id, "title", w.parent.options.Title) + // Set the unconditionallyClose flag to allow the window to close + atomic.StoreUint32(&w.parent.unconditionallyClose, 1) C.windowClose(w.nsWindow) + globalApplication.debug("Window close() completed", "windowId", w.parent.id, "title", w.parent.options.Title) // TODO: Check if we need to unregister the window here or not } @@ -1038,10 +1045,14 @@ func (w *macosWebviewWindow) setEnabled(enabled bool) { func (w *macosWebviewWindow) execJS(js string) { InvokeAsync(func() { - if globalApplication.performingShutdown { + globalApplication.shutdownLock.Lock() + performingShutdown := globalApplication.performingShutdown + globalApplication.shutdownLock.Unlock() + + if performingShutdown { return } - if w.nsWindow == nil { + if w.nsWindow == nil || w.parent.isDestroyed() { return } C.windowExecJS(w.nsWindow, C.CString(js)) @@ -1264,7 +1275,7 @@ func (w *macosWebviewWindow) run() { startURL, err := assetserver.GetStartURL(options.URL) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } w.setURL(startURL) @@ -1423,6 +1434,7 @@ func (w *macosWebviewWindow) delete() { func (w *macosWebviewWindow) redo() { } -func (w *macosWebviewWindow) showMenuBar() {} -func (w *macosWebviewWindow) hideMenuBar() {} -func (w *macosWebviewWindow) toggleMenuBar() {} +func (w *macosWebviewWindow) showMenuBar() {} +func (w *macosWebviewWindow) hideMenuBar() {} +func (w *macosWebviewWindow) toggleMenuBar() {} +func (w *macosWebviewWindow) setMenu(_ *Menu) {} diff --git a/v3/pkg/application/webview_window_darwin.m b/v3/pkg/application/webview_window_darwin.m index 616d8f4a5..bdd5d8810 100644 --- a/v3/pkg/application/webview_window_darwin.m +++ b/v3/pkg/application/webview_window_darwin.m @@ -7,6 +7,7 @@ extern void processMessage(unsigned int, const char*); extern void processURLRequest(unsigned int, void *); extern void processWindowKeyDownEvent(unsigned int, const char*); extern bool hasListeners(unsigned int); +extern bool windowShouldUnconditionallyClose(unsigned int); @implementation WebviewWindow - (WebviewWindow*) initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)windowStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation; { @@ -231,6 +232,14 @@ extern bool hasListeners(unsigned int); @implementation WebviewWindowDelegate - (BOOL)windowShouldClose:(NSWindow *)sender { WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate]; + NSLog(@"[DEBUG] windowShouldClose called for window %d", delegate.windowId); + // Check if this window should close unconditionally (called from Close() method) + if (windowShouldUnconditionallyClose(delegate.windowId)) { + NSLog(@"[DEBUG] Window %d closing unconditionally (Close() method called)", delegate.windowId); + return true; + } + // For user-initiated closes, emit WindowClosing event and let the application decide + NSLog(@"[DEBUG] Window %d close requested by user - emitting WindowClosing event", delegate.windowId); processWindowEvent(delegate.windowId, EventWindowShouldClose); return false; } @@ -446,11 +455,13 @@ extern bool hasListeners(unsigned int); } } - (void)windowDidOrderOffScreen:(NSNotification *)notification { + NSLog(@"[DEBUG] Window %d ordered OFF screen (hidden)", self.windowId); if( hasListeners(EventWindowDidOrderOffScreen) ) { processWindowEvent(self.windowId, EventWindowDidOrderOffScreen); } } - (void)windowDidOrderOnScreen:(NSNotification *)notification { + NSLog(@"[DEBUG] Window %d ordered ON screen (shown)", self.windowId); if( hasListeners(EventWindowDidOrderOnScreen) ) { processWindowEvent(self.windowId, EventWindowDidOrderOnScreen); } @@ -526,6 +537,7 @@ extern bool hasListeners(unsigned int); } } - (void)windowWillClose:(NSNotification *)notification { + NSLog(@"[DEBUG] Window %d WILL close (window is actually closing)", self.windowId); if( hasListeners(EventWindowWillClose) ) { processWindowEvent(self.windowId, EventWindowWillClose); } @@ -571,11 +583,13 @@ extern bool hasListeners(unsigned int); } } - (void)windowWillOrderOffScreen:(NSNotification *)notification { + NSLog(@"[DEBUG] Window %d WILL order off screen (about to hide)", self.windowId); if( hasListeners(EventWindowWillOrderOffScreen) ) { processWindowEvent(self.windowId, EventWindowWillOrderOffScreen); } } - (void)windowWillOrderOnScreen:(NSNotification *)notification { + NSLog(@"[DEBUG] Window %d WILL order on screen (about to show)", self.windowId); if( hasListeners(EventWindowWillOrderOnScreen) ) { processWindowEvent(self.windowId, EventWindowWillOrderOnScreen); } diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 4baf079d3..acf8a47c9 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -235,6 +235,15 @@ func (w *linuxWebviewWindow) setPhysicalBounds(physicalBounds Rect) { w.setBounds(physicalBounds) } +func (w *linuxWebviewWindow) setMenu(menu *Menu) { + if menu == nil { + w.gtkmenu = nil + return + } + w.parent.options.Linux.Menu = menu + w.gtkmenu = (menu.impl).(*linuxMenu).native +} + func (w *linuxWebviewWindow) run() { for eventId := range w.parent.eventListeners { w.on(eventId) @@ -324,7 +333,7 @@ func (w *linuxWebviewWindow) run() { startURL, err := assetserver.GetStartURL(w.parent.options.URL) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } w.setURL(startURL) @@ -380,7 +389,7 @@ func (w *linuxWebviewWindow) handleKeyEvent(acceleratorString string) { // Parse acceleratorString // accelerator, err := parseAccelerator(acceleratorString) // if err != nil { - // globalApplication.error("unable to parse accelerator: %s", err.Error()) + // globalApplication.error("unable to parse accelerator: %w", err) // return // } w.parent.processKeyBinding(acceleratorString) @@ -403,6 +412,18 @@ func (w *linuxWebviewWindow) setIgnoreMouseEvents(ignore bool) { w.ignoreMouse(w.ignoreMouseEvents) } +func (w *linuxWebviewWindow) show() { + // Linux implementation is robust - window shows immediately + // This is the preferred pattern that Windows should follow + w.windowShow() +} + +func (w *linuxWebviewWindow) hide() { + // Save position before hiding (consistent with CGO implementation) + w.lastX, w.lastY = w.position() + w.windowHide() +} + func (w *linuxWebviewWindow) showMenuBar() {} func (w *linuxWebviewWindow) hideMenuBar() {} func (w *linuxWebviewWindow) toggleMenuBar() {} diff --git a/v3/pkg/application/webview_window_options.go b/v3/pkg/application/webview_window_options.go index 04dae199e..712adf4ad 100644 --- a/v3/pkg/application/webview_window_options.go +++ b/v3/pkg/application/webview_window_options.go @@ -95,10 +95,6 @@ type WebviewWindowOptions struct { // Y is the starting Y position of the window. Y int - // TransparentTitlebar will make the titlebar transparent. - // TODO: Move to mac window options - FullscreenButtonEnabled bool - // Hidden will hide the window when it is first created. Hidden bool @@ -163,6 +159,13 @@ func NewRGB(red, green, blue uint8) RGBA { } } +func NewRGBPtr(red, green, blue uint8) *uint32 { + result := uint32(red) + result |= uint32(green) << 8 + result |= uint32(blue) << 16 + return &result +} + type BackgroundType int const ( @@ -233,7 +236,7 @@ type WindowsWindow struct { // Specify custom colours to use for dark/light mode // Default: nil - CustomTheme *ThemeSettings + CustomTheme ThemeSettings // Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown. // "Rounded Corners" are only available on Windows 11. @@ -309,21 +312,56 @@ const ( Light Theme = 2 ) +type WindowTheme struct { + // BorderColour is the colour of the window border + BorderColour *uint32 + + // TitleBarColour is the colour of the window title bar + TitleBarColour *uint32 + + // TitleTextColour is the colour of the window title text + TitleTextColour *uint32 +} + +type TextTheme struct { + // Text is the colour of the text + Text *uint32 + + // Background is the background colour of the text + Background *uint32 +} + +type MenuBarTheme struct { + // Default is the default theme + Default *TextTheme + + // Hover defines the theme to use when the menu item is hovered + Hover *TextTheme + + // Selected defines the theme to use when the menu item is selected + Selected *TextTheme +} + // ThemeSettings defines custom colours to use in dark or light mode. // They may be set using the hex values: 0x00BBGGRR type ThemeSettings struct { - DarkModeTitleBar int32 - DarkModeTitleBarInactive int32 - DarkModeTitleText int32 - DarkModeTitleTextInactive int32 - DarkModeBorder int32 - DarkModeBorderInactive int32 - LightModeTitleBar int32 - LightModeTitleBarInactive int32 - LightModeTitleText int32 - LightModeTitleTextInactive int32 - LightModeBorder int32 - LightModeBorderInactive int32 + // Dark mode active window + DarkModeActive *WindowTheme + + // Dark mode inactive window + DarkModeInactive *WindowTheme + + // Light mode active window + LightModeActive *WindowTheme + + // Light mode inactive window + LightModeInactive *WindowTheme + + // Dark mode MenuBar + DarkModeMenuBar *MenuBarTheme + + // Light mode MenuBar + LightModeMenuBar *MenuBarTheme } /****** Mac Options *******/ diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 58b6303d3..a39696943 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -57,6 +57,11 @@ type windowsWebviewWindow struct { chromium *edge.Chromium webviewNavigationCompleted bool + // Window visibility management - robust fallback for issue #2861 + showRequested bool // Track if show() was called before navigation completed + visibilityTimeout *time.Timer // Timeout to show window if navigation is delayed + windowShown bool // Track if window container has been shown + // resizeBorder* is the width/height of the resize border in pixels. resizeBorderWidth int32 resizeBorderHeight int32 @@ -71,6 +76,36 @@ type windowsWebviewWindow struct { // isMinimizing indicates whether the window is currently being minimized // Used to prevent unnecessary redraws during minimize/restore operations isMinimizing bool + + // menubarTheme is the theme for the menubar + menubarTheme *w32.MenuBarTheme +} + +func (w *windowsWebviewWindow) setMenu(menu *Menu) { + menu.Update() + w.menu = NewApplicationMenu(w, menu) + w.menu.parentWindow = w + w32.SetMenu(w.hwnd, w.menu.menu) + + // Set menu background if theme is active + if w.menubarTheme != nil { + globalApplication.debug("Applying menubar theme in setMenu", "window", w.parent.id) + w.menubarTheme.SetMenuBackground(w.menu.menu) + w32.DrawMenuBar(w.hwnd) + // Force a repaint of the menu area + w32.InvalidateRect(w.hwnd, nil, true) + } else { + globalApplication.debug("No menubar theme to apply in setMenu", "window", w.parent.id) + } + + // Check if using translucent background with Mica - this makes menubars invisible + if w.parent.options.BackgroundType == BackgroundTypeTranslucent && + (w.parent.options.Windows.BackdropType == Mica || + w.parent.options.Windows.BackdropType == Acrylic || + w.parent.options.Windows.BackdropType == Tabbed) { + // Log warning about menubar visibility issue + globalApplication.debug("Warning: Menubars may be invisible when using translucent backgrounds with Mica/Acrylic/Tabbed effects", "window", w.parent.id) + } } func (w *windowsWebviewWindow) cut() { @@ -183,7 +218,7 @@ func (w *windowsWebviewWindow) print() error { func (w *windowsWebviewWindow) startResize(border string) error { if !w32.ReleaseCapture() { - return fmt.Errorf("unable to release mouse capture") + return errors.New("unable to release mouse capture") } // Use PostMessage because we don't want to block the caller until resizing has been finished. w32.PostMessage(w.hwnd, w32.WM_NCLBUTTONDOWN, edgeMap[border], 0) @@ -192,7 +227,7 @@ func (w *windowsWebviewWindow) startResize(border string) error { func (w *windowsWebviewWindow) startDrag() error { if !w32.ReleaseCapture() { - return fmt.Errorf("unable to release mouse capture") + return errors.New("unable to release mouse capture") } // Use PostMessage because we don't want to block the caller until dragging has been finished. w32.PostMessage(w.hwnd, w32.WM_NCLBUTTONDOWN, w32.HTCAPTION, 0) @@ -259,6 +294,10 @@ func (w *windowsWebviewWindow) run() { options := w.parent.options + // Initialize showRequested based on whether window should be hidden + // Non-hidden windows should be shown by default + w.showRequested = !options.Hidden + w.chromium = edge.NewChromium() if globalApplication.options.ErrorHandler != nil { w.chromium.SetErrorCallback(globalApplication.options.ErrorHandler) @@ -266,9 +305,12 @@ func (w *windowsWebviewWindow) run() { exStyle := w32.WS_EX_CONTROLPARENT if options.BackgroundType != BackgroundTypeSolid { - exStyle |= w32.WS_EX_NOREDIRECTIONBITMAP - if w.parent.options.IgnoreMouseEvents { + if (options.Frameless && options.BackgroundType == BackgroundTypeTransparent) || w.parent.options.IgnoreMouseEvents { + // Always if transparent and frameless exStyle |= w32.WS_EX_TRANSPARENT | w32.WS_EX_LAYERED + } else { + // Only WS_EX_NOREDIRECTIONBITMAP if not (and not solid) + exStyle |= w32.WS_EX_NOREDIRECTIONBITMAP } } if options.AlwaysOnTop { @@ -334,7 +376,7 @@ func (w *windowsWebviewWindow) run() { nil) if w.hwnd == 0 { - globalApplication.fatal("Unable to create window") + globalApplication.fatal("unable to create window") } // Ensure correct window size in case the scale factor of current screen is different from the initial one. @@ -403,7 +445,13 @@ func (w *windowsWebviewWindow) run() { // Process the theme switch options.Windows.Theme { case SystemDefault: - w.updateTheme(w32.IsCurrentlyDarkMode()) + isDark := w32.IsCurrentlyDarkMode() + if isDark { + w32.AllowDarkModeForWindow(w.hwnd, true) + } + w.updateTheme(isDark) + // Don't initialize default dark theme here if custom theme might be set + // The updateTheme call above will handle both default and custom themes w.parent.onApplicationEvent(events.Windows.SystemThemeChanged, func(*ApplicationEvent) { InvokeAsync(func() { w.updateTheme(w32.IsCurrentlyDarkMode()) @@ -412,7 +460,10 @@ func (w *windowsWebviewWindow) run() { case Light: w.updateTheme(false) case Dark: + w32.AllowDarkModeForWindow(w.hwnd, true) w.updateTheme(true) + // Don't initialize default dark theme here if custom theme might be set + // The updateTheme call above will handle custom themes } switch options.BackgroundType { @@ -679,6 +730,7 @@ func (w *windowsWebviewWindow) maximise() { func (w *windowsWebviewWindow) unmaximise() { w.restore() + w.parent.emit(events.Windows.WindowUnMaximise) } func (w *windowsWebviewWindow) restore() { @@ -718,7 +770,12 @@ func (w *windowsWebviewWindow) fullscreen() { int(monitorInfo.RcMonitor.Right-monitorInfo.RcMonitor.Left), int(monitorInfo.RcMonitor.Bottom-monitorInfo.RcMonitor.Top), w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED) + + // Hide the menubar in fullscreen mode + w32.SetMenu(w.hwnd, 0) + w.chromium.Focus() + w.parent.emit(events.Windows.WindowFullscreen) } func (w *windowsWebviewWindow) unfullscreen() { @@ -735,9 +792,16 @@ func (w *windowsWebviewWindow) unfullscreen() { w32.SetWindowLong(w.hwnd, w32.GWL_EXSTYLE, w.previousWindowExStyle) w32.SetWindowPlacement(w.hwnd, &w.previousWindowPlacement) w.isCurrentlyFullscreen = false + + // Restore the menubar when exiting fullscreen + if w.menu != nil { + w32.SetMenu(w.hwnd, w.menu.menu) + } + w32.SetWindowPos(w.hwnd, 0, 0, 0, 0, 0, w32.SWP_NOMOVE|w32.SWP_NOSIZE|w32.SWP_NOZORDER|w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED) w.enableSizeConstraints() + w.parent.emit(events.Windows.WindowUnFullscreen) } func (w *windowsWebviewWindow) isMinimised() bool { @@ -772,12 +836,16 @@ func (w *windowsWebviewWindow) isVisible() bool { return style&w32.WS_VISIBLE != 0 } -func (w *windowsWebviewWindow) setFullscreenButtonEnabled(_ bool) { - // Unused in Windows -} - func (w *windowsWebviewWindow) focus() { w32.SetForegroundWindow(w.hwnd) + + if w.isDisabled() { + return + } + if w.isMinimised() { + w.unminimise() + } + w.focusingChromium = true w.chromium.Focus() w.focusingChromium = false @@ -939,14 +1007,45 @@ func (w *windowsWebviewWindow) printStyle() { } func (w *windowsWebviewWindow) show() { + // Always show the window container immediately (decouple from WebView state) + // This fixes issue #2861 where efficiency mode prevents window visibility + w32.ShowWindow(w.hwnd, w32.SW_SHOW) + w.windowShown = true + w.showRequested = true + + // Show WebView if navigation has completed if w.webviewNavigationCompleted { w.chromium.Show() - w32.ShowWindow(w.hwnd, w32.SW_SHOW) + // Cancel timeout since we can show immediately + if w.visibilityTimeout != nil { + w.visibilityTimeout.Stop() + w.visibilityTimeout = nil + } + } else { + // Start timeout to show WebView if navigation is delayed (fallback for efficiency mode) + if w.visibilityTimeout == nil { + w.visibilityTimeout = time.AfterFunc(3*time.Second, func() { + // Show WebView even if navigation hasn't completed + // This prevents permanent invisibility in efficiency mode + if !w.webviewNavigationCompleted && w.chromium != nil { + w.chromium.Show() + } + w.visibilityTimeout = nil + }) + } } } func (w *windowsWebviewWindow) hide() { w32.ShowWindow(w.hwnd, w32.SW_HIDE) + w.windowShown = false + w.showRequested = false + + // Cancel any pending visibility timeout + if w.visibilityTimeout != nil { + w.visibilityTimeout.Stop() + w.visibilityTimeout = nil + } } // Get the screen for the current window @@ -969,6 +1068,10 @@ func newWindowImpl(parent *WebviewWindow) *windowsWebviewWindow { parent: parent, resizeBorderWidth: int32(w32.GetSystemMetrics(w32.SM_CXSIZEFRAME)), resizeBorderHeight: int32(w32.GetSystemMetrics(w32.SM_CYSIZEFRAME)), + // Initialize visibility tracking fields + showRequested: false, + visibilityTimeout: nil, + windowShown: false, } return result @@ -1009,7 +1112,7 @@ func (w *windowsWebviewWindow) setBackdropType(backdropType BackdropType) { w32.SetWindowCompositionAttribute(w.hwnd, &data) } else { - w32.EnableTranslucency(w.hwnd, int32(backdropType)) + w32.EnableTranslucency(w.hwnd, uint32(backdropType)) } } @@ -1031,6 +1134,18 @@ func (w *windowsWebviewWindow) disableIcon() { ) } +func (w *windowsWebviewWindow) processThemeColour(fn func(w32.HWND, uint32), value *uint32) { + if value == nil { + return + } + fn(w.hwnd, *value) +} + +func (w *windowsWebviewWindow) isDisabled() bool { + style := uint32(w32.GetWindowLong(w.hwnd, w32.GWL_STYLE)) + return style&w32.WS_DISABLED != 0 +} + func (w *windowsWebviewWindow) updateTheme(isDarkMode bool) { if w32.IsCurrentlyHighContrastMode() { @@ -1043,30 +1158,105 @@ func (w *windowsWebviewWindow) updateTheme(isDarkMode bool) { w32.SetTheme(w.hwnd, isDarkMode) + // Clear any existing theme first + if w.menubarTheme != nil && !isDarkMode { + // Reset menu to default Windows theme when switching to light mode + w.menubarTheme = nil + if w.menu != nil { + // Clear the menu background by setting it to default + var mi w32.MENUINFO + mi.CbSize = uint32(unsafe.Sizeof(mi)) + mi.FMask = w32.MIIM_BACKGROUND | w32.MIIM_APPLYTOSUBMENUS + mi.HbrBack = 0 // NULL brush resets to default + w32.SetMenuInfo(w.menu.menu, &mi) + } + } + // Custom theme processing customTheme := w.parent.options.Windows.CustomTheme // Custom theme - if w32.SupportsCustomThemes() && customTheme != nil { - if w.isActive() { - if isDarkMode { - w32.SetTitleBarColour(w.hwnd, customTheme.DarkModeTitleBar) - w32.SetTitleTextColour(w.hwnd, customTheme.DarkModeTitleText) - w32.SetBorderColour(w.hwnd, customTheme.DarkModeBorder) - } else { - w32.SetTitleBarColour(w.hwnd, customTheme.LightModeTitleBar) - w32.SetTitleTextColour(w.hwnd, customTheme.LightModeTitleText) - w32.SetBorderColour(w.hwnd, customTheme.LightModeBorder) - } + if w32.SupportsCustomThemes() { + var userTheme *MenuBarTheme + if isDarkMode { + userTheme = customTheme.DarkModeMenuBar } else { + userTheme = customTheme.LightModeMenuBar + } + + if userTheme != nil { + modeStr := "light" if isDarkMode { - w32.SetTitleBarColour(w.hwnd, customTheme.DarkModeTitleBarInactive) - w32.SetTitleTextColour(w.hwnd, customTheme.DarkModeTitleTextInactive) - w32.SetBorderColour(w.hwnd, customTheme.DarkModeBorderInactive) - } else { - w32.SetTitleBarColour(w.hwnd, customTheme.LightModeTitleBarInactive) - w32.SetTitleTextColour(w.hwnd, customTheme.LightModeTitleTextInactive) - w32.SetBorderColour(w.hwnd, customTheme.LightModeBorderInactive) + modeStr = "dark" } + globalApplication.debug("Setting custom "+modeStr+" menubar theme", "window", w.parent.id) + w.menubarTheme = &w32.MenuBarTheme{ + TitleBarBackground: userTheme.Default.Background, + TitleBarText: userTheme.Default.Text, + MenuBarBackground: userTheme.Default.Background, // Use default background for menubar + MenuHoverBackground: userTheme.Hover.Background, + MenuHoverText: userTheme.Hover.Text, + MenuSelectedBackground: userTheme.Selected.Background, + MenuSelectedText: userTheme.Selected.Text, + } + w.menubarTheme.Init() + + // If menu is already set, update it + if w.menu != nil { + w.menubarTheme.SetMenuBackground(w.menu.menu) + w32.DrawMenuBar(w.hwnd) + w32.InvalidateRect(w.hwnd, nil, true) + } + } else if userTheme == nil && isDarkMode { + // Use default dark theme if no custom theme provided + globalApplication.debug("Setting default dark menubar theme", "window", w.parent.id) + w.menubarTheme = &w32.MenuBarTheme{ + TitleBarBackground: w32.RGBptr(45, 45, 45), // Dark titlebar + TitleBarText: w32.RGBptr(222, 222, 222), // Slightly muted white + MenuBarBackground: w32.RGBptr(33, 33, 33), // Standard dark mode (#212121) + MenuHoverBackground: w32.RGBptr(48, 48, 48), // Slightly lighter for hover (#303030) + MenuHoverText: w32.RGBptr(222, 222, 222), // Slightly muted white + MenuSelectedBackground: w32.RGBptr(48, 48, 48), // Same as hover + MenuSelectedText: w32.RGBptr(222, 222, 222), // Slightly muted white + } + w.menubarTheme.Init() + + // If menu is already set, update it + if w.menu != nil { + w.menubarTheme.SetMenuBackground(w.menu.menu) + w32.DrawMenuBar(w.hwnd) + w32.InvalidateRect(w.hwnd, nil, true) + } + } else if userTheme == nil && !isDarkMode && w.menu != nil { + // No custom theme for light mode - ensure menu is reset to default + globalApplication.debug("Resetting menu to default light theme", "window", w.parent.id) + var mi w32.MENUINFO + mi.CbSize = uint32(unsafe.Sizeof(mi)) + mi.FMask = w32.MIIM_BACKGROUND | w32.MIIM_APPLYTOSUBMENUS + mi.HbrBack = 0 // NULL brush resets to default + w32.SetMenuInfo(w.menu.menu, &mi) + w32.DrawMenuBar(w.hwnd) + w32.InvalidateRect(w.hwnd, nil, true) + } + // Define a map for theme selection + themeMap := map[bool]map[bool]*WindowTheme{ + true: { // Window is active + true: customTheme.DarkModeActive, // Dark mode + false: customTheme.LightModeActive, // Light mode + }, + false: { // Window is inactive + true: customTheme.DarkModeInactive, // Dark mode + false: customTheme.LightModeInactive, // Light mode + }, + } + + // Select the appropriate theme + theme := themeMap[w.isActive()][isDarkMode] + + // Apply theme colors + if theme != nil { + w.processThemeColour(w32.SetTitleBarColour, theme.TitleBarColour) + w.processThemeColour(w32.SetTitleTextColour, theme.TitleTextColour) + w.processThemeColour(w32.SetBorderColour, theme.BorderColour) } } } @@ -1078,6 +1268,13 @@ func (w *windowsWebviewWindow) isActive() bool { var resizePending int32 func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + + // Use the original implementation that works perfectly for maximized + processed, code := w32.MenuBarWndProc(w.hwnd, msg, wparam, lparam, w.menubarTheme) + if processed { + return code + } + switch msg { case w32.WM_ACTIVATE: if int(wparam&0xffff) == w32.WA_INACTIVE { @@ -1103,7 +1300,7 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp } case w32.WM_CLOSE: - if w.parent.unconditionallyClose == false { + if atomic.LoadUint32(&w.parent.unconditionallyClose) == 0 { // We were called by `Close()` or pressing the close button on the window w.parent.emit(events.Windows.WindowClosing) return 0 @@ -1164,6 +1361,7 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp case w32.WM_ERASEBKGND: w.parent.emit(events.Windows.WindowBackgroundErase) return 1 // Let WebView2 handle background erasing + // WM_UAHDRAWMENUITEM is handled by MenuBarWndProc at the top of this function // Check for keypress case w32.WM_SYSCOMMAND: switch wparam { @@ -1201,7 +1399,15 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp switch wparam { case w32.SIZE_MAXIMIZED: w.parent.emit(events.Windows.WindowMaximise) + // Force complete redraw when maximized + if w.menu != nil && w.menubarTheme != nil { + // Invalidate the entire window to force complete redraw + w32.RedrawWindow(w.hwnd, nil, 0, w32.RDW_FRAME|w32.RDW_INVALIDATE|w32.RDW_UPDATENOW) + } case w32.SIZE_RESTORED: + if w.isMinimizing { + w.parent.emit(events.Windows.WindowUnMinimise) + } w.isMinimizing = false w.parent.emit(events.Windows.WindowRestore) case w32.SIZE_MINIMIZED: @@ -1395,11 +1601,11 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp } w.setPadding(edge.Rect{}) } else { - // This is needed to workaround the resize flickering in frameless mode with WindowDecorations + // This is needed to work around the resize flickering in frameless mode with WindowDecorations // See: https://stackoverflow.com/a/6558508 // The workaround from the SO answer suggests to reduce the bottom of the window by 1px. - // However this would result in loosing 1px of the WebView content. - // Increasing the bottom also worksaround the flickering but we would loose 1px of the WebView content + // However, this would result in losing 1px of the WebView content. + // Increasing the bottom also worksaround the flickering, but we would lose 1px of the WebView content // therefore let's pad the content with 1px at the bottom. rgrc.Bottom += 1 w.setPadding(edge.Rect{Bottom: 1}) @@ -1465,7 +1671,7 @@ func (w *windowsWebviewWindow) setWindowMask(imageData []byte) { data, err := pngToImage(imageData) if err != nil { - globalApplication.fatal("Fatal error in callback setWindowMask: " + err.Error()) + globalApplication.fatal("fatal error in callback setWindowMask: %w", err) } bitmap, err := w32.CreateHBITMAPFromImage(data) @@ -1513,15 +1719,15 @@ func (w *windowsWebviewWindow) processRequest(req *edge.ICoreWebView2WebResource useragent = strings.Join([]string{useragent, assetserver.WailsUserAgentValue}, " ") err = reqHeaders.SetHeader(assetserver.HeaderUserAgent, useragent) if err != nil { - globalApplication.fatal("Error setting UserAgent header: " + err.Error()) + globalApplication.fatal("error setting UserAgent header: %w", err) } err = reqHeaders.SetHeader(webViewRequestHeaderWindowId, strconv.FormatUint(uint64(w.parent.id), 10)) if err != nil { - globalApplication.fatal("Error setting WindowId header: " + err.Error()) + globalApplication.fatal("error setting WindowId header: %w", err) } err = reqHeaders.Release() if err != nil { - globalApplication.fatal("Error releasing headers: " + err.Error()) + globalApplication.fatal("error releasing headers: %w", err) } } @@ -1534,7 +1740,7 @@ func (w *windowsWebviewWindow) processRequest(req *edge.ICoreWebView2WebResource uri, _ := req.GetUri() reqUri, err := url.ParseRequestURI(uri) if err != nil { - globalApplication.error("Unable to parse request uri: uri='%s' error='%s'", uri, err) + globalApplication.error("unable to parse request uri: uri='%s' error='%w'", uri, err) return } @@ -1553,7 +1759,7 @@ func (w *windowsWebviewWindow) processRequest(req *edge.ICoreWebView2WebResource InvokeSync(fn) }) if err != nil { - globalApplication.error("%s: NewRequest failed: %s", uri, err) + globalApplication.error("%s: NewRequest failed: %w", uri, err) return } @@ -1572,7 +1778,7 @@ func (w *windowsWebviewWindow) setupChromium() { webview2version, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(globalApplication.options.Windows.WebviewBrowserPath) if err != nil { - globalApplication.error("Error getting WebView2 version: " + err.Error()) + globalApplication.error("error getting WebView2 version: %w", err) return } globalApplication.capabilities = capabilities.NewCapabilities(webview2version) @@ -1611,17 +1817,26 @@ func (w *windowsWebviewWindow) setupChromium() { chromium.Embed(w.hwnd) + // Prevent efficiency mode by keeping WebView2 visible (fixes issue #2861) + // Microsoft recommendation: keep IsVisible = true to avoid efficiency mode + // See: https://github.com/MicrosoftEdge/WebView2Feedback/discussions/4021 + // TODO: Re-enable when PutIsVisible method is available in go-webview2 package + // err := chromium.PutIsVisible(true) + // if err != nil { + // globalApplication.error("Failed to set WebView2 visibility for efficiency mode prevention: %v", err) + // } + if chromium.HasCapability(edge.SwipeNavigation) { err := chromium.PutIsSwipeNavigationEnabled(opts.EnableSwipeGestures) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } } if chromium.HasCapability(edge.AllowExternalDrop) { err := chromium.AllowExternalDrag(false) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } } if w.parent.options.EnableDragAndDrop { @@ -1657,7 +1872,7 @@ func (w *windowsWebviewWindow) setupChromium() { //if windowName == "Chrome_RenderWidgetHostHWND" { err := w32.RegisterDragDrop(hwnd, w.dropTarget) if err != nil && !errors.Is(err, syscall.Errno(w32.DRAGDROP_E_ALREADYREGISTERED)) { - globalApplication.error("Error registering drag and drop: " + err.Error()) + globalApplication.error("error registering drag and drop: %w", err) } //} return 1 @@ -1665,26 +1880,21 @@ func (w *windowsWebviewWindow) setupChromium() { } - if opts.GeneralAutofillEnabled { - err = chromium.PutIsGeneralAutofillEnabled(true) - if err != nil { - if errors.Is(edge.UnsupportedCapabilityError, err) { - // warning - globalApplication.warning("unsupported capability: GeneralAutofillEnabled") - } else { - globalApplication.fatal(err.Error()) - } + err = chromium.PutIsGeneralAutofillEnabled(opts.GeneralAutofillEnabled) + if err != nil { + if errors.Is(err, edge.UnsupportedCapabilityError) { + globalApplication.warning("unsupported capability: GeneralAutofillEnabled") + } else { + globalApplication.handleFatalError(err) } } - if opts.PasswordAutosaveEnabled { - err = chromium.PutIsPasswordAutosaveEnabled(true) - if err != nil { - if errors.Is(edge.UnsupportedCapabilityError, err) { - globalApplication.warning("unsupported capability: PasswordAutosaveEnabled") - } else { - globalApplication.fatal(err.Error()) - } + err = chromium.PutIsPasswordAutosaveEnabled(opts.PasswordAutosaveEnabled) + if err != nil { + if errors.Is(err, edge.UnsupportedCapabilityError) { + globalApplication.warning("unsupported capability: PasswordAutosaveEnabled") + } else { + globalApplication.handleFatalError(err) } } @@ -1692,7 +1902,7 @@ func (w *windowsWebviewWindow) setupChromium() { //if chromium.HasCapability(edge.AllowExternalDrop) { // err := chromium.AllowExternalDrag(w.parent.options.EnableDragAndDrop) // if err != nil { - // globalApplication.fatal(err.Error()) + // globalApplication.handleFatalError(err) // } // if w.parent.options.EnableDragAndDrop { // chromium.MessageWithAdditionalObjectsCallback = w.processMessageWithAdditionalObjects @@ -1702,14 +1912,14 @@ func (w *windowsWebviewWindow) setupChromium() { chromium.Resize() settings, err := chromium.GetSettings() if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } if settings == nil { - globalApplication.fatal("Error getting settings") + globalApplication.fatal("error getting settings") } err = settings.PutAreDefaultContextMenusEnabled(debugMode || !w.parent.options.DefaultContextMenuDisabled) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } w.enableDevTools(settings) @@ -1719,20 +1929,20 @@ func (w *windowsWebviewWindow) setupChromium() { } err = settings.PutIsZoomControlEnabled(w.parent.options.ZoomControlEnabled) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } err = settings.PutIsStatusBarEnabled(false) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } err = settings.PutAreBrowserAcceleratorKeysEnabled(false) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } err = settings.PutIsSwipeNavigationEnabled(false) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } if debugMode && w.parent.options.OpenInspectorOnStartup { @@ -1761,7 +1971,7 @@ func (w *windowsWebviewWindow) setupChromium() { } else { startURL, err := assetserver.GetStartURL(w.parent.options.URL) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } w.webviewNavigationCompleted = false chromium.Navigate(startURL) @@ -1772,7 +1982,7 @@ func (w *windowsWebviewWindow) setupChromium() { func (w *windowsWebviewWindow) fullscreenChanged(sender *edge.ICoreWebView2, _ *edge.ICoreWebView2ContainsFullScreenElementChangedEventArgs) { isFullscreen, err := sender.GetContainsFullScreenElement() if err != nil { - globalApplication.fatal("Fatal error in callback fullscreenChanged: " + err.Error()) + globalApplication.fatal("fatal error in callback fullscreenChanged: %w", err) } if isFullscreen { w.fullscreen() @@ -1813,21 +2023,32 @@ func (w *windowsWebviewWindow) navigationCompleted(sender *edge.ICoreWebView2, a } w.webviewNavigationCompleted = true + // Cancel any pending visibility timeout since navigation completed + if w.visibilityTimeout != nil { + w.visibilityTimeout.Stop() + w.visibilityTimeout = nil + } + wasFocused := w.isFocused() // Hack to make it visible: https://github.com/MicrosoftEdge/WebView2Feedback/issues/1077#issuecomment-825375026 err := w.chromium.Hide() if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } err = w.chromium.Show() if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } if wasFocused { w.focus() } + + // Only call parent.Show() if not hidden and show was requested but window wasn't shown yet + // The new robust show() method handles window visibility independently if !w.parent.options.Hidden { - w.parent.Show() + if w.showRequested && !w.windowShown { + w.parent.Show() + } w.update() } } @@ -1839,7 +2060,7 @@ func (w *windowsWebviewWindow) processKeyBinding(vkey uint) bool { // Get the keyboard state and convert to an accelerator var keyState [256]byte if !w32.GetKeyboardState(keyState[:]) { - globalApplication.error("Error getting keyboard state") + globalApplication.error("error getting keyboard state") return false } @@ -1890,20 +2111,20 @@ func (w *windowsWebviewWindow) processMessageWithAdditionalObjects(message strin if strings.HasPrefix(message, "FilesDropped") { objs, err := args.GetAdditionalObjects() if err != nil { - globalApplication.error(err.Error()) + globalApplication.handleError(err) return } defer func() { err = objs.Release() if err != nil { - globalApplication.error("Error releasing objects: " + err.Error()) + globalApplication.error("error releasing objects: %w", err) } }() count, err := objs.GetCount() if err != nil { - globalApplication.error("cannot get count: %s", err.Error()) + globalApplication.error("cannot get count: %w", err) return } @@ -1911,7 +2132,7 @@ func (w *windowsWebviewWindow) processMessageWithAdditionalObjects(message strin for i := uint32(0); i < count; i++ { _file, err := objs.GetValueAtIndex(i) if err != nil { - globalApplication.error("cannot get value at %d : %s", i, err.Error()) + globalApplication.error("cannot get value at %d: %w", i, err) return } @@ -1922,7 +2143,7 @@ func (w *windowsWebviewWindow) processMessageWithAdditionalObjects(message strin filepath, err := file.GetPath() if err != nil { - globalApplication.error("cannot get path for object at %d : %s", i, err.Error()) + globalApplication.error("cannot get path for object at %d: %w", i, err) return } @@ -1983,7 +2204,7 @@ func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error var err error var result w32.HICON if result = w32.LoadIconWithResourceID(instance, resId); result == 0 { - err = errors.New(fmt.Sprintf("Cannot load icon from resource with id %v", resId)) + err = fmt.Errorf("cannot load icon from resource with id %v", resId) } return result, err } diff --git a/v3/pkg/application/webview_window_windows_devtools.go b/v3/pkg/application/webview_window_windows_devtools.go index 2ee7ea1a3..e11bebedd 100644 --- a/v3/pkg/application/webview_window_windows_devtools.go +++ b/v3/pkg/application/webview_window_windows_devtools.go @@ -11,6 +11,6 @@ func (w *windowsWebviewWindow) openDevTools() { func (w *windowsWebviewWindow) enableDevTools(settings *edge.ICoreWebViewSettings) { err := settings.PutAreDevToolsEnabled(true) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } } diff --git a/v3/pkg/application/webview_window_windows_production.go b/v3/pkg/application/webview_window_windows_production.go index 55a9d2ad2..56ef88b75 100644 --- a/v3/pkg/application/webview_window_windows_production.go +++ b/v3/pkg/application/webview_window_windows_production.go @@ -9,6 +9,6 @@ func (w *windowsWebviewWindow) openDevTools() {} func (w *windowsWebviewWindow) enableDevTools(settings *edge.ICoreWebViewSettings) { err := settings.PutAreDevToolsEnabled(false) if err != nil { - globalApplication.fatal(err.Error()) + globalApplication.handleFatalError(err) } } diff --git a/v3/pkg/application/window.go b/v3/pkg/application/window.go index 3f3dea64a..58b4c4bf5 100644 --- a/v3/pkg/application/window.go +++ b/v3/pkg/application/window.go @@ -5,7 +5,7 @@ import ( ) type Callback interface { - CallError(callID string, result string) + CallError(callID string, result string, isJSON bool) CallResponse(callID string, result string) DialogError(dialogID string, result string) DialogResponse(dialogID string, result string, isJSON bool) @@ -76,6 +76,7 @@ type Window interface { ToggleFullscreen() ToggleMaximise() ToggleMenuBar() + ToggleFrameless() UnFullscreen() UnMaximise() UnMinimise() @@ -84,4 +85,5 @@ type Window interface { ZoomIn() ZoomOut() ZoomReset() Window + SetMenu(menu *Menu) } diff --git a/v3/pkg/application/window_manager.go b/v3/pkg/application/window_manager.go new file mode 100644 index 000000000..ec6a3298a --- /dev/null +++ b/v3/pkg/application/window_manager.go @@ -0,0 +1,135 @@ +package application + +// WindowManager manages all window-related operations +type WindowManager struct { + app *App +} + +// newWindowManager creates a new WindowManager instance +func newWindowManager(app *App) *WindowManager { + return &WindowManager{ + app: app, + // Use the app's existing windows map - don't create a new one + } +} + +// GetByName returns a window by name and whether it exists +func (wm *WindowManager) GetByName(name string) (Window, bool) { + wm.app.windowsLock.RLock() + defer wm.app.windowsLock.RUnlock() + + for _, window := range wm.app.windows { + if window.Name() == name { + return window, true + } + } + return nil, false +} + +// Get is an alias for GetByName for consistency +func (wm *WindowManager) Get(name string) (Window, bool) { + return wm.GetByName(name) +} + +// GetByID returns a window by ID and whether it exists +func (wm *WindowManager) GetByID(id uint) (Window, bool) { + wm.app.windowsLock.RLock() + defer wm.app.windowsLock.RUnlock() + + window, exists := wm.app.windows[id] + return window, exists +} + +// OnCreate registers a callback to be called when a window is created +func (wm *WindowManager) OnCreate(callback func(Window)) { + wm.app.windowCreatedCallbacks = append(wm.app.windowCreatedCallbacks, callback) +} + +// New creates a new webview window +func (wm *WindowManager) New() *WebviewWindow { + return wm.NewWithOptions(WebviewWindowOptions{}) +} + +// NewWithOptions creates a new webview window with options +func (wm *WindowManager) NewWithOptions(windowOptions WebviewWindowOptions) *WebviewWindow { + newWindow := NewWindow(windowOptions) + id := newWindow.ID() + + wm.app.windowsLock.Lock() + wm.app.windows[id] = newWindow + wm.app.windowsLock.Unlock() + + // Call hooks + for _, hook := range wm.app.windowCreatedCallbacks { + hook(newWindow) + } + + wm.app.runOrDeferToAppRun(newWindow) + + return newWindow +} + +// Current returns the current active window (may be nil) +func (wm *WindowManager) Current() *WebviewWindow { + if wm.app.impl == nil { + return nil + } + id := wm.app.impl.getCurrentWindowID() + wm.app.windowsLock.RLock() + defer wm.app.windowsLock.RUnlock() + result := wm.app.windows[id] + if result == nil { + return nil + } + return result.(*WebviewWindow) +} + +// Add adds a window to the manager +func (wm *WindowManager) Add(window Window) { + wm.app.windowsLock.Lock() + defer wm.app.windowsLock.Unlock() + wm.app.windows[window.ID()] = window + + // Call registered callbacks + for _, callback := range wm.app.windowCreatedCallbacks { + callback(window) + } +} + +// Remove removes a window from the manager by ID +func (wm *WindowManager) Remove(windowID uint) { + wm.app.windowsLock.Lock() + defer wm.app.windowsLock.Unlock() + delete(wm.app.windows, windowID) +} + +// RemoveByName removes a window from the manager by name +func (wm *WindowManager) RemoveByName(name string) bool { + window, exists := wm.GetByName(name) + if exists { + wm.Remove(window.ID()) + return true + } + return false +} + +// Internal methods for backward compatibility +func (wm *WindowManager) add(window Window) { + wm.Add(window) +} + +func (wm *WindowManager) remove(windowID uint) { + wm.Remove(windowID) +} + +// GetAll returns all windows +func (wm *WindowManager) GetAll() []Window { + wm.app.windowsLock.RLock() + defer wm.app.windowsLock.RUnlock() + + windows := make([]Window, 0, len(wm.app.windows)) + for _, window := range wm.app.windows { + windows = append(windows, window) + } + return windows +} diff --git a/v3/pkg/events/defaults.go b/v3/pkg/events/defaults.go index 926d86a60..376bef36f 100644 --- a/v3/pkg/events/defaults.go +++ b/v3/pkg/events/defaults.go @@ -41,9 +41,6 @@ var defaultWindowEventMapping = map[string]map[WindowEventType]WindowEventType{ Mac.WindowZoomOut: Common.WindowZoomOut, Mac.WindowZoomReset: Common.WindowZoomReset, Mac.WindowShouldClose: Common.WindowClosing, - Mac.WindowDidResignKey: Common.WindowLostFocus, - Mac.WindowDidResignMain: Common.WindowLostFocus, - Mac.WindowDidResize: Common.WindowDidResize, }, "linux": { Linux.WindowDeleteEvent: Common.WindowClosing, diff --git a/v3/pkg/events/events.go b/v3/pkg/events/events.go index 0bb4c850c..c0d200103 100644 --- a/v3/pkg/events/events.go +++ b/v3/pkg/events/events.go @@ -8,6 +8,7 @@ var Common = newCommonEvents() type commonEvents struct { ApplicationOpenedWithFile ApplicationEventType ApplicationStarted ApplicationEventType + ApplicationLaunchedWithUrl ApplicationEventType ThemeChanged ApplicationEventType WindowClosing WindowEventType WindowDidMove WindowEventType @@ -36,28 +37,29 @@ func newCommonEvents() commonEvents { return commonEvents{ ApplicationOpenedWithFile: 1024, ApplicationStarted: 1025, - ThemeChanged: 1026, - WindowClosing: 1027, - WindowDidMove: 1028, - WindowDidResize: 1029, - WindowDPIChanged: 1030, - WindowFilesDropped: 1031, - WindowFocus: 1032, - WindowFullscreen: 1033, - WindowHide: 1034, - WindowLostFocus: 1035, - WindowMaximise: 1036, - WindowMinimise: 1037, - WindowRestore: 1038, - WindowRuntimeReady: 1039, - WindowShow: 1040, - WindowUnFullscreen: 1041, - WindowUnMaximise: 1042, - WindowUnMinimise: 1043, - WindowZoom: 1044, - WindowZoomIn: 1045, - WindowZoomOut: 1046, - WindowZoomReset: 1047, + ApplicationLaunchedWithUrl: 1026, + ThemeChanged: 1027, + WindowClosing: 1028, + WindowDidMove: 1029, + WindowDidResize: 1030, + WindowDPIChanged: 1031, + WindowFilesDropped: 1032, + WindowFocus: 1033, + WindowFullscreen: 1034, + WindowHide: 1035, + WindowLostFocus: 1036, + WindowMaximise: 1037, + WindowMinimise: 1038, + WindowRestore: 1039, + WindowRuntimeReady: 1040, + WindowShow: 1041, + WindowUnFullscreen: 1042, + WindowUnMaximise: 1043, + WindowUnMinimise: 1044, + WindowZoom: 1045, + WindowZoomIn: 1046, + WindowZoomOut: 1047, + WindowZoomReset: 1048, } } @@ -76,14 +78,14 @@ type linuxEvents struct { func newLinuxEvents() linuxEvents { return linuxEvents{ - ApplicationStartup: 1048, - SystemThemeChanged: 1049, - WindowDeleteEvent: 1050, - WindowDidMove: 1051, - WindowDidResize: 1052, - WindowFocusIn: 1053, - WindowFocusOut: 1054, - WindowLoadChanged: 1055, + ApplicationStartup: 1049, + SystemThemeChanged: 1050, + WindowDeleteEvent: 1051, + WindowDidMove: 1052, + WindowDidResize: 1053, + WindowFocusIn: 1054, + WindowFocusOut: 1055, + WindowLoadChanged: 1056, } } @@ -226,138 +228,138 @@ type macEvents struct { func newMacEvents() macEvents { return macEvents{ - ApplicationDidBecomeActive: 1056, - ApplicationDidChangeBackingProperties: 1057, - ApplicationDidChangeEffectiveAppearance: 1058, - ApplicationDidChangeIcon: 1059, - ApplicationDidChangeOcclusionState: 1060, - ApplicationDidChangeScreenParameters: 1061, - ApplicationDidChangeStatusBarFrame: 1062, - ApplicationDidChangeStatusBarOrientation: 1063, - ApplicationDidChangeTheme: 1064, - ApplicationDidFinishLaunching: 1065, - ApplicationDidHide: 1066, - ApplicationDidResignActive: 1067, - ApplicationDidUnhide: 1068, - ApplicationDidUpdate: 1069, - ApplicationShouldHandleReopen: 1070, - ApplicationWillBecomeActive: 1071, - ApplicationWillFinishLaunching: 1072, - ApplicationWillHide: 1073, - ApplicationWillResignActive: 1074, - ApplicationWillTerminate: 1075, - ApplicationWillUnhide: 1076, - ApplicationWillUpdate: 1077, - MenuDidAddItem: 1078, - MenuDidBeginTracking: 1079, - MenuDidClose: 1080, - MenuDidDisplayItem: 1081, - MenuDidEndTracking: 1082, - MenuDidHighlightItem: 1083, - MenuDidOpen: 1084, - MenuDidPopUp: 1085, - MenuDidRemoveItem: 1086, - MenuDidSendAction: 1087, - MenuDidSendActionToItem: 1088, - MenuDidUpdate: 1089, - MenuWillAddItem: 1090, - MenuWillBeginTracking: 1091, - MenuWillDisplayItem: 1092, - MenuWillEndTracking: 1093, - MenuWillHighlightItem: 1094, - MenuWillOpen: 1095, - MenuWillPopUp: 1096, - MenuWillRemoveItem: 1097, - MenuWillSendAction: 1098, - MenuWillSendActionToItem: 1099, - MenuWillUpdate: 1100, - WebViewDidCommitNavigation: 1101, - WebViewDidFinishNavigation: 1102, - WebViewDidReceiveServerRedirectForProvisionalNavigation: 1103, - WebViewDidStartProvisionalNavigation: 1104, - WindowDidBecomeKey: 1105, - WindowDidBecomeMain: 1106, - WindowDidBeginSheet: 1107, - WindowDidChangeAlpha: 1108, - WindowDidChangeBackingLocation: 1109, - WindowDidChangeBackingProperties: 1110, - WindowDidChangeCollectionBehavior: 1111, - WindowDidChangeEffectiveAppearance: 1112, - WindowDidChangeOcclusionState: 1113, - WindowDidChangeOrderingMode: 1114, - WindowDidChangeScreen: 1115, - WindowDidChangeScreenParameters: 1116, - WindowDidChangeScreenProfile: 1117, - WindowDidChangeScreenSpace: 1118, - WindowDidChangeScreenSpaceProperties: 1119, - WindowDidChangeSharingType: 1120, - WindowDidChangeSpace: 1121, - WindowDidChangeSpaceOrderingMode: 1122, - WindowDidChangeTitle: 1123, - WindowDidChangeToolbar: 1124, - WindowDidDeminiaturize: 1125, - WindowDidEndSheet: 1126, - WindowDidEnterFullScreen: 1127, - WindowDidEnterVersionBrowser: 1128, - WindowDidExitFullScreen: 1129, - WindowDidExitVersionBrowser: 1130, - WindowDidExpose: 1131, - WindowDidFocus: 1132, - WindowDidMiniaturize: 1133, - WindowDidMove: 1134, - WindowDidOrderOffScreen: 1135, - WindowDidOrderOnScreen: 1136, - WindowDidResignKey: 1137, - WindowDidResignMain: 1138, - WindowDidResize: 1139, - WindowDidUpdate: 1140, - WindowDidUpdateAlpha: 1141, - WindowDidUpdateCollectionBehavior: 1142, - WindowDidUpdateCollectionProperties: 1143, - WindowDidUpdateShadow: 1144, - WindowDidUpdateTitle: 1145, - WindowDidUpdateToolbar: 1146, - WindowDidZoom: 1147, - WindowFileDraggingEntered: 1148, - WindowFileDraggingExited: 1149, - WindowFileDraggingPerformed: 1150, - WindowHide: 1151, - WindowMaximise: 1152, - WindowUnMaximise: 1153, - WindowMinimise: 1154, - WindowUnMinimise: 1155, - WindowShouldClose: 1156, - WindowShow: 1157, - WindowWillBecomeKey: 1158, - WindowWillBecomeMain: 1159, - WindowWillBeginSheet: 1160, - WindowWillChangeOrderingMode: 1161, - WindowWillClose: 1162, - WindowWillDeminiaturize: 1163, - WindowWillEnterFullScreen: 1164, - WindowWillEnterVersionBrowser: 1165, - WindowWillExitFullScreen: 1166, - WindowWillExitVersionBrowser: 1167, - WindowWillFocus: 1168, - WindowWillMiniaturize: 1169, - WindowWillMove: 1170, - WindowWillOrderOffScreen: 1171, - WindowWillOrderOnScreen: 1172, - WindowWillResignMain: 1173, - WindowWillResize: 1174, - WindowWillUnfocus: 1175, - WindowWillUpdate: 1176, - WindowWillUpdateAlpha: 1177, - WindowWillUpdateCollectionBehavior: 1178, - WindowWillUpdateCollectionProperties: 1179, - WindowWillUpdateShadow: 1180, - WindowWillUpdateTitle: 1181, - WindowWillUpdateToolbar: 1182, - WindowWillUpdateVisibility: 1183, - WindowWillUseStandardFrame: 1184, - WindowZoomIn: 1185, - WindowZoomOut: 1186, - WindowZoomReset: 1187, + ApplicationDidBecomeActive: 1057, + ApplicationDidChangeBackingProperties: 1058, + ApplicationDidChangeEffectiveAppearance: 1059, + ApplicationDidChangeIcon: 1060, + ApplicationDidChangeOcclusionState: 1061, + ApplicationDidChangeScreenParameters: 1062, + ApplicationDidChangeStatusBarFrame: 1063, + ApplicationDidChangeStatusBarOrientation: 1064, + ApplicationDidChangeTheme: 1065, + ApplicationDidFinishLaunching: 1066, + ApplicationDidHide: 1067, + ApplicationDidResignActive: 1068, + ApplicationDidUnhide: 1069, + ApplicationDidUpdate: 1070, + ApplicationShouldHandleReopen: 1071, + ApplicationWillBecomeActive: 1072, + ApplicationWillFinishLaunching: 1073, + ApplicationWillHide: 1074, + ApplicationWillResignActive: 1075, + ApplicationWillTerminate: 1076, + ApplicationWillUnhide: 1077, + ApplicationWillUpdate: 1078, + MenuDidAddItem: 1079, + MenuDidBeginTracking: 1080, + MenuDidClose: 1081, + MenuDidDisplayItem: 1082, + MenuDidEndTracking: 1083, + MenuDidHighlightItem: 1084, + MenuDidOpen: 1085, + MenuDidPopUp: 1086, + MenuDidRemoveItem: 1087, + MenuDidSendAction: 1088, + MenuDidSendActionToItem: 1089, + MenuDidUpdate: 1090, + MenuWillAddItem: 1091, + MenuWillBeginTracking: 1092, + MenuWillDisplayItem: 1093, + MenuWillEndTracking: 1094, + MenuWillHighlightItem: 1095, + MenuWillOpen: 1096, + MenuWillPopUp: 1097, + MenuWillRemoveItem: 1098, + MenuWillSendAction: 1099, + MenuWillSendActionToItem: 1100, + MenuWillUpdate: 1101, + WebViewDidCommitNavigation: 1102, + WebViewDidFinishNavigation: 1103, + WebViewDidReceiveServerRedirectForProvisionalNavigation: 1104, + WebViewDidStartProvisionalNavigation: 1105, + WindowDidBecomeKey: 1106, + WindowDidBecomeMain: 1107, + WindowDidBeginSheet: 1108, + WindowDidChangeAlpha: 1109, + WindowDidChangeBackingLocation: 1110, + WindowDidChangeBackingProperties: 1111, + WindowDidChangeCollectionBehavior: 1112, + WindowDidChangeEffectiveAppearance: 1113, + WindowDidChangeOcclusionState: 1114, + WindowDidChangeOrderingMode: 1115, + WindowDidChangeScreen: 1116, + WindowDidChangeScreenParameters: 1117, + WindowDidChangeScreenProfile: 1118, + WindowDidChangeScreenSpace: 1119, + WindowDidChangeScreenSpaceProperties: 1120, + WindowDidChangeSharingType: 1121, + WindowDidChangeSpace: 1122, + WindowDidChangeSpaceOrderingMode: 1123, + WindowDidChangeTitle: 1124, + WindowDidChangeToolbar: 1125, + WindowDidDeminiaturize: 1126, + WindowDidEndSheet: 1127, + WindowDidEnterFullScreen: 1128, + WindowDidEnterVersionBrowser: 1129, + WindowDidExitFullScreen: 1130, + WindowDidExitVersionBrowser: 1131, + WindowDidExpose: 1132, + WindowDidFocus: 1133, + WindowDidMiniaturize: 1134, + WindowDidMove: 1135, + WindowDidOrderOffScreen: 1136, + WindowDidOrderOnScreen: 1137, + WindowDidResignKey: 1138, + WindowDidResignMain: 1139, + WindowDidResize: 1140, + WindowDidUpdate: 1141, + WindowDidUpdateAlpha: 1142, + WindowDidUpdateCollectionBehavior: 1143, + WindowDidUpdateCollectionProperties: 1144, + WindowDidUpdateShadow: 1145, + WindowDidUpdateTitle: 1146, + WindowDidUpdateToolbar: 1147, + WindowDidZoom: 1148, + WindowFileDraggingEntered: 1149, + WindowFileDraggingExited: 1150, + WindowFileDraggingPerformed: 1151, + WindowHide: 1152, + WindowMaximise: 1153, + WindowUnMaximise: 1154, + WindowMinimise: 1155, + WindowUnMinimise: 1156, + WindowShouldClose: 1157, + WindowShow: 1158, + WindowWillBecomeKey: 1159, + WindowWillBecomeMain: 1160, + WindowWillBeginSheet: 1161, + WindowWillChangeOrderingMode: 1162, + WindowWillClose: 1163, + WindowWillDeminiaturize: 1164, + WindowWillEnterFullScreen: 1165, + WindowWillEnterVersionBrowser: 1166, + WindowWillExitFullScreen: 1167, + WindowWillExitVersionBrowser: 1168, + WindowWillFocus: 1169, + WindowWillMiniaturize: 1170, + WindowWillMove: 1171, + WindowWillOrderOffScreen: 1172, + WindowWillOrderOnScreen: 1173, + WindowWillResignMain: 1174, + WindowWillResize: 1175, + WindowWillUnfocus: 1176, + WindowWillUpdate: 1177, + WindowWillUpdateAlpha: 1178, + WindowWillUpdateCollectionBehavior: 1179, + WindowWillUpdateCollectionProperties: 1180, + WindowWillUpdateShadow: 1181, + WindowWillUpdateTitle: 1182, + WindowWillUpdateToolbar: 1183, + WindowWillUpdateVisibility: 1184, + WindowWillUseStandardFrame: 1185, + WindowZoomIn: 1186, + WindowZoomOut: 1187, + WindowZoomReset: 1188, } } @@ -412,50 +414,50 @@ type windowsEvents struct { func newWindowsEvents() windowsEvents { return windowsEvents{ - APMPowerSettingChange: 1188, - APMPowerStatusChange: 1189, - APMResumeAutomatic: 1190, - APMResumeSuspend: 1191, - APMSuspend: 1192, - ApplicationStarted: 1193, - SystemThemeChanged: 1194, - WebViewNavigationCompleted: 1195, - WindowActive: 1196, - WindowBackgroundErase: 1197, - WindowClickActive: 1198, - WindowClosing: 1199, - WindowDidMove: 1200, - WindowDidResize: 1201, - WindowDPIChanged: 1202, - WindowDragDrop: 1203, - WindowDragEnter: 1204, - WindowDragLeave: 1205, - WindowDragOver: 1206, - WindowEndMove: 1207, - WindowEndResize: 1208, - WindowFullscreen: 1209, - WindowHide: 1210, - WindowInactive: 1211, - WindowKeyDown: 1212, - WindowKeyUp: 1213, - WindowKillFocus: 1214, - WindowNonClientHit: 1215, - WindowNonClientMouseDown: 1216, - WindowNonClientMouseLeave: 1217, - WindowNonClientMouseMove: 1218, - WindowNonClientMouseUp: 1219, - WindowPaint: 1220, - WindowRestore: 1221, - WindowSetFocus: 1222, - WindowShow: 1223, - WindowStartMove: 1224, - WindowStartResize: 1225, - WindowUnFullscreen: 1226, - WindowZOrderChanged: 1227, - WindowMinimise: 1228, - WindowUnMinimise: 1229, - WindowMaximise: 1230, - WindowUnMaximise: 1231, + APMPowerSettingChange: 1189, + APMPowerStatusChange: 1190, + APMResumeAutomatic: 1191, + APMResumeSuspend: 1192, + APMSuspend: 1193, + ApplicationStarted: 1194, + SystemThemeChanged: 1195, + WebViewNavigationCompleted: 1196, + WindowActive: 1197, + WindowBackgroundErase: 1198, + WindowClickActive: 1199, + WindowClosing: 1200, + WindowDidMove: 1201, + WindowDidResize: 1202, + WindowDPIChanged: 1203, + WindowDragDrop: 1204, + WindowDragEnter: 1205, + WindowDragLeave: 1206, + WindowDragOver: 1207, + WindowEndMove: 1208, + WindowEndResize: 1209, + WindowFullscreen: 1210, + WindowHide: 1211, + WindowInactive: 1212, + WindowKeyDown: 1213, + WindowKeyUp: 1214, + WindowKillFocus: 1215, + WindowNonClientHit: 1216, + WindowNonClientMouseDown: 1217, + WindowNonClientMouseLeave: 1218, + WindowNonClientMouseMove: 1219, + WindowNonClientMouseUp: 1220, + WindowPaint: 1221, + WindowRestore: 1222, + WindowSetFocus: 1223, + WindowShow: 1224, + WindowStartMove: 1225, + WindowStartResize: 1226, + WindowUnFullscreen: 1227, + WindowZOrderChanged: 1228, + WindowMinimise: 1229, + WindowUnMinimise: 1230, + WindowMaximise: 1231, + WindowUnMaximise: 1232, } } @@ -464,213 +466,214 @@ func JSEvent(event uint) string { } var eventToJS = map[uint]string{ - 1024: "common:ApplicationOpenedWithFile", - 1025: "common:ApplicationStarted", - 1026: "common:ThemeChanged", - 1027: "common:WindowClosing", - 1028: "common:WindowDidMove", - 1029: "common:WindowDidResize", - 1030: "common:WindowDPIChanged", - 1031: "common:WindowFilesDropped", - 1032: "common:WindowFocus", - 1033: "common:WindowFullscreen", - 1034: "common:WindowHide", - 1035: "common:WindowLostFocus", - 1036: "common:WindowMaximise", - 1037: "common:WindowMinimise", - 1038: "common:WindowRestore", - 1039: "common:WindowRuntimeReady", - 1040: "common:WindowShow", - 1041: "common:WindowUnFullscreen", - 1042: "common:WindowUnMaximise", - 1043: "common:WindowUnMinimise", - 1044: "common:WindowZoom", - 1045: "common:WindowZoomIn", - 1046: "common:WindowZoomOut", - 1047: "common:WindowZoomReset", - 1048: "linux:ApplicationStartup", - 1049: "linux:SystemThemeChanged", - 1050: "linux:WindowDeleteEvent", - 1051: "linux:WindowDidMove", - 1052: "linux:WindowDidResize", - 1053: "linux:WindowFocusIn", - 1054: "linux:WindowFocusOut", - 1055: "linux:WindowLoadChanged", - 1056: "mac:ApplicationDidBecomeActive", - 1057: "mac:ApplicationDidChangeBackingProperties", - 1058: "mac:ApplicationDidChangeEffectiveAppearance", - 1059: "mac:ApplicationDidChangeIcon", - 1060: "mac:ApplicationDidChangeOcclusionState", - 1061: "mac:ApplicationDidChangeScreenParameters", - 1062: "mac:ApplicationDidChangeStatusBarFrame", - 1063: "mac:ApplicationDidChangeStatusBarOrientation", - 1064: "mac:ApplicationDidChangeTheme", - 1065: "mac:ApplicationDidFinishLaunching", - 1066: "mac:ApplicationDidHide", - 1067: "mac:ApplicationDidResignActive", - 1068: "mac:ApplicationDidUnhide", - 1069: "mac:ApplicationDidUpdate", - 1070: "mac:ApplicationShouldHandleReopen", - 1071: "mac:ApplicationWillBecomeActive", - 1072: "mac:ApplicationWillFinishLaunching", - 1073: "mac:ApplicationWillHide", - 1074: "mac:ApplicationWillResignActive", - 1075: "mac:ApplicationWillTerminate", - 1076: "mac:ApplicationWillUnhide", - 1077: "mac:ApplicationWillUpdate", - 1078: "mac:MenuDidAddItem", - 1079: "mac:MenuDidBeginTracking", - 1080: "mac:MenuDidClose", - 1081: "mac:MenuDidDisplayItem", - 1082: "mac:MenuDidEndTracking", - 1083: "mac:MenuDidHighlightItem", - 1084: "mac:MenuDidOpen", - 1085: "mac:MenuDidPopUp", - 1086: "mac:MenuDidRemoveItem", - 1087: "mac:MenuDidSendAction", - 1088: "mac:MenuDidSendActionToItem", - 1089: "mac:MenuDidUpdate", - 1090: "mac:MenuWillAddItem", - 1091: "mac:MenuWillBeginTracking", - 1092: "mac:MenuWillDisplayItem", - 1093: "mac:MenuWillEndTracking", - 1094: "mac:MenuWillHighlightItem", - 1095: "mac:MenuWillOpen", - 1096: "mac:MenuWillPopUp", - 1097: "mac:MenuWillRemoveItem", - 1098: "mac:MenuWillSendAction", - 1099: "mac:MenuWillSendActionToItem", - 1100: "mac:MenuWillUpdate", - 1101: "mac:WebViewDidCommitNavigation", - 1102: "mac:WebViewDidFinishNavigation", - 1103: "mac:WebViewDidReceiveServerRedirectForProvisionalNavigation", - 1104: "mac:WebViewDidStartProvisionalNavigation", - 1105: "mac:WindowDidBecomeKey", - 1106: "mac:WindowDidBecomeMain", - 1107: "mac:WindowDidBeginSheet", - 1108: "mac:WindowDidChangeAlpha", - 1109: "mac:WindowDidChangeBackingLocation", - 1110: "mac:WindowDidChangeBackingProperties", - 1111: "mac:WindowDidChangeCollectionBehavior", - 1112: "mac:WindowDidChangeEffectiveAppearance", - 1113: "mac:WindowDidChangeOcclusionState", - 1114: "mac:WindowDidChangeOrderingMode", - 1115: "mac:WindowDidChangeScreen", - 1116: "mac:WindowDidChangeScreenParameters", - 1117: "mac:WindowDidChangeScreenProfile", - 1118: "mac:WindowDidChangeScreenSpace", - 1119: "mac:WindowDidChangeScreenSpaceProperties", - 1120: "mac:WindowDidChangeSharingType", - 1121: "mac:WindowDidChangeSpace", - 1122: "mac:WindowDidChangeSpaceOrderingMode", - 1123: "mac:WindowDidChangeTitle", - 1124: "mac:WindowDidChangeToolbar", - 1125: "mac:WindowDidDeminiaturize", - 1126: "mac:WindowDidEndSheet", - 1127: "mac:WindowDidEnterFullScreen", - 1128: "mac:WindowDidEnterVersionBrowser", - 1129: "mac:WindowDidExitFullScreen", - 1130: "mac:WindowDidExitVersionBrowser", - 1131: "mac:WindowDidExpose", - 1132: "mac:WindowDidFocus", - 1133: "mac:WindowDidMiniaturize", - 1134: "mac:WindowDidMove", - 1135: "mac:WindowDidOrderOffScreen", - 1136: "mac:WindowDidOrderOnScreen", - 1137: "mac:WindowDidResignKey", - 1138: "mac:WindowDidResignMain", - 1139: "mac:WindowDidResize", - 1140: "mac:WindowDidUpdate", - 1141: "mac:WindowDidUpdateAlpha", - 1142: "mac:WindowDidUpdateCollectionBehavior", - 1143: "mac:WindowDidUpdateCollectionProperties", - 1144: "mac:WindowDidUpdateShadow", - 1145: "mac:WindowDidUpdateTitle", - 1146: "mac:WindowDidUpdateToolbar", - 1147: "mac:WindowDidZoom", - 1148: "mac:WindowFileDraggingEntered", - 1149: "mac:WindowFileDraggingExited", - 1150: "mac:WindowFileDraggingPerformed", - 1151: "mac:WindowHide", - 1152: "mac:WindowMaximise", - 1153: "mac:WindowUnMaximise", - 1154: "mac:WindowMinimise", - 1155: "mac:WindowUnMinimise", - 1156: "mac:WindowShouldClose", - 1157: "mac:WindowShow", - 1158: "mac:WindowWillBecomeKey", - 1159: "mac:WindowWillBecomeMain", - 1160: "mac:WindowWillBeginSheet", - 1161: "mac:WindowWillChangeOrderingMode", - 1162: "mac:WindowWillClose", - 1163: "mac:WindowWillDeminiaturize", - 1164: "mac:WindowWillEnterFullScreen", - 1165: "mac:WindowWillEnterVersionBrowser", - 1166: "mac:WindowWillExitFullScreen", - 1167: "mac:WindowWillExitVersionBrowser", - 1168: "mac:WindowWillFocus", - 1169: "mac:WindowWillMiniaturize", - 1170: "mac:WindowWillMove", - 1171: "mac:WindowWillOrderOffScreen", - 1172: "mac:WindowWillOrderOnScreen", - 1173: "mac:WindowWillResignMain", - 1174: "mac:WindowWillResize", - 1175: "mac:WindowWillUnfocus", - 1176: "mac:WindowWillUpdate", - 1177: "mac:WindowWillUpdateAlpha", - 1178: "mac:WindowWillUpdateCollectionBehavior", - 1179: "mac:WindowWillUpdateCollectionProperties", - 1180: "mac:WindowWillUpdateShadow", - 1181: "mac:WindowWillUpdateTitle", - 1182: "mac:WindowWillUpdateToolbar", - 1183: "mac:WindowWillUpdateVisibility", - 1184: "mac:WindowWillUseStandardFrame", - 1185: "mac:WindowZoomIn", - 1186: "mac:WindowZoomOut", - 1187: "mac:WindowZoomReset", - 1188: "windows:APMPowerSettingChange", - 1189: "windows:APMPowerStatusChange", - 1190: "windows:APMResumeAutomatic", - 1191: "windows:APMResumeSuspend", - 1192: "windows:APMSuspend", - 1193: "windows:ApplicationStarted", - 1194: "windows:SystemThemeChanged", - 1195: "windows:WebViewNavigationCompleted", - 1196: "windows:WindowActive", - 1197: "windows:WindowBackgroundErase", - 1198: "windows:WindowClickActive", - 1199: "windows:WindowClosing", - 1200: "windows:WindowDidMove", - 1201: "windows:WindowDidResize", - 1202: "windows:WindowDPIChanged", - 1203: "windows:WindowDragDrop", - 1204: "windows:WindowDragEnter", - 1205: "windows:WindowDragLeave", - 1206: "windows:WindowDragOver", - 1207: "windows:WindowEndMove", - 1208: "windows:WindowEndResize", - 1209: "windows:WindowFullscreen", - 1210: "windows:WindowHide", - 1211: "windows:WindowInactive", - 1212: "windows:WindowKeyDown", - 1213: "windows:WindowKeyUp", - 1214: "windows:WindowKillFocus", - 1215: "windows:WindowNonClientHit", - 1216: "windows:WindowNonClientMouseDown", - 1217: "windows:WindowNonClientMouseLeave", - 1218: "windows:WindowNonClientMouseMove", - 1219: "windows:WindowNonClientMouseUp", - 1220: "windows:WindowPaint", - 1221: "windows:WindowRestore", - 1222: "windows:WindowSetFocus", - 1223: "windows:WindowShow", - 1224: "windows:WindowStartMove", - 1225: "windows:WindowStartResize", - 1226: "windows:WindowUnFullscreen", - 1227: "windows:WindowZOrderChanged", - 1228: "windows:WindowMinimise", - 1229: "windows:WindowUnMinimise", - 1230: "windows:WindowMaximise", - 1231: "windows:WindowUnMaximise", + 1024: "ApplicationOpenedWithFile", + 1025: "ApplicationStarted", + 1026: "ApplicationLaunchedWithUrl", + 1027: "ThemeChanged", + 1028: "WindowClosing", + 1029: "WindowDidMove", + 1030: "WindowDidResize", + 1031: "WindowDPIChanged", + 1032: "WindowFilesDropped", + 1033: "WindowFocus", + 1034: "WindowFullscreen", + 1035: "WindowHide", + 1036: "WindowLostFocus", + 1037: "WindowMaximise", + 1038: "WindowMinimise", + 1039: "WindowRestore", + 1040: "WindowRuntimeReady", + 1041: "WindowShow", + 1042: "WindowUnFullscreen", + 1043: "WindowUnMaximise", + 1044: "WindowUnMinimise", + 1045: "WindowZoom", + 1046: "WindowZoomIn", + 1047: "WindowZoomOut", + 1048: "WindowZoomReset", + 1049: "ApplicationStartup", + 1050: "SystemThemeChanged", + 1051: "WindowDeleteEvent", + 1052: "WindowDidMove", + 1053: "WindowDidResize", + 1054: "WindowFocusIn", + 1055: "WindowFocusOut", + 1056: "WindowLoadChanged", + 1057: "ApplicationDidBecomeActive", + 1058: "ApplicationDidChangeBackingProperties", + 1059: "ApplicationDidChangeEffectiveAppearance", + 1060: "ApplicationDidChangeIcon", + 1061: "ApplicationDidChangeOcclusionState", + 1062: "ApplicationDidChangeScreenParameters", + 1063: "ApplicationDidChangeStatusBarFrame", + 1064: "ApplicationDidChangeStatusBarOrientation", + 1065: "ApplicationDidChangeTheme", + 1066: "ApplicationDidFinishLaunching", + 1067: "ApplicationDidHide", + 1068: "ApplicationDidResignActive", + 1069: "ApplicationDidUnhide", + 1070: "ApplicationDidUpdate", + 1071: "ApplicationShouldHandleReopen", + 1072: "ApplicationWillBecomeActive", + 1073: "ApplicationWillFinishLaunching", + 1074: "ApplicationWillHide", + 1075: "ApplicationWillResignActive", + 1076: "ApplicationWillTerminate", + 1077: "ApplicationWillUnhide", + 1078: "ApplicationWillUpdate", + 1079: "MenuDidAddItem", + 1080: "MenuDidBeginTracking", + 1081: "MenuDidClose", + 1082: "MenuDidDisplayItem", + 1083: "MenuDidEndTracking", + 1084: "MenuDidHighlightItem", + 1085: "MenuDidOpen", + 1086: "MenuDidPopUp", + 1087: "MenuDidRemoveItem", + 1088: "MenuDidSendAction", + 1089: "MenuDidSendActionToItem", + 1090: "MenuDidUpdate", + 1091: "MenuWillAddItem", + 1092: "MenuWillBeginTracking", + 1093: "MenuWillDisplayItem", + 1094: "MenuWillEndTracking", + 1095: "MenuWillHighlightItem", + 1096: "MenuWillOpen", + 1097: "MenuWillPopUp", + 1098: "MenuWillRemoveItem", + 1099: "MenuWillSendAction", + 1100: "MenuWillSendActionToItem", + 1101: "MenuWillUpdate", + 1102: "WebViewDidCommitNavigation", + 1103: "WebViewDidFinishNavigation", + 1104: "WebViewDidReceiveServerRedirectForProvisionalNavigation", + 1105: "WebViewDidStartProvisionalNavigation", + 1106: "WindowDidBecomeKey", + 1107: "WindowDidBecomeMain", + 1108: "WindowDidBeginSheet", + 1109: "WindowDidChangeAlpha", + 1110: "WindowDidChangeBackingLocation", + 1111: "WindowDidChangeBackingProperties", + 1112: "WindowDidChangeCollectionBehavior", + 1113: "WindowDidChangeEffectiveAppearance", + 1114: "WindowDidChangeOcclusionState", + 1115: "WindowDidChangeOrderingMode", + 1116: "WindowDidChangeScreen", + 1117: "WindowDidChangeScreenParameters", + 1118: "WindowDidChangeScreenProfile", + 1119: "WindowDidChangeScreenSpace", + 1120: "WindowDidChangeScreenSpaceProperties", + 1121: "WindowDidChangeSharingType", + 1122: "WindowDidChangeSpace", + 1123: "WindowDidChangeSpaceOrderingMode", + 1124: "WindowDidChangeTitle", + 1125: "WindowDidChangeToolbar", + 1126: "WindowDidDeminiaturize", + 1127: "WindowDidEndSheet", + 1128: "WindowDidEnterFullScreen", + 1129: "WindowDidEnterVersionBrowser", + 1130: "WindowDidExitFullScreen", + 1131: "WindowDidExitVersionBrowser", + 1132: "WindowDidExpose", + 1133: "WindowDidFocus", + 1134: "WindowDidMiniaturize", + 1135: "WindowDidMove", + 1136: "WindowDidOrderOffScreen", + 1137: "WindowDidOrderOnScreen", + 1138: "WindowDidResignKey", + 1139: "WindowDidResignMain", + 1140: "WindowDidResize", + 1141: "WindowDidUpdate", + 1142: "WindowDidUpdateAlpha", + 1143: "WindowDidUpdateCollectionBehavior", + 1144: "WindowDidUpdateCollectionProperties", + 1145: "WindowDidUpdateShadow", + 1146: "WindowDidUpdateTitle", + 1147: "WindowDidUpdateToolbar", + 1148: "WindowDidZoom", + 1149: "WindowFileDraggingEntered", + 1150: "WindowFileDraggingExited", + 1151: "WindowFileDraggingPerformed", + 1152: "WindowHide", + 1153: "WindowMaximise", + 1154: "WindowUnMaximise", + 1155: "WindowMinimise", + 1156: "WindowUnMinimise", + 1157: "WindowShouldClose", + 1158: "WindowShow", + 1159: "WindowWillBecomeKey", + 1160: "WindowWillBecomeMain", + 1161: "WindowWillBeginSheet", + 1162: "WindowWillChangeOrderingMode", + 1163: "WindowWillClose", + 1164: "WindowWillDeminiaturize", + 1165: "WindowWillEnterFullScreen", + 1166: "WindowWillEnterVersionBrowser", + 1167: "WindowWillExitFullScreen", + 1168: "WindowWillExitVersionBrowser", + 1169: "WindowWillFocus", + 1170: "WindowWillMiniaturize", + 1171: "WindowWillMove", + 1172: "WindowWillOrderOffScreen", + 1173: "WindowWillOrderOnScreen", + 1174: "WindowWillResignMain", + 1175: "WindowWillResize", + 1176: "WindowWillUnfocus", + 1177: "WindowWillUpdate", + 1178: "WindowWillUpdateAlpha", + 1179: "WindowWillUpdateCollectionBehavior", + 1180: "WindowWillUpdateCollectionProperties", + 1181: "WindowWillUpdateShadow", + 1182: "WindowWillUpdateTitle", + 1183: "WindowWillUpdateToolbar", + 1184: "WindowWillUpdateVisibility", + 1185: "WindowWillUseStandardFrame", + 1186: "WindowZoomIn", + 1187: "WindowZoomOut", + 1188: "WindowZoomReset", + 1189: "APMPowerSettingChange", + 1190: "APMPowerStatusChange", + 1191: "APMResumeAutomatic", + 1192: "APMResumeSuspend", + 1193: "APMSuspend", + 1194: "ApplicationStarted", + 1195: "SystemThemeChanged", + 1196: "WebViewNavigationCompleted", + 1197: "WindowActive", + 1198: "WindowBackgroundErase", + 1199: "WindowClickActive", + 1200: "WindowClosing", + 1201: "WindowDidMove", + 1202: "WindowDidResize", + 1203: "WindowDPIChanged", + 1204: "WindowDragDrop", + 1205: "WindowDragEnter", + 1206: "WindowDragLeave", + 1207: "WindowDragOver", + 1208: "WindowEndMove", + 1209: "WindowEndResize", + 1210: "WindowFullscreen", + 1211: "WindowHide", + 1212: "WindowInactive", + 1213: "WindowKeyDown", + 1214: "WindowKeyUp", + 1215: "WindowKillFocus", + 1216: "WindowNonClientHit", + 1217: "WindowNonClientMouseDown", + 1218: "WindowNonClientMouseLeave", + 1219: "WindowNonClientMouseMove", + 1220: "WindowNonClientMouseUp", + 1221: "WindowPaint", + 1222: "WindowRestore", + 1223: "WindowSetFocus", + 1224: "WindowShow", + 1225: "WindowStartMove", + 1226: "WindowStartResize", + 1227: "WindowUnFullscreen", + 1228: "WindowZOrderChanged", + 1229: "WindowMinimise", + 1230: "WindowUnMinimise", + 1231: "WindowMaximise", + 1232: "WindowUnMaximise", } diff --git a/v3/pkg/events/events.txt b/v3/pkg/events/events.txt index 1d43adf5e..3478d3838 100644 --- a/v3/pkg/events/events.txt +++ b/v3/pkg/events/events.txt @@ -1,5 +1,6 @@ common:ApplicationOpenedWithFile common:ApplicationStarted +common:ApplicationLaunchedWithUrl common:ThemeChanged common:WindowClosing common:WindowDidMove @@ -12,6 +13,7 @@ common:WindowHide common:WindowLostFocus common:WindowMaximise common:WindowMinimise +common:WindowToggleFrameless common:WindowRestore common:WindowRuntimeReady common:WindowShow @@ -205,4 +207,4 @@ windows:WindowZOrderChanged windows:WindowMinimise windows:WindowUnMinimise windows:WindowMaximise -windows:WindowUnMaximise \ No newline at end of file +windows:WindowUnMaximise diff --git a/v3/pkg/events/events_darwin.h b/v3/pkg/events/events_darwin.h index bacaab781..31be2554d 100644 --- a/v3/pkg/events/events_darwin.h +++ b/v3/pkg/events/events_darwin.h @@ -6,140 +6,140 @@ extern void processApplicationEvent(unsigned int, void* data); extern void processWindowEvent(unsigned int, unsigned int); -#define EventApplicationDidBecomeActive 1056 -#define EventApplicationDidChangeBackingProperties 1057 -#define EventApplicationDidChangeEffectiveAppearance 1058 -#define EventApplicationDidChangeIcon 1059 -#define EventApplicationDidChangeOcclusionState 1060 -#define EventApplicationDidChangeScreenParameters 1061 -#define EventApplicationDidChangeStatusBarFrame 1062 -#define EventApplicationDidChangeStatusBarOrientation 1063 -#define EventApplicationDidChangeTheme 1064 -#define EventApplicationDidFinishLaunching 1065 -#define EventApplicationDidHide 1066 -#define EventApplicationDidResignActive 1067 -#define EventApplicationDidUnhide 1068 -#define EventApplicationDidUpdate 1069 -#define EventApplicationShouldHandleReopen 1070 -#define EventApplicationWillBecomeActive 1071 -#define EventApplicationWillFinishLaunching 1072 -#define EventApplicationWillHide 1073 -#define EventApplicationWillResignActive 1074 -#define EventApplicationWillTerminate 1075 -#define EventApplicationWillUnhide 1076 -#define EventApplicationWillUpdate 1077 -#define EventMenuDidAddItem 1078 -#define EventMenuDidBeginTracking 1079 -#define EventMenuDidClose 1080 -#define EventMenuDidDisplayItem 1081 -#define EventMenuDidEndTracking 1082 -#define EventMenuDidHighlightItem 1083 -#define EventMenuDidOpen 1084 -#define EventMenuDidPopUp 1085 -#define EventMenuDidRemoveItem 1086 -#define EventMenuDidSendAction 1087 -#define EventMenuDidSendActionToItem 1088 -#define EventMenuDidUpdate 1089 -#define EventMenuWillAddItem 1090 -#define EventMenuWillBeginTracking 1091 -#define EventMenuWillDisplayItem 1092 -#define EventMenuWillEndTracking 1093 -#define EventMenuWillHighlightItem 1094 -#define EventMenuWillOpen 1095 -#define EventMenuWillPopUp 1096 -#define EventMenuWillRemoveItem 1097 -#define EventMenuWillSendAction 1098 -#define EventMenuWillSendActionToItem 1099 -#define EventMenuWillUpdate 1100 -#define EventWebViewDidCommitNavigation 1101 -#define EventWebViewDidFinishNavigation 1102 -#define EventWebViewDidReceiveServerRedirectForProvisionalNavigation 1103 -#define EventWebViewDidStartProvisionalNavigation 1104 -#define EventWindowDidBecomeKey 1105 -#define EventWindowDidBecomeMain 1106 -#define EventWindowDidBeginSheet 1107 -#define EventWindowDidChangeAlpha 1108 -#define EventWindowDidChangeBackingLocation 1109 -#define EventWindowDidChangeBackingProperties 1110 -#define EventWindowDidChangeCollectionBehavior 1111 -#define EventWindowDidChangeEffectiveAppearance 1112 -#define EventWindowDidChangeOcclusionState 1113 -#define EventWindowDidChangeOrderingMode 1114 -#define EventWindowDidChangeScreen 1115 -#define EventWindowDidChangeScreenParameters 1116 -#define EventWindowDidChangeScreenProfile 1117 -#define EventWindowDidChangeScreenSpace 1118 -#define EventWindowDidChangeScreenSpaceProperties 1119 -#define EventWindowDidChangeSharingType 1120 -#define EventWindowDidChangeSpace 1121 -#define EventWindowDidChangeSpaceOrderingMode 1122 -#define EventWindowDidChangeTitle 1123 -#define EventWindowDidChangeToolbar 1124 -#define EventWindowDidDeminiaturize 1125 -#define EventWindowDidEndSheet 1126 -#define EventWindowDidEnterFullScreen 1127 -#define EventWindowDidEnterVersionBrowser 1128 -#define EventWindowDidExitFullScreen 1129 -#define EventWindowDidExitVersionBrowser 1130 -#define EventWindowDidExpose 1131 -#define EventWindowDidFocus 1132 -#define EventWindowDidMiniaturize 1133 -#define EventWindowDidMove 1134 -#define EventWindowDidOrderOffScreen 1135 -#define EventWindowDidOrderOnScreen 1136 -#define EventWindowDidResignKey 1137 -#define EventWindowDidResignMain 1138 -#define EventWindowDidResize 1139 -#define EventWindowDidUpdate 1140 -#define EventWindowDidUpdateAlpha 1141 -#define EventWindowDidUpdateCollectionBehavior 1142 -#define EventWindowDidUpdateCollectionProperties 1143 -#define EventWindowDidUpdateShadow 1144 -#define EventWindowDidUpdateTitle 1145 -#define EventWindowDidUpdateToolbar 1146 -#define EventWindowDidZoom 1147 -#define EventWindowFileDraggingEntered 1148 -#define EventWindowFileDraggingExited 1149 -#define EventWindowFileDraggingPerformed 1150 -#define EventWindowHide 1151 -#define EventWindowMaximise 1152 -#define EventWindowUnMaximise 1153 -#define EventWindowMinimise 1154 -#define EventWindowUnMinimise 1155 -#define EventWindowShouldClose 1156 -#define EventWindowShow 1157 -#define EventWindowWillBecomeKey 1158 -#define EventWindowWillBecomeMain 1159 -#define EventWindowWillBeginSheet 1160 -#define EventWindowWillChangeOrderingMode 1161 -#define EventWindowWillClose 1162 -#define EventWindowWillDeminiaturize 1163 -#define EventWindowWillEnterFullScreen 1164 -#define EventWindowWillEnterVersionBrowser 1165 -#define EventWindowWillExitFullScreen 1166 -#define EventWindowWillExitVersionBrowser 1167 -#define EventWindowWillFocus 1168 -#define EventWindowWillMiniaturize 1169 -#define EventWindowWillMove 1170 -#define EventWindowWillOrderOffScreen 1171 -#define EventWindowWillOrderOnScreen 1172 -#define EventWindowWillResignMain 1173 -#define EventWindowWillResize 1174 -#define EventWindowWillUnfocus 1175 -#define EventWindowWillUpdate 1176 -#define EventWindowWillUpdateAlpha 1177 -#define EventWindowWillUpdateCollectionBehavior 1178 -#define EventWindowWillUpdateCollectionProperties 1179 -#define EventWindowWillUpdateShadow 1180 -#define EventWindowWillUpdateTitle 1181 -#define EventWindowWillUpdateToolbar 1182 -#define EventWindowWillUpdateVisibility 1183 -#define EventWindowWillUseStandardFrame 1184 -#define EventWindowZoomIn 1185 -#define EventWindowZoomOut 1186 -#define EventWindowZoomReset 1187 +#define EventApplicationDidBecomeActive 1057 +#define EventApplicationDidChangeBackingProperties 1058 +#define EventApplicationDidChangeEffectiveAppearance 1059 +#define EventApplicationDidChangeIcon 1060 +#define EventApplicationDidChangeOcclusionState 1061 +#define EventApplicationDidChangeScreenParameters 1062 +#define EventApplicationDidChangeStatusBarFrame 1063 +#define EventApplicationDidChangeStatusBarOrientation 1064 +#define EventApplicationDidChangeTheme 1065 +#define EventApplicationDidFinishLaunching 1066 +#define EventApplicationDidHide 1067 +#define EventApplicationDidResignActive 1068 +#define EventApplicationDidUnhide 1069 +#define EventApplicationDidUpdate 1070 +#define EventApplicationShouldHandleReopen 1071 +#define EventApplicationWillBecomeActive 1072 +#define EventApplicationWillFinishLaunching 1073 +#define EventApplicationWillHide 1074 +#define EventApplicationWillResignActive 1075 +#define EventApplicationWillTerminate 1076 +#define EventApplicationWillUnhide 1077 +#define EventApplicationWillUpdate 1078 +#define EventMenuDidAddItem 1079 +#define EventMenuDidBeginTracking 1080 +#define EventMenuDidClose 1081 +#define EventMenuDidDisplayItem 1082 +#define EventMenuDidEndTracking 1083 +#define EventMenuDidHighlightItem 1084 +#define EventMenuDidOpen 1085 +#define EventMenuDidPopUp 1086 +#define EventMenuDidRemoveItem 1087 +#define EventMenuDidSendAction 1088 +#define EventMenuDidSendActionToItem 1089 +#define EventMenuDidUpdate 1090 +#define EventMenuWillAddItem 1091 +#define EventMenuWillBeginTracking 1092 +#define EventMenuWillDisplayItem 1093 +#define EventMenuWillEndTracking 1094 +#define EventMenuWillHighlightItem 1095 +#define EventMenuWillOpen 1096 +#define EventMenuWillPopUp 1097 +#define EventMenuWillRemoveItem 1098 +#define EventMenuWillSendAction 1099 +#define EventMenuWillSendActionToItem 1100 +#define EventMenuWillUpdate 1101 +#define EventWebViewDidCommitNavigation 1102 +#define EventWebViewDidFinishNavigation 1103 +#define EventWebViewDidReceiveServerRedirectForProvisionalNavigation 1104 +#define EventWebViewDidStartProvisionalNavigation 1105 +#define EventWindowDidBecomeKey 1106 +#define EventWindowDidBecomeMain 1107 +#define EventWindowDidBeginSheet 1108 +#define EventWindowDidChangeAlpha 1109 +#define EventWindowDidChangeBackingLocation 1110 +#define EventWindowDidChangeBackingProperties 1111 +#define EventWindowDidChangeCollectionBehavior 1112 +#define EventWindowDidChangeEffectiveAppearance 1113 +#define EventWindowDidChangeOcclusionState 1114 +#define EventWindowDidChangeOrderingMode 1115 +#define EventWindowDidChangeScreen 1116 +#define EventWindowDidChangeScreenParameters 1117 +#define EventWindowDidChangeScreenProfile 1118 +#define EventWindowDidChangeScreenSpace 1119 +#define EventWindowDidChangeScreenSpaceProperties 1120 +#define EventWindowDidChangeSharingType 1121 +#define EventWindowDidChangeSpace 1122 +#define EventWindowDidChangeSpaceOrderingMode 1123 +#define EventWindowDidChangeTitle 1124 +#define EventWindowDidChangeToolbar 1125 +#define EventWindowDidDeminiaturize 1126 +#define EventWindowDidEndSheet 1127 +#define EventWindowDidEnterFullScreen 1128 +#define EventWindowDidEnterVersionBrowser 1129 +#define EventWindowDidExitFullScreen 1130 +#define EventWindowDidExitVersionBrowser 1131 +#define EventWindowDidExpose 1132 +#define EventWindowDidFocus 1133 +#define EventWindowDidMiniaturize 1134 +#define EventWindowDidMove 1135 +#define EventWindowDidOrderOffScreen 1136 +#define EventWindowDidOrderOnScreen 1137 +#define EventWindowDidResignKey 1138 +#define EventWindowDidResignMain 1139 +#define EventWindowDidResize 1140 +#define EventWindowDidUpdate 1141 +#define EventWindowDidUpdateAlpha 1142 +#define EventWindowDidUpdateCollectionBehavior 1143 +#define EventWindowDidUpdateCollectionProperties 1144 +#define EventWindowDidUpdateShadow 1145 +#define EventWindowDidUpdateTitle 1146 +#define EventWindowDidUpdateToolbar 1147 +#define EventWindowDidZoom 1148 +#define EventWindowFileDraggingEntered 1149 +#define EventWindowFileDraggingExited 1150 +#define EventWindowFileDraggingPerformed 1151 +#define EventWindowHide 1152 +#define EventWindowMaximise 1153 +#define EventWindowUnMaximise 1154 +#define EventWindowMinimise 1155 +#define EventWindowUnMinimise 1156 +#define EventWindowShouldClose 1157 +#define EventWindowShow 1158 +#define EventWindowWillBecomeKey 1159 +#define EventWindowWillBecomeMain 1160 +#define EventWindowWillBeginSheet 1161 +#define EventWindowWillChangeOrderingMode 1162 +#define EventWindowWillClose 1163 +#define EventWindowWillDeminiaturize 1164 +#define EventWindowWillEnterFullScreen 1165 +#define EventWindowWillEnterVersionBrowser 1166 +#define EventWindowWillExitFullScreen 1167 +#define EventWindowWillExitVersionBrowser 1168 +#define EventWindowWillFocus 1169 +#define EventWindowWillMiniaturize 1170 +#define EventWindowWillMove 1171 +#define EventWindowWillOrderOffScreen 1172 +#define EventWindowWillOrderOnScreen 1173 +#define EventWindowWillResignMain 1174 +#define EventWindowWillResize 1175 +#define EventWindowWillUnfocus 1176 +#define EventWindowWillUpdate 1177 +#define EventWindowWillUpdateAlpha 1178 +#define EventWindowWillUpdateCollectionBehavior 1179 +#define EventWindowWillUpdateCollectionProperties 1180 +#define EventWindowWillUpdateShadow 1181 +#define EventWindowWillUpdateTitle 1182 +#define EventWindowWillUpdateToolbar 1183 +#define EventWindowWillUpdateVisibility 1184 +#define EventWindowWillUseStandardFrame 1185 +#define EventWindowZoomIn 1186 +#define EventWindowZoomOut 1187 +#define EventWindowZoomReset 1188 -#define MAX_EVENTS 1188 +#define MAX_EVENTS 1189 #endif \ No newline at end of file diff --git a/v3/pkg/events/events_linux.h b/v3/pkg/events/events_linux.h index c3ff72f5d..9f6ba67b3 100644 --- a/v3/pkg/events/events_linux.h +++ b/v3/pkg/events/events_linux.h @@ -6,16 +6,16 @@ extern void processApplicationEvent(unsigned int, void* data); extern void processWindowEvent(unsigned int, unsigned int); -#define EventApplicationStartup 1048 -#define EventSystemThemeChanged 1049 -#define EventWindowDeleteEvent 1050 -#define EventWindowDidMove 1051 -#define EventWindowDidResize 1052 -#define EventWindowFocusIn 1053 -#define EventWindowFocusOut 1054 -#define EventWindowLoadChanged 1055 +#define EventApplicationStartup 1049 +#define EventSystemThemeChanged 1050 +#define EventWindowDeleteEvent 1051 +#define EventWindowDidMove 1052 +#define EventWindowDidResize 1053 +#define EventWindowFocusIn 1054 +#define EventWindowFocusOut 1055 +#define EventWindowLoadChanged 1056 -#define MAX_EVENTS 1056 +#define MAX_EVENTS 1057 #endif \ No newline at end of file diff --git a/v3/pkg/services/badge/badge.go b/v3/pkg/services/badge/badge.go new file mode 100644 index 000000000..29b363082 --- /dev/null +++ b/v3/pkg/services/badge/badge.go @@ -0,0 +1,60 @@ +package badge + +import ( + "context" + "image/color" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +type platformBadge interface { + // Lifecycle methods + Startup(ctx context.Context, options application.ServiceOptions) error + Shutdown() error + + SetBadge(label string) error + SetCustomBadge(label string, options Options) error + RemoveBadge() error +} + +// Service represents the notifications service +type BadgeService struct { + impl platformBadge +} + +type Options struct { + TextColour color.RGBA + BackgroundColour color.RGBA + FontName string + FontSize int + SmallFontSize int +} + +// ServiceName returns the name of the service. +func (b *BadgeService) ServiceName() string { + return "github.com/wailsapp/wails/v3/services/badge" +} + +// ServiceStartup is called when the service is loaded. +func (b *BadgeService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return b.impl.Startup(ctx, options) +} + +// ServiceShutdown is called when the service is unloaded. +func (b *BadgeService) ServiceShutdown() error { + return b.impl.Shutdown() +} + +// SetBadge sets the badge label on the application icon. +func (b *BadgeService) SetBadge(label string) error { + return b.impl.SetBadge(label) +} + +func (b *BadgeService) SetCustomBadge(label string, options Options) error { + return b.impl.SetCustomBadge(label, options) +} + +// RemoveBadge removes the badge label from the application icon. +func (b *BadgeService) RemoveBadge() error { + return b.impl.RemoveBadge() +} diff --git a/v3/pkg/services/badge/badge_darwin.go b/v3/pkg/services/badge/badge_darwin.go new file mode 100644 index 000000000..cf61c71ca --- /dev/null +++ b/v3/pkg/services/badge/badge_darwin.go @@ -0,0 +1,74 @@ +//go:build darwin + +package badge + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Cocoa +#import + +static void setBadge(const char *label) { + NSString *nsLabel = nil; + if (label != NULL) { + nsLabel = [NSString stringWithUTF8String:label]; + } + [[NSApp dockTile] setBadgeLabel:nsLabel]; + [[NSApp dockTile] display]; +} +*/ +import "C" +import ( + "context" + "unsafe" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +type darwinBadge struct{} + +// Creates a new Badge Service. +func New() *BadgeService { + return &BadgeService{ + impl: &darwinBadge{}, + } +} + +// NewWithOptions creates a new badge service with the given options. +// Currently, options are not available on macOS and are ignored. +// (Windows-specific) +func NewWithOptions(options Options) *BadgeService { + return New() +} + +func (d *darwinBadge) Startup(ctx context.Context, options application.ServiceOptions) error { + return nil +} + +func (d *darwinBadge) Shutdown() error { + return nil +} + +// SetBadge sets the badge label on the application icon. +func (d *darwinBadge) SetBadge(label string) error { + var cLabel *C.char + if label != "" { + cLabel = C.CString(label) + defer C.free(unsafe.Pointer(cLabel)) + } else { + cLabel = C.CString("●") // Default badge character + } + C.setBadge(cLabel) + return nil +} + +// SetCustomBadge is not supported on macOS, SetBadge is called instead. +// (Windows-specific) +func (d *darwinBadge) SetCustomBadge(label string, options Options) error { + return d.SetBadge(label) +} + +// RemoveBadge removes the badge label from the application icon. +func (d *darwinBadge) RemoveBadge() error { + C.setBadge(nil) + return nil +} diff --git a/v3/pkg/services/badge/badge_linux.go b/v3/pkg/services/badge/badge_linux.go new file mode 100644 index 000000000..aba02f47d --- /dev/null +++ b/v3/pkg/services/badge/badge_linux.go @@ -0,0 +1,58 @@ +//go:build linux + +package badge + +import ( + "context" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +type linuxBadge struct{} + +// New creates a new Badge Service. +// On Linux, this returns a no-op implementation since most desktop environments +// don't have standardized dock badge functionality. +func New() *BadgeService { + return &BadgeService{ + impl: &linuxBadge{}, + } +} + +// NewWithOptions creates a new badge service with the given options. +// On Linux, this returns a no-op implementation since most desktop environments +// don't have standardized dock badge functionality. Options are ignored. +func NewWithOptions(options Options) *BadgeService { + return New() +} + +func (l *linuxBadge) Startup(ctx context.Context, options application.ServiceOptions) error { + // No-op: Linux doesn't have standardized badge support + return nil +} + +func (l *linuxBadge) Shutdown() error { + // No-op: Linux doesn't have standardized badge support + return nil +} + +// SetBadge is a no-op on Linux since most desktop environments don't support +// application dock badges. This method exists for cross-platform compatibility. +func (l *linuxBadge) SetBadge(label string) error { + // No-op: Linux doesn't have standardized badge support + return nil +} + +// SetCustomBadge is a no-op on Linux since most desktop environments don't support +// application dock badges. This method exists for cross-platform compatibility. +func (l *linuxBadge) SetCustomBadge(label string, options Options) error { + // No-op: Linux doesn't have standardized badge support + return nil +} + +// RemoveBadge is a no-op on Linux since most desktop environments don't support +// application dock badges. This method exists for cross-platform compatibility. +func (l *linuxBadge) RemoveBadge() error { + // No-op: Linux doesn't have standardized badge support + return nil +} diff --git a/v3/pkg/services/badge/badge_windows.go b/v3/pkg/services/badge/badge_windows.go new file mode 100644 index 000000000..d7206c87c --- /dev/null +++ b/v3/pkg/services/badge/badge_windows.go @@ -0,0 +1,374 @@ +//go:build windows + +package badge + +import ( + "bytes" + "context" + "image" + "image/color" + "image/png" + "os" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/w32" + "golang.org/x/image/font" + "golang.org/x/image/font/opentype" + "golang.org/x/image/math/fixed" +) + +type windowsBadge struct { + taskbar *w32.ITaskbarList3 + badgeImg *image.RGBA + badgeSize int + fontManager *FontManager + options Options +} + +var defaultOptions = Options{ + TextColour: color.RGBA{255, 255, 255, 255}, + BackgroundColour: color.RGBA{255, 0, 0, 255}, + FontName: "segoeuib.ttf", + FontSize: 18, + SmallFontSize: 14, +} + +// Creates a new Badge Service. +func New() *BadgeService { + return &BadgeService{ + impl: &windowsBadge{ + options: defaultOptions, + }, + } +} + +// NewWithOptions creates a new badge service with the given options. +func NewWithOptions(options Options) *BadgeService { + return &BadgeService{ + impl: &windowsBadge{ + options: options, + }, + } +} + +func (w *windowsBadge) Startup(ctx context.Context, options application.ServiceOptions) error { + taskbar, err := w32.NewTaskbarList3() + if err != nil { + return err + } + w.taskbar = taskbar + w.fontManager = NewFontManager() + + return nil +} + +func (w *windowsBadge) Shutdown() error { + if w.taskbar != nil { + w.taskbar.Release() + } + + return nil +} + +// SetBadge sets the badge label on the application icon. +func (w *windowsBadge) SetBadge(label string) error { + if w.taskbar == nil { + return nil + } + + app := application.Get() + if app == nil { + return nil + } + + window := app.Window.Current() + if window == nil { + return nil + } + + hwnd, err := window.NativeWindowHandle() + if err != nil { + return err + } + + w.createBadge() + + var hicon w32.HICON + if label == "" { + hicon, err = w.createBadgeIcon() + if err != nil { + return err + } + } else { + hicon, err = w.createBadgeIconWithText(label) + if err != nil { + return err + } + } + defer w32.DestroyIcon(hicon) + + return w.taskbar.SetOverlayIcon(hwnd, hicon, nil) +} + +// SetCustomBadge sets the badge label on the application icon with one-off options. +func (w *windowsBadge) SetCustomBadge(label string, options Options) error { + if w.taskbar == nil { + return nil + } + + app := application.Get() + if app == nil { + return nil + } + + window := app.Window.Current() + if window == nil { + return nil + } + + hwnd, err := window.NativeWindowHandle() + if err != nil { + return err + } + + const badgeSize = 32 + + img := image.NewRGBA(image.Rect(0, 0, badgeSize, badgeSize)) + + backgroundColour := options.BackgroundColour + radius := badgeSize / 2 + centerX, centerY := radius, radius + + for y := 0; y < badgeSize; y++ { + for x := 0; x < badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(radius*radius) { + img.Set(x, y, backgroundColour) + } + } + } + + var hicon w32.HICON + if label == "" { + hicon, err = createBadgeIcon(badgeSize, img, options) + if err != nil { + return err + } + } else { + hicon, err = createBadgeIconWithText(w, label, badgeSize, img, options) + if err != nil { + return err + } + } + defer w32.DestroyIcon(hicon) + + return w.taskbar.SetOverlayIcon(hwnd, hicon, nil) +} + +// RemoveBadge removes the badge label from the application icon. +func (w *windowsBadge) RemoveBadge() error { + if w.taskbar == nil { + return nil + } + + app := application.Get() + if app == nil { + return nil + } + + window := app.Window.Current() + if window == nil { + return nil + } + + hwnd, err := window.NativeWindowHandle() + if err != nil { + return err + } + + return w.taskbar.SetOverlayIcon(hwnd, 0, nil) +} + +// createBadgeIcon creates a badge icon with the specified size and color. +func (w *windowsBadge) createBadgeIcon() (w32.HICON, error) { + radius := w.badgeSize / 2 + centerX, centerY := radius, radius + innerRadius := w.badgeSize / 5 + + for y := 0; y < w.badgeSize; y++ { + for x := 0; x < w.badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(innerRadius*innerRadius) { + w.badgeImg.Set(x, y, w.options.TextColour) + } + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, w.badgeImg); err != nil { + return 0, err + } + + hicon, err := w32.CreateSmallHIconFromImage(buf.Bytes()) + return hicon, err +} + +func createBadgeIcon(badgeSize int, img *image.RGBA, options Options) (w32.HICON, error) { + radius := badgeSize / 2 + centerX, centerY := radius, radius + innerRadius := badgeSize / 5 + + for y := 0; y < badgeSize; y++ { + for x := 0; x < badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(innerRadius*innerRadius) { + img.Set(x, y, options.TextColour) + } + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return 0, err + } + + hicon, err := w32.CreateSmallHIconFromImage(buf.Bytes()) + return hicon, err +} + +// createBadgeIconWithText creates a badge icon with the specified text. +func (w *windowsBadge) createBadgeIconWithText(label string) (w32.HICON, error) { + fontPath := w.fontManager.FindFontOrDefault(w.options.FontName) + if fontPath == "" { + return w.createBadgeIcon() + } + + fontBytes, err := os.ReadFile(fontPath) + if err != nil { + return w.createBadgeIcon() + } + + ttf, err := opentype.Parse(fontBytes) + if err != nil { + return w.createBadgeIcon() + } + + fontSize := float64(w.options.FontSize) + if len(label) > 1 { + fontSize = float64(w.options.SmallFontSize) + } + + // Get DPI of the current screen + screen := w32.GetDesktopWindow() + dpi := w32.GetDpiForWindow(screen) + + face, err := opentype.NewFace(ttf, &opentype.FaceOptions{ + Size: fontSize, + DPI: float64(dpi), + Hinting: font.HintingFull, + }) + if err != nil { + return w.createBadgeIcon() + } + defer face.Close() + + d := &font.Drawer{ + Dst: w.badgeImg, + Src: image.NewUniform(w.options.TextColour), + Face: face, + } + + textWidth := d.MeasureString(label).Ceil() + d.Dot = fixed.P((w.badgeSize-textWidth)/2, int(float64(w.badgeSize)/2+fontSize/2)) + d.DrawString(label) + + var buf bytes.Buffer + if err := png.Encode(&buf, w.badgeImg); err != nil { + return 0, err + } + + return w32.CreateSmallHIconFromImage(buf.Bytes()) +} + +func createBadgeIconWithText(w *windowsBadge, label string, badgeSize int, img *image.RGBA, options Options) (w32.HICON, error) { + fontPath := w.fontManager.FindFontOrDefault(options.FontName) + if fontPath == "" { + return createBadgeIcon(badgeSize, img, options) + } + + fontBytes, err := os.ReadFile(fontPath) + if err != nil { + return createBadgeIcon(badgeSize, img, options) + } + + ttf, err := opentype.Parse(fontBytes) + if err != nil { + return createBadgeIcon(badgeSize, img, options) + } + + fontSize := float64(options.FontSize) + if len(label) > 1 { + fontSize = float64(options.SmallFontSize) + } + + // Get DPI of the current screen + screen := w32.GetDesktopWindow() + dpi := w32.GetDpiForWindow(screen) + + face, err := opentype.NewFace(ttf, &opentype.FaceOptions{ + Size: fontSize, + DPI: float64(dpi), + Hinting: font.HintingFull, + }) + if err != nil { + return createBadgeIcon(badgeSize, img, options) + } + defer face.Close() + + d := &font.Drawer{ + Dst: img, + Src: image.NewUniform(options.TextColour), + Face: face, + } + + textWidth := d.MeasureString(label).Ceil() + d.Dot = fixed.P((badgeSize-textWidth)/2, int(float64(badgeSize)/2+fontSize/2)) + d.DrawString(label) + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return 0, err + } + + return w32.CreateSmallHIconFromImage(buf.Bytes()) +} + +// createBadge creates a circular badge with the specified background color. +func (w *windowsBadge) createBadge() { + w.badgeSize = 32 + + img := image.NewRGBA(image.Rect(0, 0, w.badgeSize, w.badgeSize)) + + backgroundColour := w.options.BackgroundColour + radius := w.badgeSize / 2 + centerX, centerY := radius, radius + + for y := 0; y < w.badgeSize; y++ { + for x := 0; x < w.badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(radius*radius) { + img.Set(x, y, backgroundColour) + } + } + } + + w.badgeImg = img +} diff --git a/v3/pkg/services/badge/font.go b/v3/pkg/services/badge/font.go new file mode 100644 index 000000000..430a9c14d --- /dev/null +++ b/v3/pkg/services/badge/font.go @@ -0,0 +1,153 @@ +//go:build windows + +package badge + +import ( + "errors" + "os" + "path/filepath" + "strings" + "sync" + + "golang.org/x/sys/windows/registry" +) + +// FontManager handles font discovery on Windows with minimal caching +type FontManager struct { + fontCache map[string]string // Maps only requested font filenames to paths + fontDirs []string // Directories to search for fonts + mu sync.RWMutex // Mutex for thread-safe access to the cache + registryPaths []string // Registry paths to search for fonts +} + +// NewFontManager creates a new FontManager instance +func NewFontManager() *FontManager { + return &FontManager{ + fontCache: make(map[string]string), + fontDirs: []string{ + filepath.Join(os.Getenv("windir"), "Fonts"), + filepath.Join(os.Getenv("localappdata"), "Microsoft", "Windows", "Fonts"), + }, + registryPaths: []string{ + `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`, + }, + } +} + +// FindFont searches for a font by filename and returns its full path +// Only caches fonts that are found +func (fm *FontManager) FindFont(fontFilename string) (string, error) { + fontKey := strings.ToLower(fontFilename) + + // Check if already in cache + fm.mu.RLock() + if path, exists := fm.fontCache[fontKey]; exists { + fm.mu.RUnlock() + return path, nil + } + fm.mu.RUnlock() + + // If not in cache, search for the font + fontPath, err := fm.searchForFont(fontFilename) + if err != nil { + return "", err + } + + // Add to cache only if found + fm.mu.Lock() + fm.fontCache[fontKey] = fontPath + fm.mu.Unlock() + + return fontPath, nil +} + +// searchForFont looks for a font in all known locations +func (fm *FontManager) searchForFont(fontFilename string) (string, error) { + fontFileLower := strings.ToLower(fontFilename) + + // 1. Direct file check in font directories (fastest approach) + for _, dir := range fm.fontDirs { + fontPath := filepath.Join(dir, fontFilename) + if fileExists(fontPath) { + return fontPath, nil + } + } + + // 2. Search in registry (can find fonts with different paths) + // System fonts (HKEY_LOCAL_MACHINE) + for _, regPath := range fm.registryPaths { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, regPath, registry.QUERY_VALUE) + if err == nil { + defer k.Close() + + // Look for the specific font in registry values + fontPath, found := fm.findFontInRegistry(k, fontFileLower, fm.fontDirs[0]) + if found { + return fontPath, nil + } + } + } + + // 3. User fonts (HKEY_CURRENT_USER) + for _, regPath := range fm.registryPaths { + k, err := registry.OpenKey(registry.CURRENT_USER, regPath, registry.QUERY_VALUE) + if err == nil { + defer k.Close() + + // Look for the specific font in registry values + fontPath, found := fm.findFontInRegistry(k, fontFileLower, fm.fontDirs[1]) + if found { + return fontPath, nil + } + } + } + + return "", errors.New("font not found: " + fontFilename) +} + +// findFontInRegistry searches for a specific font in a registry key +func (fm *FontManager) findFontInRegistry(k registry.Key, fontFileLower string, defaultDir string) (string, bool) { + valueNames, err := k.ReadValueNames(0) + if err != nil { + return "", false + } + + for _, name := range valueNames { + value, _, err := k.GetStringValue(name) + if err != nil { + continue + } + + // Check if this registry entry corresponds to our font + valueLower := strings.ToLower(value) + if strings.HasSuffix(valueLower, fontFileLower) { + // If it's a relative path, assume it's in the default font directory + if !strings.Contains(value, "\\") { + value = filepath.Join(defaultDir, value) + } + + if fileExists(value) { + return value, true + } + } + } + + return "", false +} + +func (fm *FontManager) FindFontOrDefault(name string) string { + fontsToFind := []string{name, "segoeuib.ttf", "arialbd.ttf"} + for _, font := range fontsToFind { + path, err := fm.FindFont(font) + if err == nil { + return path + } + } + return "" +} + +// Helper functions +func fileExists(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} diff --git a/v3/pkg/services/fileserver/fileserver.go b/v3/pkg/services/fileserver/fileserver.go index f84b64945..de06f5c1a 100644 --- a/v3/pkg/services/fileserver/fileserver.go +++ b/v3/pkg/services/fileserver/fileserver.go @@ -1,51 +1,54 @@ package fileserver import ( - "context" "net/http" - - "github.com/wailsapp/wails/v3/pkg/application" + "sync/atomic" ) -// ---------------- Service Setup ---------------- -// This is the main Service struct. It can be named anything you like. - type Config struct { + // RootPath specifies the filesystem path from which requests are to be served. RootPath string } -type Service struct { - config *Config - fs http.Handler +type FileserverService struct { + fs atomic.Pointer[http.Handler] } -func New(config *Config) *Service { - return &Service{ - config: config, - fs: http.FileServer(http.Dir(config.RootPath)), - } +// New initialises an unconfigured fileserver. See [Configure] for details. +func New() *FileserverService { + return NewWithConfig(nil) } -// ServiceShutdown is called when the app is shutting down -// You can use this to clean up any resources you have allocated -func (s *Service) ServiceShutdown() error { - return nil +// New initialises and optionally configures a fileserver. See [Service.Configure] for details. +func NewWithConfig(config *Config) *FileserverService { + result := &FileserverService{} + result.Configure(config) + return result } // ServiceName returns the name of the plugin. // You should use the go module format e.g. github.com/myuser/myplugin -func (s *Service) ServiceName() string { +func (s *FileserverService) ServiceName() string { return "github.com/wailsapp/wails/v3/services/fileserver" } -// ServiceStartup is called when the app is starting up. You can use this to -// initialise any resources you need. -func (s *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { - // Any initialization code here - return nil +// Configure reconfigures the fileserver. +// If config is nil, then every request will receive a 503 Service Unavailable response. +// +//wails:ignore +func (s *FileserverService) Configure(config *Config) { + if config == nil { + s.fs.Store(&dummyHandler) + } else { + var fs http.Handler = http.FileServer(http.Dir(config.RootPath)) + s.fs.Store(&fs) + } } -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Create a new file server rooted at the given path - s.fs.ServeHTTP(w, r) +func (s *FileserverService) ServeHTTP(w http.ResponseWriter, r *http.Request) { + (*s.fs.Load()).ServeHTTP(w, r) } + +var dummyHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Fileserver service has not been configured yet", http.StatusServiceUnavailable) +}) diff --git a/v3/pkg/services/kvstore/kvstore.go b/v3/pkg/services/kvstore/kvstore.go index ca46dccb5..899dd86ec 100644 --- a/v3/pkg/services/kvstore/kvstore.go +++ b/v3/pkg/services/kvstore/kvstore.go @@ -3,7 +3,6 @@ package kvstore import ( "context" "encoding/json" - "io" "os" "sync" @@ -11,111 +10,147 @@ import ( "github.com/wailsapp/wails/v3/pkg/application" ) -type KeyValueStore struct { - config *Config - filename string - data map[string]any - unsaved bool - lock sync.RWMutex -} - type Config struct { + // Filename specifies the path of the on-disk file associated to the key-value store. Filename string + + // AutoSave specifies whether the store + // must be written to disk automatically after every modification. + // When AutoSave is false, stores are only saved to disk upon shutdown + // or when the [Service.Save] method is called manually. AutoSave bool } -type Service struct{} +type KVStoreService struct { + lock sync.RWMutex -func New(config *Config) *KeyValueStore { - return &KeyValueStore{ - config: config, - data: make(map[string]any), - } + config *Config + + data map[string]any + unsaved bool } -// ServiceShutdown will save the store to disk if there are unsaved changes. -func (kvs *KeyValueStore) ServiceShutdown() error { - if kvs.unsaved { - err := kvs.Save() - if err != nil { - return errors.Wrap(err, "Error saving store") - } - } - return nil +// New initialises an in-memory key-value store. See [NewWithConfig] for details. +func New() *KVStoreService { + return NewWithConfig(nil) +} + +// NewWithConfig initialises a key-value store with the given configuration: +// - if config is nil, the new store is in-memory, i.e. not associated with a file; +// - if config is non-nil, the associated file is not loaded until [Service.Load] is called. +// +// If the store is registered with the application as a service, +// [Service.Load] will be called automatically at startup. +func NewWithConfig(config *Config) *KVStoreService { + result := &KVStoreService{data: make(map[string]any)} + result.Configure(config) + return result } // ServiceName returns the name of the plugin. -func (kvs *KeyValueStore) ServiceName() string { +func (kvs *KVStoreService) ServiceName() string { return "github.com/wailsapp/wails/v3/plugins/kvstore" } -// ServiceStartup is called when the plugin is loaded. This is where you should do any setup. -func (kvs *KeyValueStore) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { - err := kvs.open(kvs.config.Filename) - if err != nil { - return err - } - - return nil +// ServiceStartup loads the store from disk if it is associated with a file. +// It returns a non-nil error in case of failure. +func (kvs *KVStoreService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return errors.Wrap(kvs.Load(), "error loading store") } -func (kvs *KeyValueStore) open(filename string) (err error) { - kvs.filename = filename - kvs.data = make(map[string]any) +// ServiceShutdown saves the store to disk if it is associated with a file. +// It returns a non-nil error in case of failure. +func (kvs *KVStoreService) ServiceShutdown() error { + return errors.Wrap(kvs.Save(), "error saving store") +} - file, err := os.Open(filename) +// Configure changes the store's configuration. +// The contents of the store at call time are preserved and marked unsaved. +// Consumers will need to call [Service.Load] manually after Configure +// in order to load a new file. +// +// If the store is unsaved upon calling Configure, no attempt is made at saving it. +// Consumers will need to call [Service.Save] manually beforehand. +// +// See [NewWithConfig] for details on configuration. +// +//wails:ignore +func (kvs *KVStoreService) Configure(config *Config) { + if config != nil { + // Clone to prevent changes from the outside. + clone := new(Config) + *clone = *config + config = clone + } + + kvs.lock.Lock() + defer kvs.lock.Unlock() + + kvs.config = config + kvs.unsaved = true +} + +// Load loads the store from disk. +// If the store is in-memory, i.e. not associated with a file, Load has no effect. +// If the operation fails, a non-nil error is returned +// and the store's content and state at call time are preserved. +func (kvs *KVStoreService) Load() error { + kvs.lock.Lock() + defer kvs.lock.Unlock() + + if kvs.config == nil { + return nil + } + + bytes, err := os.ReadFile(kvs.config.Filename) if err != nil { if os.IsNotExist(err) { return nil - } - return err - } - defer func() { - err2 := file.Close() - if err2 != nil { - application.Get().Logger.Error("Key/Value Store Plugin Error:", "error", err.Error()) - if err == nil { - err = err2 - } - } - }() - - bytes, err := io.ReadAll(file) - if err != nil { - return err - } - - if len(bytes) > 0 { - if err := json.Unmarshal(bytes, &kvs.data); err != nil { + } else { return err } } + // Init new map because [json.Unmarshal] does not clear the previous one. + data := make(map[string]any) + + if len(bytes) > 0 { + if err := json.Unmarshal(bytes, &data); err != nil { + return err + } + } + + kvs.data = data + kvs.unsaved = false return nil } -// Save saves the store to disk -func (kvs *KeyValueStore) Save() error { +// Save saves the store to disk. +// If the store is in-memory, i.e. not associated with a file, Save has no effect. +func (kvs *KVStoreService) Save() error { kvs.lock.Lock() defer kvs.lock.Unlock() + if kvs.config == nil { + return nil + } + bytes, err := json.Marshal(kvs.data) if err != nil { return err } - err = os.WriteFile(kvs.filename, bytes, 0644) + err = os.WriteFile(kvs.config.Filename, bytes, 0644) if err != nil { return err } kvs.unsaved = false - return nil } // Get returns the value for the given key. If key is empty, the entire store is returned. -func (kvs *KeyValueStore) Get(key string) any { +func (kvs *KVStoreService) Get(key string) any { kvs.lock.RLock() defer kvs.lock.RUnlock() @@ -127,35 +162,67 @@ func (kvs *KeyValueStore) Get(key string) any { } // Set sets the value for the given key. If AutoSave is true, the store is saved to disk. -func (kvs *KeyValueStore) Set(key string, value any) error { - kvs.lock.Lock() - kvs.data[key] = value - kvs.lock.Unlock() - if kvs.config.AutoSave { - err := kvs.Save() - if err != nil { - return err - } - kvs.unsaved = false - } else { +func (kvs *KVStoreService) Set(key string, value any) error { + var autosave bool + func() { + kvs.lock.Lock() + defer kvs.lock.Unlock() + + kvs.data[key] = value kvs.unsaved = true + + if kvs.config != nil { + autosave = kvs.config.AutoSave + } + }() + + if autosave { + return kvs.Save() + } else { + return nil } - return nil } -// Delete deletes the key from the store. If AutoSave is true, the store is saved to disk. -func (kvs *KeyValueStore) Delete(key string) error { - kvs.lock.Lock() - delete(kvs.data, key) - kvs.lock.Unlock() - if kvs.config.AutoSave { - err := kvs.Save() - if err != nil { - return err - } - kvs.unsaved = false - } else { +// Delete deletes the given key from the store. If AutoSave is true, the store is saved to disk. +func (kvs *KVStoreService) Delete(key string) error { + var autosave bool + func() { + kvs.lock.Lock() + defer kvs.lock.Unlock() + + delete(kvs.data, key) kvs.unsaved = true + + if kvs.config != nil { + autosave = kvs.config.AutoSave + } + }() + + if autosave { + return kvs.Save() + } else { + return nil + } +} + +// Clear deletes all keys from the store. If AutoSave is true, the store is saved to disk. +func (kvs *KVStoreService) Clear() error { + var autosave bool + func() { + kvs.lock.Lock() + defer kvs.lock.Unlock() + + kvs.data = make(map[string]any) + kvs.unsaved = true + + if kvs.config != nil { + autosave = kvs.config.AutoSave + } + }() + + if autosave { + return kvs.Save() + } else { + return nil } - return nil } diff --git a/v3/pkg/services/log/log.go b/v3/pkg/services/log/log.go index dd1aafac1..3c8c6300c 100644 --- a/v3/pkg/services/log/log.go +++ b/v3/pkg/services/log/log.go @@ -4,74 +4,192 @@ import ( "context" _ "embed" "log/slog" + "sync/atomic" "github.com/wailsapp/wails/v3/pkg/application" ) +// A Level is the importance or severity of a log event. +// The higher the level, the more important or severe the event. +// +// Values are arbitrary, but there are four predefined ones. +type Level = int + +const ( + Debug = Level(slog.LevelDebug) + Info = Level(slog.LevelInfo) + Warning = Level(slog.LevelWarn) + Error = Level(slog.LevelError) +) + type Config struct { // Logger is the logger to use. If not set, a default logger will be used. Logger *slog.Logger // LogLevel defines the log level of the logger. LogLevel slog.Level - - // Handles errors that occur when writing to the log - ErrorHandler func(err error) } -type LoggerService struct { - config *Config - app *application.App +//wails:inject export { +//wails:inject DebugContext as Debug, +//wails:inject InfoContext as Info, +//wails:inject WarningContext as Warning, +//wails:inject ErrorContext as Error, +//wails:inject }; +type LogService struct { + config atomic.Pointer[Config] level slog.LevelVar } -func NewLoggerService(config *Config) *LoggerService { - if config.Logger == nil { - config.Logger = application.DefaultLogger(config.LogLevel) - } +// New initialises a logging service with the default configuration. +func New() *LogService { + return NewWithConfig(nil) +} - result := &LoggerService{ - config: config, - } - result.level.Set(config.LogLevel) +// NewWithConfig initialises a logging service with a custom configuration. +func NewWithConfig(config *Config) *LogService { + result := &LogService{} + result.Configure(config) return result } -func New() *LoggerService { - return NewLoggerService(&Config{}) -} - -// ServiceShutdown is called when the app is shutting down -// You can use this to clean up any resources you have allocated -func (l *LoggerService) ServiceShutdown() error { return nil } - // ServiceName returns the name of the plugin. // You should use the go module format e.g. github.com/myuser/myplugin -func (l *LoggerService) ServiceName() string { +func (l *LogService) ServiceName() string { return "github.com/wailsapp/wails/v3/plugins/log" } -func (l *LoggerService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { - // Any initialization code here - return nil +// Configure reconfigures the logger dynamically. +// If config is nil, it falls back to the default configuration. +// +//wails:ignore +func (l *LogService) Configure(config *Config) { + if config == nil { + config = &Config{} + } else { + // Clone to prevent changes from the outside. + clone := new(Config) + *clone = *config + config = clone + } + + l.level.Set(slog.Level(config.LogLevel)) + + if config.Logger == nil { + config.Logger = application.DefaultLogger(&l.level) + } + + l.config.Store(config) } -func (l *LoggerService) Debug(message string, args ...any) { - l.config.Logger.Debug(message, args...) +// Level returns the currently configured log level, +// that is either the one configured initially +// or the last value passed to [Service.SetLogLevel]. +// +// Through this method, [Service] implements the [slog.Leveler] interface. +// The intended use case is to propagate +// the service's dynamic level setting to custom loggers. +// For example: +// +// logService := log.New() +// customLogger := slog.New(slog.NewTextHandler( +// customWriter, +// &slog.HandlerOptions{ +// Level: logService, +// }, +// )) +// logService.Configure(&log.Config{ +// Logger: customLogger +// }) +// +// By doing so, setting updates made through [Service.SetLogLevel] +// will propagate dynamically to the custom logger. +// +//wails:ignore +func (l *LogService) Level() slog.Level { + return l.level.Level() } -func (l *LoggerService) Info(message string, args ...any) { - l.config.Logger.Info(message, args...) +// LogLevel returns the currently configured log level, +// that is either the one configured initially +// or the last value passed to [Service.SetLogLevel]. +func (l *LogService) LogLevel() Level { + return Level(l.Level()) } -func (l *LoggerService) Warning(message string, args ...any) { - l.config.Logger.Warn(message, args...) +// SetLogLevel changes the current log level. +func (l *LogService) SetLogLevel(level Level) { + l.level.Set(slog.Level(level)) } -func (l *LoggerService) Error(message string, args ...any) { - l.config.Logger.Error(message, args...) +// Log emits a log record with the current time and the given level and message. +// The Record's attributes consist of the Logger's attributes followed by +// the attributes specified by args. +// +// The attribute arguments are processed as follows: +// - If an argument is a string and this is not the last argument, +// the following argument is treated as the value and the two are combined +// into an attribute. +// - Otherwise, the argument is treated as a value with key "!BADKEY". +// +// Log feeds the binding call context into the configured logger, +// so custom handlers may access context values, e.g. the current window. +func (l *LogService) Log(ctx context.Context, level Level, message string, args ...any) { + l.config.Load().Logger.Log(ctx, slog.Level(level), message, args...) } -func (l *LoggerService) SetLogLevel(level slog.Level) { - l.level.Set(level) +// Debug logs at level [Debug]. +// +//wails:ignore +func (l *LogService) Debug(message string, args ...any) { + l.DebugContext(context.Background(), message, args...) +} + +// Info logs at level [Info]. +// +//wails:ignore +func (l *LogService) Info(message string, args ...any) { + l.InfoContext(context.Background(), message, args...) +} + +// Warning logs at level [Warning]. +// +//wails:ignore +func (l *LogService) Warning(message string, args ...any) { + l.WarningContext(context.Background(), message, args...) +} + +// Error logs at level [Error]. +// +//wails:ignore +func (l *LogService) Error(message string, args ...any) { + l.ErrorContext(context.Background(), message, args...) +} + +// DebugContext logs at level [Debug]. +// +//wails:internal +func (l *LogService) DebugContext(ctx context.Context, message string, args ...any) { + l.Log(ctx, Debug, message, args...) +} + +// InfoContext logs at level [Info]. +// +//wails:internal +func (l *LogService) InfoContext(ctx context.Context, message string, args ...any) { + l.Log(ctx, Info, message, args...) +} + +// WarningContext logs at level [Warn]. +// +//wails:internal +func (l *LogService) WarningContext(ctx context.Context, message string, args ...any) { + l.Log(ctx, Warning, message, args...) +} + +// ErrorContext logs at level [Error]. +// +//wails:internal +func (l *LogService) ErrorContext(ctx context.Context, message string, args ...any) { + l.Log(ctx, Error, message, args...) } diff --git a/v3/pkg/services/notifications/notifications.go b/v3/pkg/services/notifications/notifications.go new file mode 100644 index 000000000..3382d220b --- /dev/null +++ b/v3/pkg/services/notifications/notifications.go @@ -0,0 +1,216 @@ +// Package notifications provides cross-platform notification capabilities for desktop applications. +// It supports macOS, Windows, and Linux with a consistent API while handling platform-specific +// differences internally. Key features include: +// - Basic notifications with title, subtitle, and body +// - Interactive notifications with buttons and actions +// - Notification categories for reusing configurations +// - User feedback handling with a unified callback system +// +// Platform-specific notes: +// - macOS: Requires a properly bundled and signed application +// - Windows: Uses Windows Toast notifications +// - Linux: Uses D-Bus and does not support text inputs +package notifications + +import ( + "context" + "fmt" + "sync" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +type platformNotifier interface { + // Lifecycle methods + Startup(ctx context.Context, options application.ServiceOptions) error + Shutdown() error + + // Core notification methods + RequestNotificationAuthorization() (bool, error) + CheckNotificationAuthorization() (bool, error) + SendNotification(options NotificationOptions) error + SendNotificationWithActions(options NotificationOptions) error + + // Category management + RegisterNotificationCategory(category NotificationCategory) error + RemoveNotificationCategory(categoryID string) error + + // Notification management + RemoveAllPendingNotifications() error + RemovePendingNotification(identifier string) error + RemoveAllDeliveredNotifications() error + RemoveDeliveredNotification(identifier string) error + RemoveNotification(identifier string) error +} + +// Service represents the notifications service +type NotificationService struct { + impl platformNotifier + + // notificationResponseCallback is called when a notification result is received. + // Only one callback can be assigned at a time. + notificationResultCallback func(result NotificationResult) + + callbackLock sync.RWMutex +} + +var ( + notificationServiceOnce sync.Once + NotificationService_ *NotificationService + notificationServiceLock sync.RWMutex +) + +// NotificationAction represents an action button for a notification. +type NotificationAction struct { + ID string `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Destructive bool `json:"destructive,omitempty"` // (macOS-specific) +} + +// NotificationCategory groups actions for notifications. +type NotificationCategory struct { + ID string `json:"id,omitempty"` + Actions []NotificationAction `json:"actions,omitempty"` + HasReplyField bool `json:"hasReplyField,omitempty"` + ReplyPlaceholder string `json:"replyPlaceholder,omitempty"` + ReplyButtonTitle string `json:"replyButtonTitle,omitempty"` +} + +// NotificationOptions contains configuration for a notification +type NotificationOptions struct { + ID string `json:"id"` + Title string `json:"title"` + Subtitle string `json:"subtitle,omitempty"` // (macOS and Linux only) + Body string `json:"body,omitempty"` + CategoryID string `json:"categoryId,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` +} + +const DefaultActionIdentifier = "DEFAULT_ACTION" + +// NotificationResponse represents the response sent by interacting with a notification. +type NotificationResponse struct { + ID string `json:"id,omitempty"` + ActionIdentifier string `json:"actionIdentifier,omitempty"` + CategoryID string `json:"categoryIdentifier,omitempty"` + Title string `json:"title,omitempty"` + Subtitle string `json:"subtitle,omitempty"` // (macOS and Linux only) + Body string `json:"body,omitempty"` + UserText string `json:"userText,omitempty"` + UserInfo map[string]interface{} `json:"userInfo,omitempty"` +} + +// NotificationResult represents the result of a notification response, +// returning the response or any errors that occurred. +type NotificationResult struct { + Response NotificationResponse + Error error +} + +// ServiceName returns the name of the service. +func (ns *NotificationService) ServiceName() string { + return "github.com/wailsapp/wails/v3/services/notifications" +} + +// OnNotificationResponse registers a callback function that will be called when +// a notification response is received from the user. +// +//wails:ignore +func (ns *NotificationService) OnNotificationResponse(callback func(result NotificationResult)) { + ns.callbackLock.Lock() + defer ns.callbackLock.Unlock() + + ns.notificationResultCallback = callback +} + +// handleNotificationResponse is an internal method to handle notification responses +// and invoke the registered callback if one exists. +func (ns *NotificationService) handleNotificationResult(result NotificationResult) { + ns.callbackLock.RLock() + callback := ns.notificationResultCallback + ns.callbackLock.RUnlock() + + if callback != nil { + callback(result) + } +} + +// ServiceStartup is called when the service is loaded. +func (ns *NotificationService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return ns.impl.Startup(ctx, options) +} + +// ServiceShutdown is called when the service is unloaded. +func (ns *NotificationService) ServiceShutdown() error { + return ns.impl.Shutdown() +} + +// Public methods that delegate to the implementation. +func (ns *NotificationService) RequestNotificationAuthorization() (bool, error) { + return ns.impl.RequestNotificationAuthorization() +} + +func (ns *NotificationService) CheckNotificationAuthorization() (bool, error) { + return ns.impl.CheckNotificationAuthorization() +} + +func (ns *NotificationService) SendNotification(options NotificationOptions) error { + if err := validateNotificationOptions(options); err != nil { + return err + } + return ns.impl.SendNotification(options) +} + +func (ns *NotificationService) SendNotificationWithActions(options NotificationOptions) error { + if err := validateNotificationOptions(options); err != nil { + return err + } + return ns.impl.SendNotificationWithActions(options) +} + +func (ns *NotificationService) RegisterNotificationCategory(category NotificationCategory) error { + return ns.impl.RegisterNotificationCategory(category) +} + +func (ns *NotificationService) RemoveNotificationCategory(categoryID string) error { + return ns.impl.RemoveNotificationCategory(categoryID) +} + +func (ns *NotificationService) RemoveAllPendingNotifications() error { + return ns.impl.RemoveAllPendingNotifications() +} + +func (ns *NotificationService) RemovePendingNotification(identifier string) error { + return ns.impl.RemovePendingNotification(identifier) +} + +func (ns *NotificationService) RemoveAllDeliveredNotifications() error { + return ns.impl.RemoveAllDeliveredNotifications() +} + +func (ns *NotificationService) RemoveDeliveredNotification(identifier string) error { + return ns.impl.RemoveDeliveredNotification(identifier) +} + +func (ns *NotificationService) RemoveNotification(identifier string) error { + return ns.impl.RemoveNotification(identifier) +} + +func getNotificationService() *NotificationService { + notificationServiceLock.RLock() + defer notificationServiceLock.RUnlock() + return NotificationService_ +} + +// validateNotificationOptions validates an ID and Title are provided for notifications. +func validateNotificationOptions(options NotificationOptions) error { + if options.ID == "" { + return fmt.Errorf("notification ID cannot be empty") + } + + if options.Title == "" { + return fmt.Errorf("notification title cannot be empty") + } + + return nil +} diff --git a/v3/pkg/services/notifications/notifications_darwin.go b/v3/pkg/services/notifications/notifications_darwin.go new file mode 100644 index 000000000..263e780d4 --- /dev/null +++ b/v3/pkg/services/notifications/notifications_darwin.go @@ -0,0 +1,426 @@ +//go:build darwin + +package notifications + +/* +#cgo CFLAGS:-x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa + +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 +#cgo LDFLAGS: -framework UserNotifications +#endif + +#import "./notifications_darwin.h" +*/ +import "C" +import ( + "context" + "encoding/json" + "fmt" + "sync" + "time" + "unsafe" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +type darwinNotifier struct { + channels map[int]chan notificationChannel + channelsLock sync.Mutex + nextChannelID int +} + +type notificationChannel struct { + Success bool + Error error +} + +type ChannelHandler interface { + GetChannel(id int) (chan notificationChannel, bool) +} + +const AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier" + +// Creates a new Notifications Service. +// Your app must be packaged and signed for this feature to work. +func New() *NotificationService { + notificationServiceOnce.Do(func() { + impl := &darwinNotifier{ + channels: make(map[int]chan notificationChannel), + nextChannelID: 0, + } + + NotificationService_ = &NotificationService{ + impl: impl, + } + }) + + return NotificationService_ +} + +func (dn *darwinNotifier) Startup(ctx context.Context, options application.ServiceOptions) error { + if !isNotificationAvailable() { + return fmt.Errorf("notifications are not available on this system") + } + if !checkBundleIdentifier() { + return fmt.Errorf("notifications require a valid bundle identifier") + } + if !bool(C.ensureDelegateInitialized()) { + return fmt.Errorf("failed to initialize notification center delegate") + } + return nil +} + +func (dn *darwinNotifier) Shutdown() error { + return nil +} + +// isNotificationAvailable checks if notifications are available on the system. +func isNotificationAvailable() bool { + return bool(C.isNotificationAvailable()) +} + +func checkBundleIdentifier() bool { + return bool(C.checkBundleIdentifier()) +} + +// RequestNotificationAuthorization requests permission for notifications. +// Default timeout is 3 minutes +func (dn *darwinNotifier) RequestNotificationAuthorization() (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) + defer cancel() + + id, resultCh := dn.registerChannel() + + C.requestNotificationAuthorization(C.int(id)) + + select { + case result := <-resultCh: + return result.Success, result.Error + case <-ctx.Done(): + dn.cleanupChannel(id) + return false, fmt.Errorf("notification authorization timed out after 3 minutes: %w", ctx.Err()) + } +} + +// CheckNotificationAuthorization checks current notification permission status. +func (dn *darwinNotifier) CheckNotificationAuthorization() (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + id, resultCh := dn.registerChannel() + + C.checkNotificationAuthorization(C.int(id)) + + select { + case result := <-resultCh: + return result.Success, result.Error + case <-ctx.Done(): + dn.cleanupChannel(id) + return false, fmt.Errorf("notification authorization timed out after 15s: %w", ctx.Err()) + } +} + +// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body. +func (dn *darwinNotifier) SendNotification(options NotificationOptions) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cIdentifier := C.CString(options.ID) + cTitle := C.CString(options.Title) + cSubtitle := C.CString(options.Subtitle) + cBody := C.CString(options.Body) + defer C.free(unsafe.Pointer(cIdentifier)) + defer C.free(unsafe.Pointer(cTitle)) + defer C.free(unsafe.Pointer(cSubtitle)) + defer C.free(unsafe.Pointer(cBody)) + + var cDataJSON *C.char + if options.Data != nil { + jsonData, err := json.Marshal(options.Data) + if err != nil { + return fmt.Errorf("failed to marshal notification data: %w", err) + } + cDataJSON = C.CString(string(jsonData)) + defer C.free(unsafe.Pointer(cDataJSON)) + } + + id, resultCh := dn.registerChannel() + C.sendNotification(C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cDataJSON) + + select { + case result := <-resultCh: + if !result.Success { + if result.Error != nil { + return result.Error + } + return fmt.Errorf("sending notification failed") + } + return nil + case <-ctx.Done(): + dn.cleanupChannel(id) + return fmt.Errorf("sending notification timed out: %w", ctx.Err()) + } +} + +// SendNotificationWithActions sends a notification with additional actions and inputs. +// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category. +// If a NotificationCategory is not registered a basic notification will be sent. +func (dn *darwinNotifier) SendNotificationWithActions(options NotificationOptions) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cIdentifier := C.CString(options.ID) + cTitle := C.CString(options.Title) + cSubtitle := C.CString(options.Subtitle) + cBody := C.CString(options.Body) + cCategoryID := C.CString(options.CategoryID) + defer C.free(unsafe.Pointer(cIdentifier)) + defer C.free(unsafe.Pointer(cTitle)) + defer C.free(unsafe.Pointer(cSubtitle)) + defer C.free(unsafe.Pointer(cBody)) + defer C.free(unsafe.Pointer(cCategoryID)) + + var cDataJSON *C.char + if options.Data != nil { + jsonData, err := json.Marshal(options.Data) + if err != nil { + return fmt.Errorf("failed to marshal notification data: %w", err) + } + cDataJSON = C.CString(string(jsonData)) + defer C.free(unsafe.Pointer(cDataJSON)) + } + + id, resultCh := dn.registerChannel() + C.sendNotificationWithActions(C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cCategoryID, cDataJSON) + + select { + case result := <-resultCh: + if !result.Success { + if result.Error != nil { + return result.Error + } + return fmt.Errorf("sending notification failed") + } + return nil + case <-ctx.Done(): + dn.cleanupChannel(id) + return fmt.Errorf("sending notification timed out: %w", ctx.Err()) + } +} + +// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions. +// Registering a category with the same name as a previously registered NotificationCategory will override it. +func (dn *darwinNotifier) RegisterNotificationCategory(category NotificationCategory) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cCategoryID := C.CString(category.ID) + defer C.free(unsafe.Pointer(cCategoryID)) + + actionsJSON, err := json.Marshal(category.Actions) + if err != nil { + return fmt.Errorf("failed to marshal notification category: %w", err) + } + cActionsJSON := C.CString(string(actionsJSON)) + defer C.free(unsafe.Pointer(cActionsJSON)) + + var cReplyPlaceholder, cReplyButtonTitle *C.char + if category.HasReplyField { + cReplyPlaceholder = C.CString(category.ReplyPlaceholder) + cReplyButtonTitle = C.CString(category.ReplyButtonTitle) + defer C.free(unsafe.Pointer(cReplyPlaceholder)) + defer C.free(unsafe.Pointer(cReplyButtonTitle)) + } + + id, resultCh := dn.registerChannel() + C.registerNotificationCategory(C.int(id), cCategoryID, cActionsJSON, C.bool(category.HasReplyField), + cReplyPlaceholder, cReplyButtonTitle) + + select { + case result := <-resultCh: + if !result.Success { + if result.Error != nil { + return result.Error + } + return fmt.Errorf("category registration failed") + } + return nil + case <-ctx.Done(): + dn.cleanupChannel(id) + return fmt.Errorf("category registration timed out: %w", ctx.Err()) + } +} + +// RemoveNotificationCategory remove a previously registered NotificationCategory. +func (dn *darwinNotifier) RemoveNotificationCategory(categoryId string) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cCategoryID := C.CString(categoryId) + defer C.free(unsafe.Pointer(cCategoryID)) + + id, resultCh := dn.registerChannel() + C.removeNotificationCategory(C.int(id), cCategoryID) + + select { + case result := <-resultCh: + if !result.Success { + if result.Error != nil { + return result.Error + } + return fmt.Errorf("category removal failed") + } + return nil + case <-ctx.Done(): + dn.cleanupChannel(id) + return fmt.Errorf("category removal timed out: %w", ctx.Err()) + } +} + +// RemoveAllPendingNotifications removes all pending notifications. +func (dn *darwinNotifier) RemoveAllPendingNotifications() error { + C.removeAllPendingNotifications() + return nil +} + +// RemovePendingNotification removes a pending notification matching the unique identifier. +func (dn *darwinNotifier) RemovePendingNotification(identifier string) error { + cIdentifier := C.CString(identifier) + defer C.free(unsafe.Pointer(cIdentifier)) + C.removePendingNotification(cIdentifier) + return nil +} + +// RemoveAllDeliveredNotifications removes all delivered notifications. +func (dn *darwinNotifier) RemoveAllDeliveredNotifications() error { + C.removeAllDeliveredNotifications() + return nil +} + +// RemoveDeliveredNotification removes a delivered notification matching the unique identifier. +func (dn *darwinNotifier) RemoveDeliveredNotification(identifier string) error { + cIdentifier := C.CString(identifier) + defer C.free(unsafe.Pointer(cIdentifier)) + C.removeDeliveredNotification(cIdentifier) + return nil +} + +// RemoveNotification is a macOS stub that always returns nil. +// Use one of the following instead: +// RemoveAllPendingNotifications +// RemovePendingNotification +// RemoveAllDeliveredNotifications +// RemoveDeliveredNotification +// (Linux-specific) +func (dn *darwinNotifier) RemoveNotification(identifier string) error { + return nil +} + +//export captureResult +func captureResult(channelID C.int, success C.bool, errorMsg *C.char) { + ns := getNotificationService() + if ns == nil { + return + } + + handler, ok := ns.impl.(ChannelHandler) + if !ok { + return + } + + resultCh, exists := handler.GetChannel(int(channelID)) + if !exists { + return + } + + var err error + if errorMsg != nil { + err = fmt.Errorf("%s", C.GoString(errorMsg)) + } + + resultCh <- notificationChannel{ + Success: bool(success), + Error: err, + } + + close(resultCh) +} + +//export didReceiveNotificationResponse +func didReceiveNotificationResponse(jsonPayload *C.char, err *C.char) { + result := NotificationResult{} + + if err != nil { + errMsg := C.GoString(err) + result.Error = fmt.Errorf("notification response error: %s", errMsg) + if ns := getNotificationService(); ns != nil { + ns.handleNotificationResult(result) + } + return + } + + if jsonPayload == nil { + result.Error = fmt.Errorf("received nil JSON payload in notification response") + if ns := getNotificationService(); ns != nil { + ns.handleNotificationResult(result) + } + return + } + + payload := C.GoString(jsonPayload) + + var response NotificationResponse + if err := json.Unmarshal([]byte(payload), &response); err != nil { + result.Error = fmt.Errorf("failed to unmarshal notification response: %w", err) + if ns := getNotificationService(); ns != nil { + ns.handleNotificationResult(result) + } + return + } + + if response.ActionIdentifier == AppleDefaultActionIdentifier { + response.ActionIdentifier = DefaultActionIdentifier + } + + result.Response = response + if ns := getNotificationService(); ns != nil { + ns.handleNotificationResult(result) + } +} + +// Helper methods + +func (dn *darwinNotifier) registerChannel() (int, chan notificationChannel) { + dn.channelsLock.Lock() + defer dn.channelsLock.Unlock() + + id := dn.nextChannelID + dn.nextChannelID++ + + resultCh := make(chan notificationChannel, 1) + + dn.channels[id] = resultCh + return id, resultCh +} + +func (dn *darwinNotifier) GetChannel(id int) (chan notificationChannel, bool) { + dn.channelsLock.Lock() + defer dn.channelsLock.Unlock() + + ch, exists := dn.channels[id] + if exists { + delete(dn.channels, id) + } + return ch, exists +} + +func (dn *darwinNotifier) cleanupChannel(id int) { + dn.channelsLock.Lock() + defer dn.channelsLock.Unlock() + + if ch, exists := dn.channels[id]; exists { + delete(dn.channels, id) + close(ch) + } +} diff --git a/v3/pkg/services/notifications/notifications_darwin.h b/v3/pkg/services/notifications/notifications_darwin.h new file mode 100644 index 000000000..71e167656 --- /dev/null +++ b/v3/pkg/services/notifications/notifications_darwin.h @@ -0,0 +1,22 @@ +//go:build darwin + +#ifndef NOTIFICATIONS_DARWIN_H +#define NOTIFICATIONS_DARWIN_H + +#import + +bool isNotificationAvailable(void); +bool checkBundleIdentifier(void); +bool ensureDelegateInitialized(void); +void requestNotificationAuthorization(int channelID); +void checkNotificationAuthorization(int channelID); +void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json); +void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json); +void registerNotificationCategory(int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle); +void removeNotificationCategory(int channelID, const char *categoryId); +void removeAllPendingNotifications(void); +void removePendingNotification(const char *identifier); +void removeAllDeliveredNotifications(void); +void removeDeliveredNotification(const char *identifier); + +#endif /* NOTIFICATIONS_DARWIN_H */ \ No newline at end of file diff --git a/v3/pkg/services/notifications/notifications_darwin.m b/v3/pkg/services/notifications/notifications_darwin.m new file mode 100644 index 000000000..373197cb0 --- /dev/null +++ b/v3/pkg/services/notifications/notifications_darwin.m @@ -0,0 +1,377 @@ +#import "notifications_darwin.h" +#include +#import + +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 +#import +#endif + +bool isNotificationAvailable(void) { + if (@available(macOS 11.0, *)) { + return YES; + } else { + return NO; + } +} + +bool checkBundleIdentifier(void) { + NSBundle *main = [NSBundle mainBundle]; + if (main.bundleIdentifier == nil) { + return NO; + } + return YES; +} + +extern void captureResult(int channelID, bool success, const char* error); +extern void didReceiveNotificationResponse(const char *jsonPayload, const char* error); + +@interface NotificationsDelegate : NSObject +@end + +@implementation NotificationsDelegate + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + willPresentNotification:(UNNotification *)notification + withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler { + UNNotificationPresentationOptions options = 0; + + if (@available(macOS 11.0, *)) { + // These options are only available in macOS 11.0+ + options = UNNotificationPresentationOptionList | + UNNotificationPresentationOptionBanner | + UNNotificationPresentationOptionSound; + } + + completionHandler(options); +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center +didReceiveNotificationResponse:(UNNotificationResponse *)response + withCompletionHandler:(void (^)(void))completionHandler { + + NSMutableDictionary *payload = [NSMutableDictionary dictionary]; + + [payload setObject:response.notification.request.identifier forKey:@"id"]; + [payload setObject:response.actionIdentifier forKey:@"actionIdentifier"]; + [payload setObject:response.notification.request.content.title ?: @"" forKey:@"title"]; + [payload setObject:response.notification.request.content.body ?: @"" forKey:@"body"]; + + if (response.notification.request.content.categoryIdentifier) { + [payload setObject:response.notification.request.content.categoryIdentifier forKey:@"categoryIdentifier"]; + } + + if (response.notification.request.content.subtitle) { + [payload setObject:response.notification.request.content.subtitle forKey:@"subtitle"]; + } + + if (response.notification.request.content.userInfo) { + [payload setObject:response.notification.request.content.userInfo forKey:@"userInfo"]; + } + + if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) { + UNTextInputNotificationResponse *textResponse = (UNTextInputNotificationResponse *)response; + [payload setObject:textResponse.userText forKey:@"userText"]; + } + + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error]; + if (error) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; + didReceiveNotificationResponse(NULL, [errorMsg UTF8String]); + } else { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + didReceiveNotificationResponse([jsonString UTF8String], NULL); + } + + completionHandler(); +} + +@end + +static NotificationsDelegate *delegateInstance = nil; +static dispatch_once_t onceToken; + +bool ensureDelegateInitialized(void) { + __block BOOL success = YES; + + dispatch_once(&onceToken, ^{ + delegateInstance = [[NotificationsDelegate alloc] init]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = delegateInstance; + }); + + if (!delegateInstance) { + success = NO; + } + + return success; +} + +void requestNotificationAuthorization(int channelID) { + if (!ensureDelegateInitialized()) { + NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service."; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; + + [center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) { + if (error) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + } else { + captureResult(channelID, granted, NULL); + } + }]; +} + +void checkNotificationAuthorization(int channelID) { + if (!ensureDelegateInitialized()) { + NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service."; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) { + BOOL isAuthorized = (settings.authorizationStatus == UNAuthorizationStatusAuthorized); + captureResult(channelID, isAuthorized, NULL); + }]; +} + +// Helper function to create notification content +UNMutableNotificationContent* createNotificationContent(const char *title, const char *subtitle, + const char *body, const char *data_json, NSError **contentError) { + NSString *nsTitle = [NSString stringWithUTF8String:title]; + NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @""; + NSString *nsBody = [NSString stringWithUTF8String:body]; + + UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; + content.title = nsTitle; + if (![nsSubtitle isEqualToString:@""]) { + content.subtitle = nsSubtitle; + } + content.body = nsBody; + content.sound = [UNNotificationSound defaultSound]; + + // Parse JSON data if provided + if (data_json) { + NSString *dataJsonStr = [NSString stringWithUTF8String:data_json]; + NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error = nil; + NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + if (!error && parsedData) { + content.userInfo = parsedData; + } else if (error) { + *contentError = error; + } + } + + return content; +} + +void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) { + if (!ensureDelegateInitialized()) { + NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service."; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + + NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; + + NSError *contentError = nil; + UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json, &contentError); + + if (contentError) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [contentError localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + UNTimeIntervalNotificationTrigger *trigger = nil; + + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger]; + + [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { + if (error) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + } else { + captureResult(channelID, true, NULL); + } + }]; +} + +void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle, + const char *body, const char *categoryId, const char *data_json) { + if (!ensureDelegateInitialized()) { + NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service."; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + + NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; + NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId]; + + NSError *contentError = nil; + UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json, &contentError); + + if (contentError) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [contentError localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + content.categoryIdentifier = nsCategoryId; + + UNTimeIntervalNotificationTrigger *trigger = nil; + + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger]; + + [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { + if (error) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + } else { + captureResult(channelID, true, NULL); + } + }]; +} + +void registerNotificationCategory(int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, + const char *replyPlaceholder, const char *replyButtonTitle) { + if (!ensureDelegateInitialized()) { + NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service."; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId]; + NSString *actionsJsonStr = actions_json ? [NSString stringWithUTF8String:actions_json] : @"[]"; + + NSData *jsonData = [actionsJsonStr dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error = nil; + NSArray *actionsArray = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + + if (error) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + NSMutableArray *actions = [NSMutableArray array]; + + for (NSDictionary *actionDict in actionsArray) { + NSString *actionId = actionDict[@"id"]; + NSString *actionTitle = actionDict[@"title"]; + BOOL destructive = [actionDict[@"destructive"] boolValue]; + + if (actionId && actionTitle) { + UNNotificationActionOptions options = UNNotificationActionOptionNone; + if (destructive) options |= UNNotificationActionOptionDestructive; + + UNNotificationAction *action = [UNNotificationAction + actionWithIdentifier:actionId + title:actionTitle + options:options]; + [actions addObject:action]; + } + } + + if (hasReplyField && replyPlaceholder && replyButtonTitle) { + NSString *placeholder = [NSString stringWithUTF8String:replyPlaceholder]; + NSString *buttonTitle = [NSString stringWithUTF8String:replyButtonTitle]; + + UNTextInputNotificationAction *textAction = + [UNTextInputNotificationAction actionWithIdentifier:@"TEXT_REPLY" + title:buttonTitle + options:UNNotificationActionOptionNone + textInputButtonTitle:buttonTitle + textInputPlaceholder:placeholder]; + [actions addObject:textAction]; + } + + UNNotificationCategory *newCategory = [UNNotificationCategory + categoryWithIdentifier:nsCategoryId + actions:actions + intentIdentifiers:@[] + options:UNNotificationCategoryOptionNone]; + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center getNotificationCategoriesWithCompletionHandler:^(NSSet *categories) { + NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories]; + + // Remove existing category with same ID if it exists + UNNotificationCategory *existingCategory = nil; + for (UNNotificationCategory *category in updatedCategories) { + if ([category.identifier isEqualToString:nsCategoryId]) { + existingCategory = category; + break; + } + } + if (existingCategory) { + [updatedCategories removeObject:existingCategory]; + } + + // Add the new category + [updatedCategories addObject:newCategory]; + [center setNotificationCategories:updatedCategories]; + + captureResult(channelID, true, NULL); + }]; +} + +void removeNotificationCategory(int channelID, const char *categoryId) { + NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + + [center getNotificationCategoriesWithCompletionHandler:^(NSSet *categories) { + NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories]; + + // Find and remove the category with matching identifier + UNNotificationCategory *categoryToRemove = nil; + for (UNNotificationCategory *category in updatedCategories) { + if ([category.identifier isEqualToString:nsCategoryId]) { + categoryToRemove = category; + break; + } + } + + if (categoryToRemove) { + [updatedCategories removeObject:categoryToRemove]; + [center setNotificationCategories:updatedCategories]; + captureResult(channelID, true, NULL); + } else { + NSString *errorMsg = [NSString stringWithFormat:@"Category '%@' not found", nsCategoryId]; + captureResult(channelID, false, [errorMsg UTF8String]); + } + }]; +} + +void removeAllPendingNotifications(void) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removeAllPendingNotificationRequests]; +} + +void removePendingNotification(const char *identifier) { + NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removePendingNotificationRequestsWithIdentifiers:@[nsIdentifier]]; +} + +void removeAllDeliveredNotifications(void) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removeAllDeliveredNotifications]; +} + +void removeDeliveredNotification(const char *identifier) { + NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removeDeliveredNotificationsWithIdentifiers:@[nsIdentifier]]; +} \ No newline at end of file diff --git a/v3/pkg/services/notifications/notifications_linux.go b/v3/pkg/services/notifications/notifications_linux.go new file mode 100644 index 000000000..8dc9765cd --- /dev/null +++ b/v3/pkg/services/notifications/notifications_linux.go @@ -0,0 +1,565 @@ +//go:build linux + +package notifications + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/godbus/dbus/v5" + "github.com/wailsapp/wails/v3/pkg/application" +) + +type linuxNotifier struct { + conn *dbus.Conn + categories map[string]NotificationCategory + categoriesLock sync.RWMutex + notifications map[uint32]*notificationData + notificationsLock sync.RWMutex + appName string + cancel context.CancelFunc +} + +type notificationData struct { + ID string + Title string + Subtitle string + Body string + CategoryID string + Data map[string]interface{} + DBusID uint32 + ActionMap map[string]string +} + +const ( + dbusNotificationInterface = "org.freedesktop.Notifications" + dbusNotificationPath = "/org/freedesktop/Notifications" +) + +// Creates a new Notifications Service. +func New() *NotificationService { + notificationServiceOnce.Do(func() { + impl := &linuxNotifier{ + categories: make(map[string]NotificationCategory), + notifications: make(map[uint32]*notificationData), + } + + NotificationService_ = &NotificationService{ + impl: impl, + } + }) + + return NotificationService_ +} + +// Startup is called when the service is loaded. +func (ln *linuxNotifier) Startup(ctx context.Context, options application.ServiceOptions) error { + ln.appName = application.Get().Config().Name + + conn, err := dbus.ConnectSessionBus() + if err != nil { + return fmt.Errorf("failed to connect to session bus: %w", err) + } + ln.conn = conn + + if err := ln.loadCategories(); err != nil { + fmt.Printf("Failed to load notification categories: %v\n", err) + } + + var signalCtx context.Context + signalCtx, ln.cancel = context.WithCancel(context.Background()) + + if err := ln.setupSignalHandling(signalCtx); err != nil { + return fmt.Errorf("failed to set up notification signal handling: %w", err) + } + + return nil +} + +// Shutdown will save categories and close the D-Bus connection when the service unloads. +func (ln *linuxNotifier) Shutdown() error { + if ln.cancel != nil { + ln.cancel() + } + + if err := ln.saveCategories(); err != nil { + fmt.Printf("Failed to save notification categories: %v\n", err) + } + + if ln.conn != nil { + return ln.conn.Close() + } + return nil +} + +// RequestNotificationAuthorization is a Linux stub that always returns true, nil. +// (authorization is macOS-specific) +func (ln *linuxNotifier) RequestNotificationAuthorization() (bool, error) { + return true, nil +} + +// CheckNotificationAuthorization is a Linux stub that always returns true. +// (authorization is macOS-specific) +func (ln *linuxNotifier) CheckNotificationAuthorization() (bool, error) { + return true, nil +} + +// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body. +func (ln *linuxNotifier) SendNotification(options NotificationOptions) error { + hints := map[string]dbus.Variant{} + + body := options.Body + if options.Subtitle != "" { + body = options.Subtitle + "\n" + body + } + + defaultActionID := "default" + actions := []string{defaultActionID, "Default"} + + actionMap := map[string]string{ + defaultActionID: DefaultActionIdentifier, + } + + hints["x-notification-id"] = dbus.MakeVariant(options.ID) + + if options.Data != nil { + userData, err := json.Marshal(options.Data) + if err == nil { + hints["x-user-data"] = dbus.MakeVariant(string(userData)) + } + } + + // Call the Notify method on the D-Bus interface + obj := ln.conn.Object(dbusNotificationInterface, dbusNotificationPath) + call := obj.Call( + dbusNotificationInterface+".Notify", + 0, + ln.appName, + uint32(0), + "", // Icon + options.Title, + body, + actions, + hints, + int32(-1), + ) + + if call.Err != nil { + return fmt.Errorf("failed to send notification: %w", call.Err) + } + + var dbusID uint32 + if err := call.Store(&dbusID); err != nil { + return fmt.Errorf("failed to store notification ID: %w", err) + } + + notification := ¬ificationData{ + ID: options.ID, + Title: options.Title, + Subtitle: options.Subtitle, + Body: options.Body, + Data: options.Data, + DBusID: dbusID, + ActionMap: actionMap, + } + + ln.notificationsLock.Lock() + ln.notifications[dbusID] = notification + ln.notificationsLock.Unlock() + + return nil +} + +// SendNotificationWithActions sends a notification with additional actions. +func (ln *linuxNotifier) SendNotificationWithActions(options NotificationOptions) error { + ln.categoriesLock.RLock() + category, exists := ln.categories[options.CategoryID] + ln.categoriesLock.RUnlock() + + if options.CategoryID == "" || !exists { + // Fall back to basic notification + return ln.SendNotification(options) + } + + body := options.Body + if options.Subtitle != "" { + body = options.Subtitle + "\n" + body + } + + var actions []string + actionMap := make(map[string]string) + + defaultActionID := "default" + actions = append(actions, defaultActionID, "Default") + actionMap[defaultActionID] = DefaultActionIdentifier + + for _, action := range category.Actions { + actions = append(actions, action.ID, action.Title) + actionMap[action.ID] = action.ID + } + + hints := map[string]dbus.Variant{} + + hints["x-notification-id"] = dbus.MakeVariant(options.ID) + + hints["x-category-id"] = dbus.MakeVariant(options.CategoryID) + + if options.Data != nil { + userData, err := json.Marshal(options.Data) + if err == nil { + hints["x-user-data"] = dbus.MakeVariant(string(userData)) + } + } + + obj := ln.conn.Object(dbusNotificationInterface, dbusNotificationPath) + call := obj.Call( + dbusNotificationInterface+".Notify", + 0, + ln.appName, + uint32(0), + "", // Icon + options.Title, + body, + actions, + hints, + int32(-1), + ) + + if call.Err != nil { + return fmt.Errorf("failed to send notification: %w", call.Err) + } + + var dbusID uint32 + if err := call.Store(&dbusID); err != nil { + return fmt.Errorf("failed to store notification ID: %w", err) + } + + notification := ¬ificationData{ + ID: options.ID, + Title: options.Title, + Subtitle: options.Subtitle, + Body: options.Body, + CategoryID: options.CategoryID, + Data: options.Data, + DBusID: dbusID, + ActionMap: actionMap, + } + + ln.notificationsLock.Lock() + ln.notifications[dbusID] = notification + ln.notificationsLock.Unlock() + + return nil +} + +// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions. +func (ln *linuxNotifier) RegisterNotificationCategory(category NotificationCategory) error { + ln.categoriesLock.Lock() + ln.categories[category.ID] = category + ln.categoriesLock.Unlock() + + if err := ln.saveCategories(); err != nil { + fmt.Printf("Failed to save notification categories: %v\n", err) + } + + return nil +} + +// RemoveNotificationCategory removes a previously registered NotificationCategory. +func (ln *linuxNotifier) RemoveNotificationCategory(categoryId string) error { + ln.categoriesLock.Lock() + delete(ln.categories, categoryId) + ln.categoriesLock.Unlock() + + if err := ln.saveCategories(); err != nil { + fmt.Printf("Failed to save notification categories: %v\n", err) + } + + return nil +} + +// RemoveAllPendingNotifications attempts to remove all active notifications. +func (ln *linuxNotifier) RemoveAllPendingNotifications() error { + ln.notificationsLock.Lock() + dbusIDs := make([]uint32, 0, len(ln.notifications)) + for id := range ln.notifications { + dbusIDs = append(dbusIDs, id) + } + ln.notificationsLock.Unlock() + + for _, id := range dbusIDs { + ln.closeNotification(id) + } + + return nil +} + +// RemovePendingNotification removes a pending notification. +func (ln *linuxNotifier) RemovePendingNotification(identifier string) error { + var dbusID uint32 + found := false + + ln.notificationsLock.Lock() + for id, notif := range ln.notifications { + if notif.ID == identifier { + dbusID = id + found = true + break + } + } + ln.notificationsLock.Unlock() + + if !found { + return nil + } + + return ln.closeNotification(dbusID) +} + +// RemoveAllDeliveredNotifications functionally equivalent to RemoveAllPendingNotification on Linux. +func (ln *linuxNotifier) RemoveAllDeliveredNotifications() error { + return ln.RemoveAllPendingNotifications() +} + +// RemoveDeliveredNotification functionally equivalent RemovePendingNotification on Linux. +func (ln *linuxNotifier) RemoveDeliveredNotification(identifier string) error { + return ln.RemovePendingNotification(identifier) +} + +// RemoveNotification removes a notification by identifier. +func (ln *linuxNotifier) RemoveNotification(identifier string) error { + return ln.RemovePendingNotification(identifier) +} + +// Helper method to close a notification. +func (ln *linuxNotifier) closeNotification(id uint32) error { + obj := ln.conn.Object(dbusNotificationInterface, dbusNotificationPath) + call := obj.Call(dbusNotificationInterface+".CloseNotification", 0, id) + + if call.Err != nil { + return fmt.Errorf("failed to close notification: %w", call.Err) + } + + return nil +} + +func (ln *linuxNotifier) getConfigDir() (string, error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", fmt.Errorf("failed to get user config directory: %w", err) + } + + appConfigDir := filepath.Join(configDir, ln.appName) + if err := os.MkdirAll(appConfigDir, 0755); err != nil { + return "", fmt.Errorf("failed to create app config directory: %w", err) + } + + return appConfigDir, nil +} + +// Save notification categories. +func (ln *linuxNotifier) saveCategories() error { + configDir, err := ln.getConfigDir() + if err != nil { + return err + } + + categoriesFile := filepath.Join(configDir, "notification-categories.json") + + ln.categoriesLock.RLock() + categoriesData, err := json.MarshalIndent(ln.categories, "", " ") + ln.categoriesLock.RUnlock() + + if err != nil { + return fmt.Errorf("failed to marshal notification categories: %w", err) + } + + if err := os.WriteFile(categoriesFile, categoriesData, 0644); err != nil { + return fmt.Errorf("failed to write notification categories to disk: %w", err) + } + + return nil +} + +// Load notification categories. +func (ln *linuxNotifier) loadCategories() error { + configDir, err := ln.getConfigDir() + if err != nil { + return err + } + + categoriesFile := filepath.Join(configDir, "notification-categories.json") + + if _, err := os.Stat(categoriesFile); os.IsNotExist(err) { + return nil + } + + categoriesData, err := os.ReadFile(categoriesFile) + if err != nil { + return fmt.Errorf("failed to read notification categories from disk: %w", err) + } + + categories := make(map[string]NotificationCategory) + if err := json.Unmarshal(categoriesData, &categories); err != nil { + return fmt.Errorf("failed to unmarshal notification categories: %w", err) + } + + ln.categoriesLock.Lock() + ln.categories = categories + ln.categoriesLock.Unlock() + + return nil +} + +// Setup signal handling for notification actions. +func (ln *linuxNotifier) setupSignalHandling(ctx context.Context) error { + if err := ln.conn.AddMatchSignal( + dbus.WithMatchInterface(dbusNotificationInterface), + dbus.WithMatchMember("ActionInvoked"), + ); err != nil { + return err + } + + if err := ln.conn.AddMatchSignal( + dbus.WithMatchInterface(dbusNotificationInterface), + dbus.WithMatchMember("NotificationClosed"), + ); err != nil { + return err + } + + c := make(chan *dbus.Signal, 10) + ln.conn.Signal(c) + + go ln.handleSignals(ctx, c) + + return nil +} + +// Handle incoming D-Bus signals. +func (ln *linuxNotifier) handleSignals(ctx context.Context, c chan *dbus.Signal) { + for { + select { + case <-ctx.Done(): + return + case signal, ok := <-c: + if !ok { + return + } + + switch signal.Name { + case dbusNotificationInterface + ".ActionInvoked": + ln.handleActionInvoked(signal) + case dbusNotificationInterface + ".NotificationClosed": + ln.handleNotificationClosed(signal) + } + } + } +} + +// Handle ActionInvoked signal. +func (ln *linuxNotifier) handleActionInvoked(signal *dbus.Signal) { + if len(signal.Body) < 2 { + return + } + + dbusID, ok := signal.Body[0].(uint32) + if !ok { + return + } + + actionID, ok := signal.Body[1].(string) + if !ok { + return + } + + ln.notificationsLock.Lock() + notification, exists := ln.notifications[dbusID] + if exists { + delete(ln.notifications, dbusID) + } + ln.notificationsLock.Unlock() + + if !exists { + return + } + + appActionID, ok := notification.ActionMap[actionID] + if !ok { + appActionID = actionID + } + + response := NotificationResponse{ + ID: notification.ID, + ActionIdentifier: appActionID, + Title: notification.Title, + Subtitle: notification.Subtitle, + Body: notification.Body, + CategoryID: notification.CategoryID, + UserInfo: notification.Data, + } + + result := NotificationResult{ + Response: response, + } + + if ns := getNotificationService(); ns != nil { + ns.handleNotificationResult(result) + } +} + +// Handle NotificationClosed signal. +// Reason codes: +// 1 - expired timeout +// 2 - dismissed by user (click on X) +// 3 - closed by CloseNotification call +// 4 - undefined/reserved +func (ln *linuxNotifier) handleNotificationClosed(signal *dbus.Signal) { + if len(signal.Body) < 2 { + return + } + + dbusID, ok := signal.Body[0].(uint32) + if !ok { + return + } + + reason, ok := signal.Body[1].(uint32) + if !ok { + reason = 0 // Unknown reason + } + + ln.notificationsLock.Lock() + notification, exists := ln.notifications[dbusID] + if exists { + delete(ln.notifications, dbusID) + } + ln.notificationsLock.Unlock() + + if !exists { + return + } + + if reason == 2 { + response := NotificationResponse{ + ID: notification.ID, + ActionIdentifier: DefaultActionIdentifier, + Title: notification.Title, + Subtitle: notification.Subtitle, + Body: notification.Body, + CategoryID: notification.CategoryID, + UserInfo: notification.Data, + } + + result := NotificationResult{ + Response: response, + } + + if ns := getNotificationService(); ns != nil { + ns.handleNotificationResult(result) + } + } +} diff --git a/v3/pkg/services/notifications/notifications_windows.go b/v3/pkg/services/notifications/notifications_windows.go new file mode 100644 index 000000000..1a1a6dd85 --- /dev/null +++ b/v3/pkg/services/notifications/notifications_windows.go @@ -0,0 +1,459 @@ +//go:build windows + +package notifications + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + _ "unsafe" + + "git.sr.ht/~jackmordaunt/go-toast/v2" + wintoast "git.sr.ht/~jackmordaunt/go-toast/v2/wintoast" + "github.com/google/uuid" + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/w32" + "golang.org/x/sys/windows/registry" +) + +type windowsNotifier struct { + categories map[string]NotificationCategory + categoriesLock sync.RWMutex + appName string + appGUID string + iconPath string + exePath string +} + +const ( + ToastRegistryPath = `Software\Classes\AppUserModelId\` + ToastRegistryGuidKey = "CustomActivator" + NotificationCategoriesRegistryPath = `SOFTWARE\%s\NotificationCategories` + NotificationCategoriesRegistryKey = "Categories" +) + +// NotificationPayload combines the action ID and user data into a single structure +type NotificationPayload struct { + Action string `json:"action"` + Options NotificationOptions `json:"payload,omitempty"` +} + +// Creates a new Notifications Service. +func New() *NotificationService { + notificationServiceOnce.Do(func() { + impl := &windowsNotifier{ + categories: make(map[string]NotificationCategory), + } + + NotificationService_ = &NotificationService{ + impl: impl, + } + }) + + return NotificationService_ +} + +//go:linkname registerFactoryInternal git.sr.ht/~jackmordaunt/go-toast/v2/wintoast.registerClassFactory +func registerFactoryInternal(factory *wintoast.IClassFactory) error + +// Startup is called when the service is loaded +// Sets an activation callback to emit an event when notifications are interacted with. +func (wn *windowsNotifier) Startup(ctx context.Context, options application.ServiceOptions) error { + wn.categoriesLock.Lock() + defer wn.categoriesLock.Unlock() + + app := application.Get() + cfg := app.Config() + + wn.appName = cfg.Name + + guid, err := wn.getGUID() + if err != nil { + return err + } + wn.appGUID = guid + + wn.iconPath = filepath.Join(os.TempDir(), wn.appName+wn.appGUID+".png") + + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + wn.exePath = exe + + // Create the registry key for the toast activator + key, _, err := registry.CreateKey(registry.CURRENT_USER, + `Software\Classes\CLSID\`+wn.appGUID+`\LocalServer32`, registry.ALL_ACCESS) + if err != nil { + return fmt.Errorf("failed to create CLSID key: %w", err) + } + + if err := key.SetStringValue("", fmt.Sprintf("\"%s\" %%1", wn.exePath)); err != nil { + return fmt.Errorf("failed to set CLSID server path: %w", err) + } + key.Close() + + toast.SetAppData(toast.AppData{ + AppID: wn.appName, + GUID: guid, + IconPath: wn.iconPath, + ActivationExe: wn.exePath, + }) + + toast.SetActivationCallback(func(args string, data []toast.UserData) { + result := NotificationResult{} + + actionIdentifier, options, err := parseNotificationResponse(args) + + if err != nil { + result.Error = err + } else { + // Subtitle is retained but was not shown with the notification + response := NotificationResponse{ + ID: options.ID, + ActionIdentifier: actionIdentifier, + Title: options.Title, + Subtitle: options.Subtitle, + Body: options.Body, + CategoryID: options.CategoryID, + UserInfo: options.Data, + } + + if userText, found := wn.getUserText(data); found { + response.UserText = userText + } + + result.Response = response + } + + if ns := getNotificationService(); ns != nil { + ns.handleNotificationResult(result) + } + }) + + // Register the class factory for the toast activator + if err := registerFactoryInternal(wintoast.ClassFactory); err != nil { + return fmt.Errorf("CoRegisterClassObject failed: %w", err) + } + + return wn.loadCategoriesFromRegistry() +} + +// Shutdown will attempt to save the categories to the registry when the service unloads +func (wn *windowsNotifier) Shutdown() error { + wn.categoriesLock.Lock() + defer wn.categoriesLock.Unlock() + + return wn.saveCategoriesToRegistry() +} + +// RequestNotificationAuthorization is a Windows stub that always returns true, nil. +// (user authorization is macOS-specific) +func (wn *windowsNotifier) RequestNotificationAuthorization() (bool, error) { + return true, nil +} + +// CheckNotificationAuthorization is a Windows stub that always returns true. +// (user authorization is macOS-specific) +func (wn *windowsNotifier) CheckNotificationAuthorization() (bool, error) { + return true, nil +} + +// SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows. +// (subtitle is only available on macOS and Linux) +func (wn *windowsNotifier) SendNotification(options NotificationOptions) error { + if err := wn.saveIconToDir(); err != nil { + fmt.Printf("Error saving icon: %v\n", err) + } + + n := toast.Notification{ + Title: options.Title, + Body: options.Body, + ActivationType: toast.Foreground, + ActivationArguments: DefaultActionIdentifier, + } + + encodedPayload, err := wn.encodePayload(DefaultActionIdentifier, options) + if err != nil { + return fmt.Errorf("failed to encode notification payload: %w", err) + } + n.ActivationArguments = encodedPayload + + return n.Push() +} + +// SendNotificationWithActions sends a notification with additional actions and inputs. +// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category. +// If a NotificationCategory is not registered a basic notification will be sent. +// (subtitle is only available on macOS and Linux) +func (wn *windowsNotifier) SendNotificationWithActions(options NotificationOptions) error { + if err := wn.saveIconToDir(); err != nil { + fmt.Printf("Error saving icon: %v\n", err) + } + + wn.categoriesLock.RLock() + nCategory, categoryExists := wn.categories[options.CategoryID] + wn.categoriesLock.RUnlock() + + if options.CategoryID == "" || !categoryExists { + fmt.Printf("Category '%s' not found, sending basic notification without actions\n", options.CategoryID) + } + + n := toast.Notification{ + Title: options.Title, + Body: options.Body, + ActivationType: toast.Foreground, + ActivationArguments: DefaultActionIdentifier, + } + + for _, action := range nCategory.Actions { + n.Actions = append(n.Actions, toast.Action{ + Content: action.Title, + Arguments: action.ID, + }) + } + + if nCategory.HasReplyField { + n.Inputs = append(n.Inputs, toast.Input{ + ID: "userText", + Placeholder: nCategory.ReplyPlaceholder, + }) + + n.Actions = append(n.Actions, toast.Action{ + Content: nCategory.ReplyButtonTitle, + Arguments: "TEXT_REPLY", + InputID: "userText", + }) + } + + encodedPayload, err := wn.encodePayload(n.ActivationArguments, options) + if err != nil { + return fmt.Errorf("failed to encode notification payload: %w", err) + } + n.ActivationArguments = encodedPayload + + for index := range n.Actions { + encodedPayload, err := wn.encodePayload(n.Actions[index].Arguments, options) + if err != nil { + return fmt.Errorf("failed to encode notification payload: %w", err) + } + n.Actions[index].Arguments = encodedPayload + } + + return n.Push() +} + +// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions. +// Registering a category with the same name as a previously registered NotificationCategory will override it. +func (wn *windowsNotifier) RegisterNotificationCategory(category NotificationCategory) error { + wn.categoriesLock.Lock() + defer wn.categoriesLock.Unlock() + + wn.categories[category.ID] = NotificationCategory{ + ID: category.ID, + Actions: category.Actions, + HasReplyField: bool(category.HasReplyField), + ReplyPlaceholder: category.ReplyPlaceholder, + ReplyButtonTitle: category.ReplyButtonTitle, + } + + return wn.saveCategoriesToRegistry() +} + +// RemoveNotificationCategory removes a previously registered NotificationCategory. +func (wn *windowsNotifier) RemoveNotificationCategory(categoryId string) error { + wn.categoriesLock.Lock() + defer wn.categoriesLock.Unlock() + + delete(wn.categories, categoryId) + + return wn.saveCategoriesToRegistry() +} + +// RemoveAllPendingNotifications is a Windows stub that always returns nil. +// (macOS and Linux only) +func (wn *windowsNotifier) RemoveAllPendingNotifications() error { + return nil +} + +// RemovePendingNotification is a Windows stub that always returns nil. +// (macOS and Linux only) +func (wn *windowsNotifier) RemovePendingNotification(_ string) error { + return nil +} + +// RemoveAllDeliveredNotifications is a Windows stub that always returns nil. +// (macOS and Linux only) +func (wn *windowsNotifier) RemoveAllDeliveredNotifications() error { + return nil +} + +// RemoveDeliveredNotification is a Windows stub that always returns nil. +// (macOS and Linux only) +func (wn *windowsNotifier) RemoveDeliveredNotification(_ string) error { + return nil +} + +// RemoveNotification is a Windows stub that always returns nil. +// (Linux-specific) +func (wn *windowsNotifier) RemoveNotification(identifier string) error { + return nil +} + +// encodePayload combines an action ID and user data into a single encoded string +func (wn *windowsNotifier) encodePayload(actionID string, options NotificationOptions) (string, error) { + payload := NotificationPayload{ + Action: actionID, + Options: options, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return actionID, err + } + + encodedPayload := base64.StdEncoding.EncodeToString(jsonData) + return encodedPayload, nil +} + +// decodePayload extracts the action ID and user data from an encoded payload +func decodePayload(encodedString string) (string, NotificationOptions, error) { + jsonData, err := base64.StdEncoding.DecodeString(encodedString) + if err != nil { + return encodedString, NotificationOptions{}, fmt.Errorf("failed to decode base64 payload: %w", err) + } + + var payload NotificationPayload + if err := json.Unmarshal(jsonData, &payload); err != nil { + return encodedString, NotificationOptions{}, fmt.Errorf("failed to unmarshal notification payload: %w", err) + } + + return payload.Action, payload.Options, nil +} + +// parseNotificationResponse updated to use structured payload decoding +func parseNotificationResponse(response string) (action string, options NotificationOptions, err error) { + actionID, options, err := decodePayload(response) + + if err != nil { + fmt.Printf("Warning: Failed to decode notification response: %v\n", err) + return response, NotificationOptions{}, err + } + + return actionID, options, nil +} + +func (wn *windowsNotifier) saveIconToDir() error { + icon, err := application.NewIconFromResource(w32.GetModuleHandle(""), uint16(3)) + if err != nil { + return fmt.Errorf("failed to retrieve application icon: %w", err) + } + + return w32.SaveHIconAsPNG(icon, wn.iconPath) +} + +func (wn *windowsNotifier) saveCategoriesToRegistry() error { + // We assume lock is held by caller + + registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, wn.appName) + + key, _, err := registry.CreateKey( + registry.CURRENT_USER, + registryPath, + registry.ALL_ACCESS, + ) + if err != nil { + return err + } + defer key.Close() + + data, err := json.Marshal(wn.categories) + if err != nil { + return err + } + + return key.SetStringValue(NotificationCategoriesRegistryKey, string(data)) +} + +func (wn *windowsNotifier) loadCategoriesFromRegistry() error { + // We assume lock is held by caller + + registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, wn.appName) + + key, err := registry.OpenKey( + registry.CURRENT_USER, + registryPath, + registry.QUERY_VALUE, + ) + if err != nil { + if err == registry.ErrNotExist { + // Not an error, no saved categories + return nil + } + return fmt.Errorf("failed to open registry key: %w", err) + } + defer key.Close() + + data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey) + if err != nil { + if err == registry.ErrNotExist { + // No value yet, but key exists + return nil + } + return fmt.Errorf("failed to read categories from registry: %w", err) + } + + categories := make(map[string]NotificationCategory) + if err := json.Unmarshal([]byte(data), &categories); err != nil { + return fmt.Errorf("failed to parse notification categories from registry: %w", err) + } + + wn.categories = categories + + return nil +} + +func (wn *windowsNotifier) getUserText(data []toast.UserData) (string, bool) { + for _, d := range data { + if d.Key == "userText" { + return d.Value, true + } + } + return "", false +} + +func (wn *windowsNotifier) getGUID() (string, error) { + keyPath := ToastRegistryPath + wn.appName + + k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE) + if err == nil { + guid, _, err := k.GetStringValue(ToastRegistryGuidKey) + k.Close() + if err == nil && guid != "" { + return guid, nil + } + } + + guid := wn.generateGUID() + + k, _, err = registry.CreateKey(registry.CURRENT_USER, keyPath, registry.WRITE) + if err != nil { + return "", fmt.Errorf("failed to create registry key: %w", err) + } + defer k.Close() + + if err := k.SetStringValue(ToastRegistryGuidKey, guid); err != nil { + return "", fmt.Errorf("failed to write GUID to registry: %w", err) + } + + return guid, nil +} + +func (wn *windowsNotifier) generateGUID() string { + guid := uuid.New() + return fmt.Sprintf("{%s}", guid.String()) +} diff --git a/v3/pkg/services/sqlite/sqlite.go b/v3/pkg/services/sqlite/sqlite.go index c8cde7b7f..daf3ecd05 100644 --- a/v3/pkg/services/sqlite/sqlite.go +++ b/v3/pkg/services/sqlite/sqlite.go @@ -1,99 +1,410 @@ +//wails:include stmt.js package sqlite import ( + "bytes" "context" "database/sql" - "errors" + "fmt" + "sync" + "sync/atomic" + "github.com/pkg/errors" "github.com/wailsapp/wails/v3/pkg/application" _ "modernc.org/sqlite" ) -// ---------------- Service Setup ---------------- -// This is the main Service struct. It can be named anything you like. - type Config struct { - DBFile string + // DBSource is the database URI to use. + // The string ":memory:" can be used to create an in-memory database. + // The sqlite driver can be configured through query parameters. + // For more details see https://pkg.go.dev/modernc.org/sqlite#Driver.Open + DBSource string } -type Service struct { +//wails:inject export { +//wails:inject ExecContext as Execute, +//wails:inject QueryContext as Query +//wails:inject }; +//wails:inject +//wails:inject import { Stmt } from "./stmt.js"; +//wails:inject +//wails:inject **:/** +//wails:inject **: * Prepare creates a prepared statement for later queries or executions. +//wails:inject **: * Multiple queries or executions may be run concurrently from the returned statement. +//wails:inject **: * +//wails:inject **: * The caller must call the statement's Close method when it is no longer needed. +//wails:inject **: * Statements are closed automatically +//wails:inject **: * when the connection they are associated with is closed. +//wails:inject **: * +//wails:inject **: * Prepare supports early cancellation. +//wails:inject j*: * +//wails:inject j*: * @param {string} query +//wails:inject j*: * @returns {Promise & { cancel(): void }} +//wails:inject **: */ +//wails:inject j*:export function Prepare(query) { +//wails:inject t*:export function Prepare(query: string): Promise & { cancel(): void } { +//wails:inject **: const promise = PrepareContext(query); +//wails:inject j*: const wrapper = /** @type {any} */(promise.then(function (id) { +//wails:inject t*: const wrapper: any = (promise.then(function (id) { +//wails:inject **: return id == null ? null : new Stmt( +//wails:inject **: ClosePrepared.bind(null, id), +//wails:inject **: ExecPrepared.bind(null, id), +//wails:inject **: QueryPrepared.bind(null, id)); +//wails:inject **: })); +//wails:inject **: wrapper.cancel = promise.cancel; +//wails:inject **: return wrapper; +//wails:inject **:} +type SQLiteService struct { + lock sync.RWMutex config *Config conn *sql.DB + stmts map[uint64]struct{} } -func New(config *Config) *Service { - return &Service{ - config: config, - } +// New initialises a sqlite service with the default configuration. +func New() *SQLiteService { + return NewWithConfig(nil) } -// ServiceShutdown is called when the app is shutting down -// You can use this to clean up any resources you have allocated -func (s *Service) ServiceShutdown() error { - if s.conn != nil { - return s.conn.Close() - } - return nil +// NewWithConfig initialises a sqlite service with a custom configuration. +// If config is nil, it falls back to the default configuration, i.e. an in-memory database. +// +// The database connection is not opened right away. +// A call to [Service.Open] must succeed before using all other methods. +// If the service is registered with the application, +// [Service.Open] will be called automatically at startup. +func NewWithConfig(config *Config) *SQLiteService { + result := &SQLiteService{} + result.Configure(config) + return result } // ServiceName returns the name of the plugin. // You should use the go module format e.g. github.com/myuser/myplugin -func (s *Service) ServiceName() string { +func (s *SQLiteService) ServiceName() string { return "github.com/wailsapp/wails/v3/plugins/sqlite" } -// ServiceStartup is called when the app is starting up. You can use this to -// initialise any resources you need. -func (s *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { - if s.config.DBFile == "" { - return errors.New(`no database file specified. Please set DBFile in the config to either a filename or use ":memory:" to use an in-memory database`) +// ServiceStartup opens the database connection. +// It returns a non-nil error in case of failures. +func (s *SQLiteService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return s.Open() +} + +// ServiceShutdown closes the database connection. +// It returns a non-nil error in case of failures. +func (s *SQLiteService) ServiceShutdown() error { + return s.Close() +} + +// Configure changes the database service configuration. +// The connection state at call time is preserved. +// Consumers will need to call [Service.Open] manually after Configure +// in order to reconnect with the new configuration. +// +// See [NewWithConfig] for details on configuration. +// +//wails:ignore +func (s *SQLiteService) Configure(config *Config) { + if config == nil { + config = &Config{DBSource: ":memory:"} + } else { + // Clone to prevent changes from the outside. + clone := new(Config) + *clone = *config + config = clone } - db, err := s.Open(s.config.DBFile) - if err != nil { + + s.lock.Lock() + defer s.lock.Unlock() + + s.config = config +} + +// Open validates the current configuration, +// closes the current connection if one is present, +// then opens and validates a new connection. +// +// Even when a non-nil error is returned, +// the database service is left in a consistent state, +// ready for a new call to Open. +func (s *SQLiteService) Open() error { + s.lock.Lock() + defer s.lock.Unlock() + + if s.config.DBSource == "" { + return errors.New(`no database source specified; please set DBSource in the config to a filename or specify ":memory:" to use an in-memory database`) + } + + if err := s.closeImpl(); err != nil { return err } - _ = db + + conn, err := sql.Open("sqlite", s.config.DBSource) + if err != nil { + return errors.Wrap(err, "error opening database connection") + } + + // Test connection + if err := conn.Ping(); err != nil { + _ = conn.Close() + return errors.Wrap(err, "error opening database connection") + } + + s.conn = conn + s.stmts = make(map[uint64]struct{}) return nil } -func (s *Service) Open(dbPath string) (string, error) { - var err error - s.conn, err = sql.Open("sqlite", dbPath) - if err != nil { - return "", err - } - return "Database connection opened", nil +// Close closes the current database connection if one is open, otherwise has no effect. +// Additionally, Close closes all open prepared statements associated to the connection. +// +// Even when a non-nil error is returned, +// the database service is left in a consistent state, +// ready for a call to [Service.Open]. +func (s *SQLiteService) Close() error { + s.lock.Lock() + defer s.lock.Unlock() + + return s.closeImpl() } -func (s *Service) Execute(query string, args ...any) error { +// closeImpl performs the close operation without acquiring the lock first. +// It is the caller's responsibility +// to ensure the lock is held exclusively (in write mode) +// for the entire duration of the call. +func (s *SQLiteService) closeImpl() error { if s.conn == nil { + return nil + } + + for id := range s.stmts { + if stmt, ok := stmts.Load(id); ok { + // WARN: do not delegate to [Stmt.Close], it would cause a deadlock. + // Ignore errors, closing the connection should free up all resources. + _ = stmt.(*Stmt).sqlStmt.Close() + } + } + + err := s.conn.Close() + + // Clear the connection even in case of errors: + // if [sql.DB.Close] returns an error, + // the connection becomes unusable. + s.conn = nil + s.stmts = nil + + return err +} + +// Execute executes a query without returning any rows. +// +//wails:ignore +func (s *SQLiteService) Execute(query string, args ...any) error { + return s.ExecContext(context.Background(), query, args...) +} + +// ExecContext executes a query without returning any rows. +// It supports early cancellation. +// +//wails:internal +func (s *SQLiteService) ExecContext(ctx context.Context, query string, args ...any) error { + s.lock.RLock() + conn := s.conn + s.lock.RUnlock() + + if conn == nil { return errors.New("no open database connection") } - _, err := s.conn.Exec(query, args...) - if err != nil { + _, err := conn.ExecContext(ctx, query, args...) + if err != nil && !errors.Is(err, context.Canceled) { return err } + return nil } -func (s *Service) Select(query string, args ...any) ([]map[string]any, error) { - if s.conn == nil { +// Query executes a query and returns a slice of key-value records, +// one per row, with column names as keys. +// +//wails:ignore +func (s *SQLiteService) Query(query string, args ...any) (Rows, error) { + return s.QueryContext(context.Background(), query, args...) +} + +// QueryContext executes a query and returns a slice of key-value records, +// one per row, with column names as keys. +// It supports early cancellation, returning the slice of results fetched so far. +// +//wails:internal +func (s *SQLiteService) QueryContext(ctx context.Context, query string, args ...any) (Rows, error) { + s.lock.RLock() + conn := s.conn + s.lock.RUnlock() + + if conn == nil { return nil, errors.New("no open database connection") } - rows, err := s.conn.Query(query, args...) + rows, err := conn.QueryContext(ctx, query, args...) if err != nil { - return nil, err + if errors.Is(err, context.Canceled) { + return Rows{}, nil + } else { + return nil, err + } } + + return parseRows(ctx, rows) +} + +// Prepare creates a prepared statement for later queries or executions. +// Multiple queries or executions may be run concurrently from the returned statement. +// +// The caller should call the statement's Close method when it is no longer needed. +// Statements are closed automatically +// when the connection they are associated with is closed. +// +//wails:ignore +func (s *SQLiteService) Prepare(query string) (*Stmt, error) { + return s.PrepareContext(context.Background(), query) +} + +// PrepareContext creates a prepared statement for later queries or executions. +// Multiple queries or executions may be run concurrently from the returned statement. +// +// The caller must call the statement's Close method when it is no longer needed. +// Statements are closed automatically +// when the connection they are associated with is closed. +// +// PrepareContext supports early cancellation. +// +//wails:internal +func (s *SQLiteService) PrepareContext(ctx context.Context, query string) (*Stmt, error) { + s.lock.RLock() + conn := s.conn + s.lock.RUnlock() + + if conn == nil { + return nil, errors.New("no open database connection") + } + + id := nextId.Load() + for id != 0 && !nextId.CompareAndSwap(id, id+1) { + } + if id == 0 { + return nil, errors.New("prepared statement ids exhausted") + } + + stmt, err := conn.PrepareContext(ctx, query) + if err != nil { + if errors.Is(err, context.Canceled) { + return nil, nil + } else { + return nil, err + } + } + + func() { + s.lock.Lock() + defer s.lock.Unlock() + + s.stmts[id] = struct{}{} + }() + + wrapper := &Stmt{ + sqlStmt: stmt, + db: s, + id: id, + } + stmts.Store(id, wrapper) + + return wrapper, nil +} + +// ClosePrepared closes a prepared statement +// obtained with [Service.Prepare] or [Service.PrepareContext]. +// ClosePrepared is idempotent: +// it has no effect on prepared statements that are already closed. +// +//wails:internal +func (s *SQLiteService) ClosePrepared(stmt *Stmt) error { + return stmt.Close() +} + +// ExecPrepared executes a prepared statement +// obtained with [Service.Prepare] or [Service.PrepareContext] +// without returning any rows. +// It supports early cancellation. +// +//wails:internal +func (s *SQLiteService) ExecPrepared(ctx context.Context, stmt *Stmt, args ...any) error { + if stmt == nil { + return errors.New("no prepared statement provided") + } else if stmt.sqlStmt == nil { + return errors.New("prepared statement is not valid") + } + + _, err := stmt.ExecContext(ctx, args...) + if err != nil && !errors.Is(err, context.Canceled) { + return err + } + + return nil +} + +// QueryPrepared executes a prepared statement +// obtained with [Service.Prepare] or [Service.PrepareContext] +// and returns a slice of key-value records, one per row, with column names as keys. +// It supports early cancellation, returning the slice of results fetched so far. +// +//wails:internal +func (s *SQLiteService) QueryPrepared(ctx context.Context, stmt *Stmt, args ...any) (Rows, error) { + if stmt == nil { + return nil, errors.New("no prepared statement provided") + } else if stmt.sqlStmt == nil { + return nil, errors.New("prepared statement is not valid") + } + + rows, err := stmt.sqlStmt.QueryContext(ctx, args...) + if err != nil { + if errors.Is(err, context.Canceled) { + return Rows{}, nil + } else { + return nil, err + } + } + + return parseRows(ctx, rows) +} + +type ( + // Row holds a single row in the result of a query. + // It is a key-value map where keys are column names. + Row = map[string]any + + // Rows holds the result of a query + // as an array of key-value maps where keys are column names. + Rows = []Row +) + +func parseRows(ctx context.Context, rows *sql.Rows) (Rows, error) { defer rows.Close() - columns, err := rows.Columns() - var results []map[string]any + columns, _ := rows.Columns() + values := make([]any, len(columns)) + pointers := make([]any, len(columns)) + results := []map[string]any{} + for rows.Next() { - values := make([]any, len(columns)) - pointers := make([]any, len(columns)) + select { + default: + case <-ctx.Done(): + return results, nil + } for i := range values { pointers[i] = &values[i] @@ -107,21 +418,83 @@ func (s *Service) Select(query string, args ...any) ([]map[string]any, error) { for i, column := range columns { row[column] = values[i] } + results = append(results, row) } return results, nil } -func (s *Service) Close() (string, error) { - if s.conn == nil { - return "", errors.New("no open database connection") +var ( + // stmts holds all currently active prepared statements, + // for all [Service] instances. + stmts sync.Map + + // nextId holds the next available prepared statement id. + // We use a counter to make sure IDs are never reused. + nextId atomic.Uint64 +) + +func init() { + nextId.Store(1) +} + +type ( + sqlStmt = *sql.Stmt + + // Stmt wraps a prepared sql statement pointer. + // It provides the same methods as the [sql.Stmt] type. + // + //wails:internal + Stmt struct { + sqlStmt + db *SQLiteService + id uint64 + } +) + +// Close closes the statement. +// It has no effect when the statement is already closed. +func (s *Stmt) Close() error { + if s == nil || s.sqlStmt == nil { + return nil } - err := s.conn.Close() - if err != nil { - return "", err - } - s.conn = nil - return "Database connection closed", nil + err := s.sqlStmt.Close() + stmts.Delete(s.id) + + func() { + s.db.lock.Lock() + defer s.db.lock.Unlock() + + delete(s.db.stmts, s.id) + }() + + return errors.Wrap(err, "error closing prepared statement") +} + +func (s *Stmt) MarshalText() ([]byte, error) { + var buf bytes.Buffer + buf.Grow(16) + + if _, err := fmt.Fprintf(&buf, "%016x", s.id); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (s *Stmt) UnmarshalText(text []byte) error { + if n, err := fmt.Fscanf(bytes.NewReader(text), "%x", &s.id); n < 1 || err != nil { + return errors.New("invalid prepared statement id") + } + + if stmt, ok := stmts.Load(s.id); ok { + *s = *(stmt.(*Stmt)) + } else { + s.sqlStmt = nil + s.db = nil + } + + return nil } diff --git a/v3/pkg/services/sqlite/stmt.js b/v3/pkg/services/sqlite/stmt.js new file mode 100644 index 000000000..948b0c3dd --- /dev/null +++ b/v3/pkg/services/sqlite/stmt.js @@ -0,0 +1,79 @@ +//@ts-check + +//@ts-ignore: Unused imports +import * as $models from "./models.js"; + +const execSymbol = Symbol("exec"), + querySymbol = Symbol("query"), + closeSymbol = Symbol("close"); + +/** + * Stmt represents a prepared statement for later queries or executions. + * Multiple queries or executions may be run concurrently on the same statement. + * + * The caller must call the statement's Close method when it is no longer needed. + * Statements are closed automatically + * when the connection they are associated with is closed. + */ +export class Stmt { + /** + * Constructs a new prepared statement instance. + * @param {(...args: any[]) => Promise} close + * @param {(...args: any[]) => Promise & { cancel(): void }} exec + * @param {(...args: any[]) => Promise<$models.Rows> & { cancel(): void }} query + */ + constructor(close, exec, query) { + /** + * @member + * @private + * @type {typeof close} + */ + this[closeSymbol] = close; + + /** + * @member + * @private + * @type {typeof exec} + */ + this[execSymbol] = exec; + + /** + * @member + * @private + * @type {typeof query} + */ + this[querySymbol] = query; + } + + /** + * Closes the prepared statement. + * It has no effect when the statement is already closed. + * @returns {Promise} + */ + Close() { + return this[closeSymbol](); + } + + /** + * Executes the prepared statement without returning any rows. + * It supports early cancellation. + * + * @param {any[]} args + * @returns {Promise & { cancel(): void }} + */ + Exec(...args) { + return this[execSymbol](...args); + } + + /** + * Executes the prepared statement + * and returns a slice of key-value records, one per row, with column names as keys. + * It supports early cancellation, returning the array of results fetched so far. + * + * @param {any[]} args + * @returns {Promise<$models.Rows> & { cancel(): void }} + */ + Query(...args) { + return this[querySymbol](...args); + } +} diff --git a/v3/pkg/w32/constants.go b/v3/pkg/w32/constants.go index 234933ba6..83ed4b9d1 100644 --- a/v3/pkg/w32/constants.go +++ b/v3/pkg/w32/constants.go @@ -528,6 +528,9 @@ const ( WM_PAINT = 15 WM_PAINTCLIPBOARD = 777 WM_PAINTICON = 38 + WM_UAHDRAWMENU = 0x0091 + WM_UAHDRAWMENUITEM = 0x0092 + WM_UAHMEASUREMENUITEM = 0x0094 WM_PALETTECHANGED = 785 WM_PALETTEISCHANGING = 784 WM_PARENTNOTIFY = 528 @@ -1371,6 +1374,7 @@ const ( SM_STARTER = 88 SM_SERVERR2 = 89 SM_CMETRICS = 91 + SM_CXPADDEDBORDER = 92 SM_REMOTESESSION = 0x1000 SM_SHUTTINGDOWN = 0x2000 SM_REMOTECONTROL = 0x2001 diff --git a/v3/pkg/w32/gdi32.go b/v3/pkg/w32/gdi32.go index b4b9053e6..04d11ca14 100644 --- a/v3/pkg/w32/gdi32.go +++ b/v3/pkg/w32/gdi32.go @@ -62,6 +62,11 @@ var ( procGetPixelFormat = modgdi32.NewProc("GetPixelFormat") procSetPixelFormat = modgdi32.NewProc("SetPixelFormat") procSwapBuffers = modgdi32.NewProc("SwapBuffers") + procSaveDC = modgdi32.NewProc("SaveDC") + procRestoreDC = modgdi32.NewProc("RestoreDC") + procSelectClipRgn = modgdi32.NewProc("SelectClipRgn") + procExcludeClipRect = modgdi32.NewProc("ExcludeClipRect") + procExtTextOut = modgdi32.NewProc("ExtTextOutW") ) func GetDeviceCaps(hdc HDC, index int) int { @@ -524,3 +529,53 @@ func SwapBuffers(hdc HDC) bool { ret, _, _ := procSwapBuffers.Call(uintptr(hdc)) return ret == TRUE } + +func SaveDC(hdc HDC) int { + ret, _, _ := procSaveDC.Call(uintptr(hdc)) + return int(ret) +} + +func RestoreDC(hdc HDC, nSavedDC int) bool { + ret, _, _ := procRestoreDC.Call( + uintptr(hdc), + uintptr(nSavedDC)) + return ret != 0 +} + +func SelectClipRgn(hdc HDC, hrgn HRGN) int { + ret, _, _ := procSelectClipRgn.Call( + uintptr(hdc), + uintptr(hrgn)) + return int(ret) +} + +func ExcludeClipRect(hdc HDC, left, top, right, bottom int32) int { + ret, _, _ := procExcludeClipRect.Call( + uintptr(hdc), + uintptr(left), + uintptr(top), + uintptr(right), + uintptr(bottom)) + return int(ret) +} + +func ExtTextOut(hdc HDC, x, y int32, fuOptions uint32, lprc *RECT, lpString *uint16, cbCount uint32, lpDx *int) bool { + var rectPtr uintptr + if lprc != nil { + rectPtr = uintptr(unsafe.Pointer(lprc)) + } + var dxPtr uintptr + if lpDx != nil { + dxPtr = uintptr(unsafe.Pointer(lpDx)) + } + ret, _, _ := procExtTextOut.Call( + uintptr(hdc), + uintptr(x), + uintptr(y), + uintptr(fuOptions), + rectPtr, + uintptr(unsafe.Pointer(lpString)), + uintptr(cbCount), + dxPtr) + return ret != 0 +} diff --git a/v3/pkg/w32/icon.go b/v3/pkg/w32/icon.go index 009479323..97d4ad854 100644 --- a/v3/pkg/w32/icon.go +++ b/v3/pkg/w32/icon.go @@ -6,8 +6,11 @@ import ( "bytes" "fmt" "image" + "image/color" "image/draw" "image/png" + "os" + "syscall" "unsafe" ) @@ -90,6 +93,121 @@ func CreateLargeHIconFromImage(fileData []byte) (HICON, error) { return HICON(icon), err } +type ICONINFO struct { + FIcon int32 + XHotspot int32 + YHotspot int32 + HbmMask syscall.Handle + HbmColor syscall.Handle +} + +func SaveHIconAsPNG(hIcon HICON, filePath string) error { + // Load necessary DLLs + user32 := syscall.NewLazyDLL("user32.dll") + gdi32 := syscall.NewLazyDLL("gdi32.dll") + + // Get procedures + getIconInfo := user32.NewProc("GetIconInfo") + getObject := gdi32.NewProc("GetObjectW") + createCompatibleDC := gdi32.NewProc("CreateCompatibleDC") + selectObject := gdi32.NewProc("SelectObject") + getDIBits := gdi32.NewProc("GetDIBits") + deleteObject := gdi32.NewProc("DeleteObject") + deleteDC := gdi32.NewProc("DeleteDC") + + // Get icon info + var iconInfo ICONINFO + ret, _, err := getIconInfo.Call( + uintptr(hIcon), + uintptr(unsafe.Pointer(&iconInfo)), + ) + if ret == 0 { + return err + } + defer deleteObject.Call(uintptr(iconInfo.HbmMask)) + defer deleteObject.Call(uintptr(iconInfo.HbmColor)) + + // Get bitmap info + var bmp BITMAP + ret, _, err = getObject.Call( + uintptr(iconInfo.HbmColor), + unsafe.Sizeof(bmp), + uintptr(unsafe.Pointer(&bmp)), + ) + if ret == 0 { + return err + } + + // Create DC + hdc, _, _ := createCompatibleDC.Call(0) + if hdc == 0 { + return syscall.EINVAL + } + defer deleteDC.Call(hdc) + + // Select bitmap into DC + oldBitmap, _, _ := selectObject.Call(hdc, uintptr(iconInfo.HbmColor)) + defer selectObject.Call(hdc, oldBitmap) + + // Prepare bitmap info header + var bi BITMAPINFO + bi.BmiHeader.BiSize = uint32(unsafe.Sizeof(bi.BmiHeader)) + bi.BmiHeader.BiWidth = bmp.BmWidth + bi.BmiHeader.BiHeight = bmp.BmHeight + bi.BmiHeader.BiPlanes = 1 + bi.BmiHeader.BiBitCount = 32 + bi.BmiHeader.BiCompression = BI_RGB + + // Allocate memory for bitmap bits + width, height := int(bmp.BmWidth), int(bmp.BmHeight) + bufferSize := width * height * 4 + bits := make([]byte, bufferSize) + + // Get bitmap bits + ret, _, err = getDIBits.Call( + hdc, + uintptr(iconInfo.HbmColor), + 0, + uintptr(bmp.BmHeight), + uintptr(unsafe.Pointer(&bits[0])), + uintptr(unsafe.Pointer(&bi)), + DIB_RGB_COLORS, + ) + if ret == 0 { + return err + } + + // Create Go image + img := image.NewRGBA(image.Rect(0, 0, width, height)) + + // Convert DIB to RGBA + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + // DIB is bottom-up, so we need to invert Y + dibIndex := ((height-1-y)*width + x) * 4 + + // BGRA to RGBA + b := bits[dibIndex] + g := bits[dibIndex+1] + r := bits[dibIndex+2] + a := bits[dibIndex+3] + + // Set pixel in the image + img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a}) + } + } + + // Create output file + outFile, err := os.Create(filePath) + if err != nil { + return err + } + defer outFile.Close() + + // Encode and save the image + return png.Encode(outFile, img) +} + func SetWindowIcon(hwnd HWND, icon HICON) { SendMessage(hwnd, WM_SETICON, ICON_SMALL, uintptr(icon)) } diff --git a/v3/pkg/w32/menubar.go b/v3/pkg/w32/menubar.go new file mode 100644 index 000000000..6b11c1e02 --- /dev/null +++ b/v3/pkg/w32/menubar.go @@ -0,0 +1,980 @@ +//go:build windows + +package w32 + +import ( + "os" + "unsafe" +) + +const ( + OBJID_MENU = -3 + ODT_MENU = 1 + // Menu info flags + MIIM_BACKGROUND = 0x00000002 + MIIM_APPLYTOSUBMENUS = 0x80000000 +) + +var ( + menuTheme HTHEME + procSetMenuInfo = moduser32.NewProc("SetMenuInfo") +) + +type DTTOPTS struct { + DwSize uint32 + DwFlags uint32 + CrText uint32 + CrBorder uint32 + CrShadow uint32 + ITextShadowType int32 + PtShadowOffset POINT + iBorderSize int32 + iFontPropId int32 + IColorPropId int32 + IStateId int32 + FApplyOverlay int32 + IGlowSize int32 + PfnDrawTextCallback uintptr + LParam uintptr +} + +const ( + MENU_POPUPITEM = 14 + MENU_BARITEM = 8 // Menu bar item part ID for theme drawing + DTT_TEXTCOLOR = 1 +) + +// Menu item states +const ( + ODS_SELECTED = 0x0001 + ODS_GRAYED = 0x0002 + ODS_DISABLED = 0x0004 + ODS_CHECKED = 0x0008 + ODS_FOCUS = 0x0010 + ODS_DEFAULT = 0x0020 + ODS_HOTLIGHT = 0x0040 + ODS_INACTIVE = 0x0080 + ODS_NOACCEL = 0x0100 + ODS_NOFOCUSRECT = 0x0200 +) + +// Menu Button Image states +const ( + MBI_NORMAL = 1 + MBI_HOT = 2 + MBI_PUSHED = 3 + MBI_DISABLED = 4 +) + +var ( + procGetMenuItemInfo = moduser32.NewProc("GetMenuItemInfoW") + procGetMenuItemCount = moduser32.NewProc("GetMenuItemCount") + procGetMenuItemRect = moduser32.NewProc("GetMenuItemRect") +) + +func GetMenuItemInfo(hmenu HMENU, item uint32, fByPosition bool, lpmii *MENUITEMINFO) bool { + ret, _, _ := procGetMenuItemInfo.Call( + uintptr(hmenu), + uintptr(item), + uintptr(boolToUint(fByPosition)), + uintptr(unsafe.Pointer(lpmii)), + ) + return ret != 0 +} + +func GetMenuItemCount(hmenu HMENU) int { + ret, _, _ := procGetMenuItemCount.Call(uintptr(hmenu)) + return int(ret) +} + +func GetMenuItemRect(hwnd HWND, hmenu HMENU, item uint32, rect *RECT) bool { + ret, _, _ := procGetMenuItemRect.Call( + uintptr(hwnd), + uintptr(hmenu), + uintptr(item), + uintptr(unsafe.Pointer(rect)), + ) + return ret != 0 +} + +// Helper function to convert bool to uint +func boolToUint(b bool) uint { + if b { + return 1 + } + return 0 +} + +type UAHMENU struct { + Hmenu HMENU + Hdc HDC + DwFlags uint32 +} + +type MENUBARINFO struct { + CbSize uint32 + Bar RECT + Menu HMENU + Window HWND + BarFocused int32 + Focused int32 +} + +type DRAWITEMSTRUCT struct { + ControlType uint32 + ControlID uint32 + ItemID uint32 + ItemAction uint32 + ItemState uint32 + HWNDItem HWND + HDC HDC + RcItem RECT + ItemData uintptr +} + +type UAHDRAWMENUITEM struct { + DIS DRAWITEMSTRUCT + UM UAHMENU + UAMI UAHMENUITEM +} + +type UAHMENUITEM struct { + Position int + Umim UAHMENUITEMMETRICS + Umpm UAHMENUPOPUPMETRICS +} +type UAHMENUITEMMETRICS struct { + data [32]byte // Total size of the union in bytes (4 DWORDs * 4 bytes each * 2 arrays) +} + +func (u *UAHMENUITEMMETRICS) RgsizeBar() *[2]struct{ cx, cy uint32 } { + return (*[2]struct{ cx, cy uint32 })(unsafe.Pointer(&u.data)) +} + +func (u *UAHMENUITEMMETRICS) RgsizePopup() *[4]struct{ cx, cy uint32 } { + return (*[4]struct{ cx, cy uint32 })(unsafe.Pointer(&u.data)) +} + +type UAHMEASUREMENUITEM struct { + UM UAHMENU + UAMI UAHMENUITEM + Mis MEASUREITEMSTRUCT +} + +type MEASUREITEMSTRUCT struct { + CtlType uint32 + CtlID uint32 + ItemID uint32 + ItemWidth uint32 + ItemHeight uint32 + ItemData uintptr +} + +type UAHMENUPOPUPMETRICS struct { + Rgcx [4]uint32 // Array of 4 DWORDs + FUpdateMaxWidths uint32 // Bit-field represented as a uint32 +} + +// Helper function to get the value of the fUpdateMaxWidths bit-field +func (u *UAHMENUPOPUPMETRICS) GetFUpdateMaxWidths() uint32 { + return u.FUpdateMaxWidths & 0x3 // Mask to get the first 2 bits +} + +// Helper function to set the value of the fUpdateMaxWidths bit-field +func (u *UAHMENUPOPUPMETRICS) SetFUpdateMaxWidths(value uint32) { + u.FUpdateMaxWidths = (u.FUpdateMaxWidths &^ 0x3) | (value & 0x3) // Clear and set the first 2 bits +} + +type MenuBarTheme struct { + TitleBarBackground *uint32 + TitleBarText *uint32 + MenuBarBackground *uint32 // Separate color for menubar + MenuHoverBackground *uint32 + MenuHoverText *uint32 + MenuSelectedBackground *uint32 + MenuSelectedText *uint32 + + // private brushes + titleBarBackgroundBrush HBRUSH + menuBarBackgroundBrush HBRUSH // Separate brush for menubar + menuHoverBackgroundBrush HBRUSH + menuSelectedBackgroundBrush HBRUSH +} + +func createColourWithDefaultColor(color *uint32, def uint32) *uint32 { + if color == nil { + return &def + } + return color +} + +func (d *MenuBarTheme) Init() { + d.TitleBarBackground = createColourWithDefaultColor(d.TitleBarBackground, RGB(25, 25, 26)) + d.TitleBarText = createColourWithDefaultColor(d.TitleBarText, RGB(222, 222, 222)) + d.MenuBarBackground = createColourWithDefaultColor(d.MenuBarBackground, RGB(33, 33, 33)) + d.MenuSelectedText = createColourWithDefaultColor(d.MenuSelectedText, RGB(222, 222, 222)) + d.MenuSelectedBackground = createColourWithDefaultColor(d.MenuSelectedBackground, RGB(48, 48, 48)) + d.MenuHoverText = createColourWithDefaultColor(d.MenuHoverText, RGB(222, 222, 222)) + d.MenuHoverBackground = createColourWithDefaultColor(d.MenuHoverBackground, RGB(48, 48, 48)) + // Create brushes + d.titleBarBackgroundBrush = CreateSolidBrush(*d.TitleBarBackground) + d.menuBarBackgroundBrush = CreateSolidBrush(*d.MenuBarBackground) + d.menuHoverBackgroundBrush = CreateSolidBrush(*d.MenuHoverBackground) + d.menuSelectedBackgroundBrush = CreateSolidBrush(*d.MenuSelectedBackground) +} + +// SetMenuBackground sets the menu background brush directly +func (d *MenuBarTheme) SetMenuBackground(hmenu HMENU) { + var mi MENUINFO + mi.CbSize = uint32(unsafe.Sizeof(mi)) + mi.FMask = MIIM_BACKGROUND | MIIM_APPLYTOSUBMENUS + mi.HbrBack = d.menuBarBackgroundBrush // Use separate menubar brush + SetMenuInfo(hmenu, &mi) +} + +// SetMenuInfo wrapper function +func SetMenuInfo(hmenu HMENU, lpcmi *MENUINFO) bool { + ret, _, _ := procSetMenuInfo.Call( + uintptr(hmenu), + uintptr(unsafe.Pointer(lpcmi))) + return ret != 0 +} + +func CreateSolidBrush(color COLORREF) HBRUSH { + ret, _, _ := procCreateSolidBrush.Call( + uintptr(color), + ) + return HBRUSH(ret) +} + +func RGB(r, g, b byte) uint32 { + return uint32(r) | uint32(g)<<8 | uint32(b)<<16 +} + +func RGBptr(r, g, b byte) *uint32 { + result := uint32(r) | uint32(g)<<8 | uint32(b)<<16 + return &result +} + +// Track hover state for menubar items when maximized +var ( + currentHoverItem int = -1 + menuIsOpen bool = false // Track if a dropdown menu is open +) + +func MenuBarWndProc(hwnd HWND, msg uint32, wParam WPARAM, lParam LPARAM, theme *MenuBarTheme) (bool, LRESULT) { + // Only proceed if we have a theme (either for dark or light mode) + if theme == nil { + return false, 0 + } + switch msg { + case WM_UAHDRAWMENU: + udm := (*UAHMENU)(unsafe.Pointer(lParam)) + + // Check if maximized first + isMaximized := IsZoomed(hwnd) + + // get the menubar rect + var menuBarInfo MENUBARINFO + menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo)) + if !GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) { + return false, 0 + } + + winRect := GetWindowRect(hwnd) + + // the rcBar is offset by the window rect + rc := menuBarInfo.Bar + OffsetRect(&rc, int(-winRect.Left), int(-winRect.Top)) + + // DEBUG: Log the coordinates + // println("WM_UAHDRAWMENU: maximized=", isMaximized) + // println(" menubar screen rect: L=", menuBarInfo.Bar.Left, "T=", menuBarInfo.Bar.Top, + // "R=", menuBarInfo.Bar.Right, "B=", menuBarInfo.Bar.Bottom) + // println(" window rect: L=", winRect.Left, "T=", winRect.Top, + // "R=", winRect.Right, "B=", winRect.Bottom) + // println(" converted rect: L=", rc.Left, "T=", rc.Top, + // "R=", rc.Right, "B=", rc.Bottom) + + // When maximized, Windows extends the window beyond the visible area + // We need to adjust the menubar rect to ensure it's fully visible + if isMaximized { + // Get the frame size - this is how much the window extends beyond visible area when maximized + frameY := GetSystemMetrics(SM_CYSIZEFRAME) + paddedBorder := GetSystemMetrics(SM_CXPADDEDBORDER) + + // In Windows 10/11, the actual border is frame + padding + borderSize := frameY + paddedBorder + + // println(" Frame metrics: frameY=", frameY, "paddedBorder=", paddedBorder, "borderSize=", borderSize) + + // First, fill the area from the top of the visible area to the menubar + topFillRect := RECT{ + Left: rc.Left, + Top: int32(borderSize), // Start of visible area in window coordinates + Right: rc.Right, + Bottom: rc.Top, // Up to where the menubar starts + } + FillRect(udm.Hdc, &topFillRect, theme.menuBarBackgroundBrush) + } + + // Fill the entire menubar background with dark color + FillRect(udm.Hdc, &rc, theme.menuBarBackgroundBrush) + + // Paint over the menubar border explicitly + // The border is typically 1-2 pixels at the bottom + borderRect := rc + borderRect.Top = borderRect.Bottom - 1 + borderRect.Bottom = borderRect.Bottom + 2 + FillRect(udm.Hdc, &borderRect, theme.menuBarBackgroundBrush) + + // When maximized, we still need to handle the drawing ourselves + // Some projects found that returning false here causes issues + + // When maximized, manually draw all menu items here + if isMaximized { + // Draw each menu item manually + itemCount := GetMenuItemCount(menuBarInfo.Menu) + for i := 0; i < itemCount; i++ { + var itemRect RECT + if GetMenuItemRect(hwnd, menuBarInfo.Menu, uint32(i), &itemRect) { + // Convert to window coordinates + OffsetRect(&itemRect, int(-winRect.Left), int(-winRect.Top)) + + // Check if this item is hovered + if i == currentHoverItem { + // Fill with hover background + FillRect(udm.Hdc, &itemRect, theme.menuHoverBackgroundBrush) + } + + // Get menu text + menuString := make([]uint16, 256) + mii := MENUITEMINFO{ + CbSize: uint32(unsafe.Sizeof(MENUITEMINFO{})), + FMask: MIIM_STRING, + DwTypeData: &menuString[0], + Cch: uint32(len(menuString) - 1), + } + + if GetMenuItemInfo(menuBarInfo.Menu, uint32(i), true, &mii) { + // Draw the text + if i == currentHoverItem { + SetTextColor(udm.Hdc, COLORREF(*theme.MenuHoverText)) + } else { + SetTextColor(udm.Hdc, COLORREF(*theme.TitleBarText)) + } + SetBkMode(udm.Hdc, TRANSPARENT) + DrawText(udm.Hdc, menuString, -1, &itemRect, DT_CENTER|DT_SINGLELINE|DT_VCENTER) + } + } + } + } + + // Return the original HDC so Windows can draw the menu text + return true, LRESULT(udm.Hdc) + case WM_DRAWITEM: + // Handle owner-drawn menu items + dis := (*DRAWITEMSTRUCT)(unsafe.Pointer(lParam)) + + // Check if this is a menu item + if dis.ControlType == ODT_MENU { + // Draw the menu item background + var bgBrush HBRUSH + var textColor uint32 + + if dis.ItemState&ODS_SELECTED != 0 { + // Selected state + bgBrush = theme.menuSelectedBackgroundBrush + textColor = *theme.MenuSelectedText + } else { + // Normal state + bgBrush = theme.titleBarBackgroundBrush + textColor = *theme.TitleBarText + } + + // Fill background + FillRect(dis.HDC, &dis.RcItem, bgBrush) + + // Draw text if we have item data + if dis.ItemData != 0 { + text := (*uint16)(unsafe.Pointer(dis.ItemData)) + if text != nil { + // Set text color and draw + SetTextColor(dis.HDC, COLORREF(textColor)) + SetBkMode(dis.HDC, TRANSPARENT) + DrawText(dis.HDC, (*[256]uint16)(unsafe.Pointer(text))[:], -1, &dis.RcItem, DT_CENTER|DT_SINGLELINE|DT_VCENTER) + } + } + + return true, 1 + } + case WM_UAHDRAWMENUITEM: + udmi := (*UAHDRAWMENUITEM)(unsafe.Pointer(lParam)) + + // Check if we're getting menu item draw messages when maximized or fullscreen + isMaximized := IsZoomed(hwnd) + + // Create buffer for menu text + menuString := make([]uint16, 256) + + // Setup menu item info structure + mii := MENUITEMINFO{ + CbSize: uint32(unsafe.Sizeof(MENUITEMINFO{})), + FMask: MIIM_STRING | MIIM_SUBMENU, + DwTypeData: &menuString[0], + Cch: uint32(len(menuString) - 1), + } + + if !GetMenuItemInfo(udmi.UM.Hmenu, uint32(udmi.UAMI.Position), true, &mii) { + // Failed to get menu item info, let default handler process + return false, 0 + } + + // Remove automatic popup on hover - menus should only open on click + // This was causing the menu to appear at wrong coordinates + dwFlags := uint32(DT_CENTER | DT_SINGLELINE | DT_VCENTER) + + // When maximized/fullscreen, try without VCENTER to see if text appears + if isMaximized && os.Getenv("WAILS_TEST_NO_VCENTER") == "1" { + dwFlags = uint32(DT_CENTER | DT_SINGLELINE) + println(" Using dwFlags without VCENTER") + } + + // Check if this is a menubar item + // When dwFlags has 0x0A00 (2560) it's a menubar item + isMenuBarItem := (udmi.UM.DwFlags&0x0A00) == 0x0A00 || udmi.UM.DwFlags == 0 + + // Use different colors for menubar vs popup items + var bgBrush HBRUSH + var textColor uint32 + + if udmi.DIS.ItemState&ODS_HOTLIGHT != 0 { + // Hot state - use a specific color for hover + bgBrush = theme.menuHoverBackgroundBrush + textColor = *theme.MenuHoverText + } else if udmi.DIS.ItemState&ODS_SELECTED != 0 { + // Selected state + bgBrush = theme.menuSelectedBackgroundBrush + textColor = *theme.MenuSelectedText + } else { + // Normal state + if isMenuBarItem { + // Menubar items in normal state + bgBrush = theme.menuBarBackgroundBrush + textColor = *theme.TitleBarText + } else { + // Popup menu items in normal state - use same color as menubar + bgBrush = theme.menuBarBackgroundBrush + textColor = *theme.TitleBarText + } + } + + // Fill background + if bgBrush != 0 { + FillRect(udmi.UM.Hdc, &udmi.DIS.RcItem, bgBrush) + } + + // Draw text + SetTextColor(udmi.UM.Hdc, COLORREF(textColor)) + SetBkMode(udmi.UM.Hdc, TRANSPARENT) + + // When maximized/fullscreen and menubar item, use the same font settings as drawMenuBarText + if isMaximized && isMenuBarItem { + // Create a non-bold font explicitly + menuFont := LOGFONT{ + Height: -12, // Standard Windows menu font height (9pt) + Weight: 400, // FW_NORMAL (not bold) + CharSet: 1, // DEFAULT_CHARSET + Quality: 5, // CLEARTYPE_QUALITY + PitchAndFamily: 0, // DEFAULT_PITCH + } + // Set font face name to "Segoe UI" (Windows default) + fontName := []uint16{'S', 'e', 'g', 'o', 'e', ' ', 'U', 'I', 0} + copy(menuFont.FaceName[:], fontName) + + hFont := CreateFontIndirect(&menuFont) + if hFont != 0 { + oldFont := SelectObject(udmi.UM.Hdc, HGDIOBJ(hFont)) + DrawText(udmi.UM.Hdc, menuString, -1, &udmi.DIS.RcItem, dwFlags) + SelectObject(udmi.UM.Hdc, oldFont) + DeleteObject(HGDIOBJ(hFont)) + } else { + DrawText(udmi.UM.Hdc, menuString, -1, &udmi.DIS.RcItem, dwFlags) + } + return true, 4 // CDRF_SKIPDEFAULT + } else { + DrawText(udmi.UM.Hdc, menuString, -1, &udmi.DIS.RcItem, dwFlags) + } + + // Return appropriate value based on whether we're in maximized/fullscreen + // For maximized, we need to ensure Windows doesn't override our drawing + if isMaximized { + // Skip default processing to prevent Windows from overriding our colors + return true, 4 // CDRF_SKIPDEFAULT + } + // Return 1 to indicate we've handled the drawing + return true, 1 + case WM_UAHMEASUREMENUITEM: + // Let the default window procedure handle the menu item measurement + // We're not modifying the default sizing anymore + result := DefWindowProc(hwnd, msg, wParam, lParam) + + return true, result + case WM_NCPAINT: + // Paint our custom menubar first + paintDarkMenuBar(hwnd, theme) + + // Then let Windows do its default painting + result := DefWindowProc(hwnd, msg, wParam, lParam) + + // Paint again to ensure our painting is on top + paintDarkMenuBar(hwnd, theme) + + return true, result + case WM_NCACTIVATE: + result := DefWindowProc(hwnd, msg, wParam, lParam) + + // Force paint the menubar with dark background + paintDarkMenuBar(hwnd, theme) + + return false, result + case WM_PAINT: + // Let Windows paint first + result := DefWindowProc(hwnd, msg, wParam, lParam) + + // Then paint our menubar + paintDarkMenuBar(hwnd, theme) + + return false, result + case WM_ACTIVATEAPP, WM_ACTIVATE: + // Handle app activation/deactivation + result := DefWindowProc(hwnd, msg, wParam, lParam) + + // Repaint menubar + paintDarkMenuBar(hwnd, theme) + + return false, result + case WM_SIZE, WM_WINDOWPOSCHANGED: + // Handle window size changes + result := DefWindowProc(hwnd, msg, wParam, lParam) + + // Repaint menubar after size change + paintDarkMenuBar(hwnd, theme) + + // CRITICAL: Force complete menubar redraw when maximized + if msg == WM_SIZE && wParam == SIZE_MAXIMIZED { + // Invalidate the entire menubar area to force redraw + var mbi MENUBARINFO + mbi.CbSize = uint32(unsafe.Sizeof(mbi)) + if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &mbi) { + InvalidateRect(hwnd, &mbi.Bar, true) + DrawMenuBar(hwnd) + } + } + + return false, result + case WM_SETFOCUS, WM_KILLFOCUS: + // Handle focus changes (e.g., when inspector opens) + result := DefWindowProc(hwnd, msg, wParam, lParam) + + // Repaint menubar after focus change + paintDarkMenuBar(hwnd, theme) + + return false, result + case WM_ERASEBKGND: + // When maximized, draw menubar text here + if IsZoomed(hwnd) { + var menuBarInfo MENUBARINFO + menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo)) + if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) { + hdc := HDC(wParam) + drawMenuBarText(hwnd, hdc, &menuBarInfo, theme) + } + } + return false, 0 + case WM_NCMOUSEMOVE, WM_MOUSEMOVE: + // Track mouse movement for hover effects when maximized + if IsZoomed(hwnd) { + // Don't process hover changes while menu is open + if menuIsOpen { + return false, 0 + } + + var screenX, screenY int32 + if msg == WM_NCMOUSEMOVE { + // For NC messages, lParam contains screen coordinates + screenX = int32(LOWORD(uint32(lParam))) + screenY = int32(HIWORD(uint32(lParam))) + } else { + // For regular MOUSEMOVE, convert client to screen coordinates + clientX := int32(LOWORD(uint32(lParam))) + clientY := int32(HIWORD(uint32(lParam))) + sx, sy := ClientToScreen(hwnd, int(clientX), int(clientY)) + screenX = int32(sx) + screenY = int32(sy) + } + + // Check if we're over the menubar + var menuBarInfo MENUBARINFO + menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo)) + if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) { + // menuBarInfo.Bar already contains screen coordinates + // Check if mouse is over menubar using screen coordinates + if screenX >= menuBarInfo.Bar.Left && screenX <= menuBarInfo.Bar.Right && + screenY >= menuBarInfo.Bar.Top && screenY <= menuBarInfo.Bar.Bottom { + + // Always re-request mouse tracking to ensure we get leave messages + TrackMouseEvent(&TRACKMOUSEEVENT{ + CbSize: uint32(unsafe.Sizeof(TRACKMOUSEEVENT{})), + DwFlags: TME_LEAVE | TME_NONCLIENT, + HwndTrack: hwnd, + DwHoverTime: 0, + }) + // Find which menu item we're over + itemCount := GetMenuItemCount(menuBarInfo.Menu) + newHoverItem := -1 + + for i := 0; i < itemCount; i++ { + var itemRect RECT + if GetMenuItemRect(hwnd, menuBarInfo.Menu, uint32(i), &itemRect) { + // itemRect is already in screen coordinates from GetMenuItemRect + // Check using screen coordinates + if screenX >= itemRect.Left && screenX <= itemRect.Right && + screenY >= itemRect.Top && screenY <= itemRect.Bottom { + newHoverItem = i + break + } + } + } + + // If hover item changed, update and redraw just the menubar + if newHoverItem != currentHoverItem { + currentHoverItem = newHoverItem + // Get the actual menubar rect for precise invalidation + winRect := GetWindowRect(hwnd) + menubarRect := menuBarInfo.Bar + // Convert to window coordinates + menubarRect.Left -= winRect.Left + menubarRect.Top -= winRect.Top + menubarRect.Right -= winRect.Left + menubarRect.Bottom -= winRect.Top + // Invalidate only the menubar + InvalidateRect(hwnd, &menubarRect, false) + } + } else { + // Mouse left menubar + if currentHoverItem != -1 { + currentHoverItem = -1 + // Get the actual menubar rect + winRect := GetWindowRect(hwnd) + menubarRect := menuBarInfo.Bar + // Convert to window coordinates + menubarRect.Left -= winRect.Left + menubarRect.Top -= winRect.Top + menubarRect.Right -= winRect.Left + menubarRect.Bottom -= winRect.Top + InvalidateRect(hwnd, &menubarRect, false) + } + } + } + } + return false, 0 + case WM_NCLBUTTONDOWN: + // When clicking on menubar, clear hover state immediately + if IsZoomed(hwnd) && currentHoverItem != -1 { + // Check if click is on menubar + var menuBarInfo MENUBARINFO + menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo)) + if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) { + // Get click position (screen coordinates) + clickX := int32(LOWORD(uint32(lParam))) + clickY := int32(HIWORD(uint32(lParam))) + + if clickX >= menuBarInfo.Bar.Left && clickX <= menuBarInfo.Bar.Right && + clickY >= menuBarInfo.Bar.Top && clickY <= menuBarInfo.Bar.Bottom { + // Click is on menubar - clear hover + currentHoverItem = -1 + } + } + } + return false, 0 + case WM_NCMOUSELEAVE, WM_MOUSELEAVE: + // Clear hover state when mouse leaves (but not if menu is open) + if IsZoomed(hwnd) && currentHoverItem != -1 && !menuIsOpen { + currentHoverItem = -1 + // Get menubar info for precise invalidation + var menuBarInfo MENUBARINFO + menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo)) + if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) { + winRect := GetWindowRect(hwnd) + menubarRect := menuBarInfo.Bar + menubarRect.Left -= winRect.Left + menubarRect.Top -= winRect.Top + menubarRect.Right -= winRect.Left + menubarRect.Bottom -= winRect.Top + InvalidateRect(hwnd, &menubarRect, false) + } + } + return false, 0 + case WM_ENTERMENULOOP: + // Menu is being opened - clear hover state + menuIsOpen = true + if IsZoomed(hwnd) && currentHoverItem != -1 { + oldHoverItem := currentHoverItem + currentHoverItem = -1 + // Redraw the previously hovered item to remove hover effect + var menuBarInfo MENUBARINFO + menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo)) + if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) { + var itemRect RECT + if GetMenuItemRect(hwnd, menuBarInfo.Menu, uint32(oldHoverItem), &itemRect) { + winRect := GetWindowRect(hwnd) + // Convert to window coordinates + itemRect.Left -= winRect.Left + itemRect.Top -= winRect.Top + itemRect.Right -= winRect.Left + itemRect.Bottom -= winRect.Top + // Add some padding + itemRect.Left -= 5 + itemRect.Right += 5 + itemRect.Top -= 5 + itemRect.Bottom += 5 + InvalidateRect(hwnd, &itemRect, false) + } + } + } + return false, 0 + case WM_EXITMENULOOP: + // Menu has been closed + menuIsOpen = false + // Clear any existing hover state first + currentHoverItem = -1 + // Force a complete menubar redraw + if IsZoomed(hwnd) { + var menuBarInfo MENUBARINFO + menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo)) + if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) { + winRect := GetWindowRect(hwnd) + menubarRect := menuBarInfo.Bar + menubarRect.Left -= winRect.Left + menubarRect.Top -= winRect.Top + menubarRect.Right -= winRect.Left + menubarRect.Bottom -= winRect.Top + InvalidateRect(hwnd, &menubarRect, false) + } + // Force a timer to restart mouse tracking + SetTimer(hwnd, 1001, 50, 0) + } + return false, 0 + case WM_TIMER: + // Handle our mouse tracking restart timer + if wParam == 1001 { + KillTimer(hwnd, 1001) + if IsZoomed(hwnd) { + // Get current mouse position and simulate a mouse move + x, y, _ := GetCursorPos() + // Check if mouse is over the window + winRect := GetWindowRect(hwnd) + if x >= int(winRect.Left) && x <= int(winRect.Right) && + y >= int(winRect.Top) && y <= int(winRect.Bottom) { + // Check if we're over the menubar specifically + var menuBarInfo MENUBARINFO + menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo)) + if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) { + if int32(x) >= menuBarInfo.Bar.Left && int32(x) <= menuBarInfo.Bar.Right && + int32(y) >= menuBarInfo.Bar.Top && int32(y) <= menuBarInfo.Bar.Bottom { + // Post a non-client mouse move to restart tracking + PostMessage(hwnd, WM_NCMOUSEMOVE, 0, uintptr(y)<<16|uintptr(x)&0xFFFF) + } else { + // Convert to client coordinates for regular mouse move + clientX, clientY, _ := ScreenToClient(hwnd, x, y) + // Post a mouse move message to restart tracking + PostMessage(hwnd, WM_MOUSEMOVE, 0, uintptr(clientY)<<16|uintptr(clientX)&0xFFFF) + } + } + } + } + return true, 0 + } + return false, 0 + } + return false, 0 +} + +// paintDarkMenuBar paints the menubar with dark background +func paintDarkMenuBar(hwnd HWND, theme *MenuBarTheme) { + // Get menubar info + var menuBarInfo MENUBARINFO + menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo)) + if !GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) { + return + } + + // Get window DC + hdc := GetWindowDC(hwnd) + if hdc == 0 { + return + } + defer ReleaseDC(hwnd, hdc) + + // Check if window is maximized or fullscreen + isMaximized := IsZoomed(hwnd) + isFullscreen := false + + // Check if window is in fullscreen by checking if it covers the monitor + windowRect := GetWindowRect(hwnd) + monitor := MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY) + var monitorInfo MONITORINFO + monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo)) + if GetMonitorInfo(monitor, &monitorInfo) { + // If window matches monitor bounds, it's fullscreen + if windowRect.Left == monitorInfo.RcMonitor.Left && + windowRect.Top == monitorInfo.RcMonitor.Top && + windowRect.Right == monitorInfo.RcMonitor.Right && + windowRect.Bottom == monitorInfo.RcMonitor.Bottom { + isFullscreen = true + } + } + + // When maximized or fullscreen, we need to handle the special case + if isMaximized || isFullscreen { + // Convert menubar rect from screen to window coordinates + menubarRect := menuBarInfo.Bar + menubarRect.Left -= windowRect.Left + menubarRect.Top -= windowRect.Top + menubarRect.Right -= windowRect.Left + menubarRect.Bottom -= windowRect.Top + + if isMaximized && !isFullscreen { + // Get the frame size (only for maximized, not fullscreen) + frameY := GetSystemMetrics(SM_CYSIZEFRAME) + paddedBorder := GetSystemMetrics(SM_CXPADDEDBORDER) + borderSize := frameY + paddedBorder + + // Fill from visible area top to menubar + topFillRect := RECT{ + Left: menubarRect.Left, + Top: int32(borderSize), // Start of visible area + Right: menubarRect.Right, + Bottom: menubarRect.Top, + } + FillRect(hdc, &topFillRect, theme.menuBarBackgroundBrush) + } else if isFullscreen { + // In fullscreen, fill from the very top + topFillRect := RECT{ + Left: menubarRect.Left, + Top: 0, // Start from top in fullscreen + Right: menubarRect.Right, + Bottom: menubarRect.Top, + } + FillRect(hdc, &topFillRect, theme.menuBarBackgroundBrush) + } + + // Fill the menubar itself + FillRect(hdc, &menubarRect, theme.menuBarBackgroundBrush) + } else { + // Paint the menubar background with dark color + FillRect(hdc, &menuBarInfo.Bar, theme.menuBarBackgroundBrush) + } + + // Get window and client rects to find the non-client area + clientRect := GetClientRect(hwnd) + + // Convert client rect top-left to screen coordinates + _, screenY := ClientToScreen(hwnd, int(clientRect.Left), int(clientRect.Top)) + + // Paint the entire area between menubar and client area + // This should cover any borders + borderRect := RECT{ + Left: 0, + Top: menuBarInfo.Bar.Bottom - windowRect.Top, + Right: windowRect.Right - windowRect.Left, + Bottom: int32(screenY) - windowRect.Top, + } + FillRect(hdc, &borderRect, theme.menuBarBackgroundBrush) + + // When maximized or fullscreen, also draw menubar text + if isMaximized || isFullscreen { + drawMenuBarText(hwnd, hdc, &menuBarInfo, theme) + } +} + +func drawMenuBarText(hwnd HWND, hdc HDC, menuBarInfo *MENUBARINFO, theme *MenuBarTheme) { + // Get the menu handle + hmenu := menuBarInfo.Menu + if hmenu == 0 { + return + } + + // Get the number of menu items + itemCount := GetMenuItemCount(hmenu) + if itemCount <= 0 { + return + } + + // Create a non-bold font explicitly + menuFont := LOGFONT{ + Height: -12, // Standard Windows menu font height (9pt) + Weight: 400, // FW_NORMAL (not bold) + CharSet: 1, // DEFAULT_CHARSET + Quality: 5, // CLEARTYPE_QUALITY + PitchAndFamily: 0, // DEFAULT_PITCH + } + // Set font face name to "Segoe UI" (Windows default) + fontName := []uint16{'S', 'e', 'g', 'o', 'e', ' ', 'U', 'I', 0} + copy(menuFont.FaceName[:], fontName) + + hFont := CreateFontIndirect(&menuFont) + if hFont != 0 { + oldFont := SelectObject(hdc, HGDIOBJ(hFont)) + defer func() { + SelectObject(hdc, oldFont) + DeleteObject(HGDIOBJ(hFont)) + }() + } + + // Set text color and background mode + SetTextColor(hdc, COLORREF(*theme.TitleBarText)) + SetBkMode(hdc, TRANSPARENT) + + // Get the window rect for coordinate conversion + winRect := GetWindowRect(hwnd) + + // Iterate through each menu item + for i := 0; i < itemCount; i++ { + // Get the menu item rect + var itemRect RECT + if !GetMenuItemRect(hwnd, hmenu, uint32(i), &itemRect) { + continue + } + + // Convert to window coordinates + OffsetRect(&itemRect, int(-winRect.Left), int(-winRect.Top)) + + // Check if this item is hovered + if i == currentHoverItem { + // Fill with hover background + FillRect(hdc, &itemRect, theme.menuHoverBackgroundBrush) + } + + // Get the menu item text + menuString := make([]uint16, 256) + mii := MENUITEMINFO{ + CbSize: uint32(unsafe.Sizeof(MENUITEMINFO{})), + FMask: MIIM_STRING, + DwTypeData: &menuString[0], + Cch: uint32(len(menuString) - 1), + } + + if GetMenuItemInfo(hmenu, uint32(i), true, &mii) { + // Set text color based on hover state + if i == currentHoverItem { + SetTextColor(hdc, COLORREF(*theme.MenuHoverText)) + } else { + SetTextColor(hdc, COLORREF(*theme.TitleBarText)) + } + // Draw the text + DrawText(hdc, menuString, -1, &itemRect, DT_CENTER|DT_SINGLELINE|DT_VCENTER) + } + } +} diff --git a/v3/pkg/w32/ole32.go b/v3/pkg/w32/ole32.go index 8e7c20d4a..a7d204912 100644 --- a/v3/pkg/w32/ole32.go +++ b/v3/pkg/w32/ole32.go @@ -7,9 +7,10 @@ package w32 import ( - "github.com/wailsapp/go-webview2/pkg/combridge" "syscall" "unsafe" + + "github.com/wailsapp/go-webview2/pkg/combridge" ) var ( @@ -19,6 +20,7 @@ var ( procCoInitialize = modole32.NewProc("CoInitialize") procOleInitialize = modole32.NewProc("OleInitialize") procCoUninitialize = modole32.NewProc("CoUninitialize") + procCoCreateInstance = modole32.NewProc("CoCreateInstance") procCreateStreamOnHGlobal = modole32.NewProc("CreateStreamOnHGlobal") procRegisterDragDrop = modole32.NewProc("RegisterDragDrop") procRevokeDragDrop = modole32.NewProc("RevokeDragDrop") @@ -49,6 +51,26 @@ func CoUninitialize() { procCoUninitialize.Call() } +func CoCreateInstance(clsid *syscall.GUID, dwClsContext uintptr, riid *syscall.GUID, ppv uintptr) HRESULT { + ret, _, _ := procCoCreateInstance.Call( + uintptr(unsafe.Pointer(clsid)), + 0, + uintptr(dwClsContext), + uintptr(unsafe.Pointer(riid)), + uintptr(ppv)) + + switch uint32(ret) { + case E_INVALIDARG: + panic("CoCreateInstance failed with E_INVALIDARG") + case E_OUTOFMEMORY: + panic("CoCreateInstance failed with E_OUTOFMEMORY") + case E_UNEXPECTED: + panic("CoCreateInstance failed with E_UNEXPECTED") + } + + return HRESULT(ret) +} + func CreateStreamOnHGlobal(hGlobal HGLOBAL, fDeleteOnRelease bool) *IStream { stream := new(IStream) ret, _, _ := procCreateStreamOnHGlobal.Call( diff --git a/v3/pkg/w32/taskbar.go b/v3/pkg/w32/taskbar.go new file mode 100644 index 000000000..7f2a697d6 --- /dev/null +++ b/v3/pkg/w32/taskbar.go @@ -0,0 +1,95 @@ +//go:build windows + +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + CLSID_TaskbarList = syscall.GUID{Data1: 0x56FDF344, Data2: 0xFD6D, Data3: 0x11D0, Data4: [8]byte{0x95, 0x8A, 0x00, 0x60, 0x97, 0xC9, 0xA0, 0x90}} + IID_ITaskbarList3 = syscall.GUID{Data1: 0xEA1AFB91, Data2: 0x9E28, Data3: 0x4B86, Data4: [8]byte{0x90, 0xE9, 0x9E, 0x9F, 0x8A, 0x5E, 0xEF, 0xAF}} +) + +// ITaskbarList3 interface for Windows taskbar functionality +type ITaskbarList3 struct { + lpVtbl *taskbarList3Vtbl +} + +type taskbarList3Vtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + HrInit uintptr + AddTab uintptr + DeleteTab uintptr + ActivateTab uintptr + SetActiveAlt uintptr + MarkFullscreenWindow uintptr + SetProgressValue uintptr + SetProgressState uintptr + RegisterTab uintptr + UnregisterTab uintptr + SetTabOrder uintptr + SetTabActive uintptr + ThumbBarAddButtons uintptr + ThumbBarUpdateButtons uintptr + ThumbBarSetImageList uintptr + SetOverlayIcon uintptr + SetThumbnailTooltip uintptr + SetThumbnailClip uintptr +} + +// NewTaskbarList3 creates a new instance of ITaskbarList3 +func NewTaskbarList3() (*ITaskbarList3, error) { + const COINIT_APARTMENTTHREADED = 0x2 + + if hrInit := CoInitializeEx(COINIT_APARTMENTTHREADED); hrInit != 0 && hrInit != 0x1 { + return nil, syscall.Errno(hrInit) + } + + var taskbar *ITaskbarList3 + hr := CoCreateInstance( + &CLSID_TaskbarList, + CLSCTX_INPROC_SERVER, + &IID_ITaskbarList3, + uintptr(unsafe.Pointer(&taskbar)), + ) + + if hr != 0 { + CoUninitialize() + return nil, syscall.Errno(hr) + } + + if r, _, _ := syscall.SyscallN(taskbar.lpVtbl.HrInit, uintptr(unsafe.Pointer(taskbar))); r != 0 { + syscall.SyscallN(taskbar.lpVtbl.Release, uintptr(unsafe.Pointer(taskbar))) + CoUninitialize() + return nil, syscall.Errno(r) + } + + return taskbar, nil +} + +// SetOverlayIcon sets an overlay icon on the taskbar +func (t *ITaskbarList3) SetOverlayIcon(hwnd HWND, hIcon HICON, description *uint16) error { + ret, _, _ := syscall.SyscallN( + t.lpVtbl.SetOverlayIcon, + uintptr(unsafe.Pointer(t)), + uintptr(hwnd), + uintptr(hIcon), + uintptr(unsafe.Pointer(description)), + ) + if ret != 0 { + return syscall.Errno(ret) + } + return nil +} + +// Release releases the ITaskbarList3 interface +func (t *ITaskbarList3) Release() { + if t != nil { + syscall.SyscallN(t.lpVtbl.Release, uintptr(unsafe.Pointer(t))) + CoUninitialize() + } +} diff --git a/v3/pkg/w32/theme.go b/v3/pkg/w32/theme.go index 40ebb69bd..0e8a8ef17 100644 --- a/v3/pkg/w32/theme.go +++ b/v3/pkg/w32/theme.go @@ -3,8 +3,11 @@ package w32 import ( + "fmt" + "syscall" "unsafe" + "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) @@ -20,6 +23,142 @@ const DwmwaSystemBackdropType DWMWINDOWATTRIBUTE = 38 const SPI_GETHIGHCONTRAST = 0x0042 const HCF_HIGHCONTRASTON = 0x00000001 +type WINDOWCOMPOSITIONATTRIB DWORD + +type HTHEME HANDLE + +const ( + WCA_UNDEFINED WINDOWCOMPOSITIONATTRIB = 0 + WCA_NCRENDERING_ENABLED WINDOWCOMPOSITIONATTRIB = 1 + WCA_NCRENDERING_POLICY WINDOWCOMPOSITIONATTRIB = 2 + WCA_TRANSITIONS_FORCEDISABLED WINDOWCOMPOSITIONATTRIB = 3 + WCA_ALLOW_NCPAINT WINDOWCOMPOSITIONATTRIB = 4 + WCA_CAPTION_BUTTON_BOUNDS WINDOWCOMPOSITIONATTRIB = 5 + WCA_NONCLIENT_RTL_LAYOUT WINDOWCOMPOSITIONATTRIB = 6 + WCA_FORCE_ICONIC_REPRESENTATION WINDOWCOMPOSITIONATTRIB = 7 + WCA_EXTENDED_FRAME_BOUNDS WINDOWCOMPOSITIONATTRIB = 8 + WCA_HAS_ICONIC_BITMAP WINDOWCOMPOSITIONATTRIB = 9 + WCA_THEME_ATTRIBUTES WINDOWCOMPOSITIONATTRIB = 10 + WCA_NCRENDERING_EXILED WINDOWCOMPOSITIONATTRIB = 11 + WCA_NCADORNMENTINFO WINDOWCOMPOSITIONATTRIB = 12 + WCA_EXCLUDED_FROM_LIVEPREVIEW WINDOWCOMPOSITIONATTRIB = 13 + WCA_VIDEO_OVERLAY_ACTIVE WINDOWCOMPOSITIONATTRIB = 14 + WCA_FORCE_ACTIVEWINDOW_APPEARANCE WINDOWCOMPOSITIONATTRIB = 15 + WCA_DISALLOW_PEEK WINDOWCOMPOSITIONATTRIB = 16 + WCA_CLOAK WINDOWCOMPOSITIONATTRIB = 17 + WCA_CLOAKED WINDOWCOMPOSITIONATTRIB = 18 + WCA_ACCENT_POLICY WINDOWCOMPOSITIONATTRIB = 19 + WCA_FREEZE_REPRESENTATION WINDOWCOMPOSITIONATTRIB = 20 + WCA_EVER_UNCLOAKED WINDOWCOMPOSITIONATTRIB = 21 + WCA_VISUAL_OWNER WINDOWCOMPOSITIONATTRIB = 22 + WCA_HOLOGRAPHIC WINDOWCOMPOSITIONATTRIB = 23 + WCA_EXCLUDED_FROM_DDA WINDOWCOMPOSITIONATTRIB = 24 + WCA_PASSIVEUPDATEMODE WINDOWCOMPOSITIONATTRIB = 25 + WCA_USEDARKMODECOLORS WINDOWCOMPOSITIONATTRIB = 26 + WCA_CORNER_STYLE WINDOWCOMPOSITIONATTRIB = 27 + WCA_PART_COLOR WINDOWCOMPOSITIONATTRIB = 28 + WCA_DISABLE_MOVESIZE_FEEDBACK WINDOWCOMPOSITIONATTRIB = 29 + WCA_LAST WINDOWCOMPOSITIONATTRIB = 30 +) + +type WINDOWCOMPOSITIONATTRIBDATA struct { + Attrib WINDOWCOMPOSITIONATTRIB + PvData unsafe.Pointer + CbData uintptr +} + +var ( + uxtheme = syscall.NewLazyDLL("uxtheme.dll") + procSetWindowTheme = uxtheme.NewProc("SetWindowTheme") + procOpenThemeData = uxtheme.NewProc("OpenThemeData") + procCloseThemeData = uxtheme.NewProc("CloseThemeData") + procDrawThemeBackground = uxtheme.NewProc("DrawThemeBackground") + procAllowDarkModeForApplication = uxtheme.NewProc("AllowDarkModeForApp") + procDrawThemeTextEx = uxtheme.NewProc("DrawThemeTextEx") +) + +type PreferredAppMode = int32 + +const ( + PreferredAppModeDefault PreferredAppMode = iota + PreferredAppModeAllowDark + PreferredAppModeForceDark + PreferredAppModeForceLight + PreferredAppModeMax +) + +var ( + AllowDarkModeForWindow func(hwnd HWND, allow bool) uintptr + SetPreferredAppMode func(mode int32) uintptr + FlushMenuThemes func() + RefreshImmersiveColorPolicyState func() + ShouldAppsUseDarkMode func() bool +) + +func init() { + if IsWindowsVersionAtLeast(10, 0, 18334) { + // AllowDarkModeForWindow is only available on Windows 10+ + localUXTheme, err := windows.LoadLibrary("uxtheme.dll") + if err == nil { + procAllowDarkModeForWindow, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(133)) + if err == nil { + AllowDarkModeForWindow = func(hwnd HWND, allow bool) uintptr { + var allowInt int32 + if allow { + allowInt = 1 + } + ret, _, _ := syscall.SyscallN(procAllowDarkModeForWindow, uintptr(allowInt)) + return ret + } + } + + // Add ShouldAppsUseDarkMode + procShouldAppsUseDarkMode, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(132)) + if err == nil { + ShouldAppsUseDarkMode = func() bool { + ret, _, _ := syscall.SyscallN(procShouldAppsUseDarkMode) + return ret != 0 + } + } + + // SetPreferredAppMode is only available on Windows 10+ + procSetPreferredAppMode, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(135)) + if err == nil { + SetPreferredAppMode = func(mode int32) uintptr { + ret, _, _ := syscall.SyscallN(procSetPreferredAppMode, uintptr(mode)) + return ret + } + } + + // Add FlushMenuThemes + procFlushMenuThemesAddr, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(136)) + if err == nil { + FlushMenuThemes = func() { + syscall.SyscallN(procFlushMenuThemesAddr) + } + } + + // Add RefreshImmersiveColorPolicyState + procRefreshImmersiveColorPolicyStateAddr, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(104)) + if err == nil { + RefreshImmersiveColorPolicyState = func() { + syscall.SyscallN(procRefreshImmersiveColorPolicyStateAddr) + } + } + + // Initialize dark mode + if SetPreferredAppMode != nil { + SetPreferredAppMode(PreferredAppModeAllowDark) + if RefreshImmersiveColorPolicyState != nil { + RefreshImmersiveColorPolicyState() + } + } + + windows.FreeLibrary(localUXTheme) + } + } +} + func dwmSetWindowAttribute(hwnd uintptr, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) { ret, _, err := procDwmSetWindowAttribute.Call( hwnd, @@ -49,6 +188,42 @@ func SupportsImmersiveDarkMode() bool { return IsWindowsVersionAtLeast(10, 0, 18985) } +func SetMenuTheme(hwnd uintptr, useDarkMode bool) { + if !SupportsThemes() { + return + } + + // Check if dark mode is supported and enabled + if useDarkMode && ShouldAppsUseDarkMode != nil && !ShouldAppsUseDarkMode() { + useDarkMode = false + } + + // Set the window theme + themeName := "Explorer" + if useDarkMode { + themeName = "DarkMode_Explorer" + } + procSetWindowTheme.Call(HWND(hwnd), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(themeName))), 0) + + // Update the theme state + if RefreshImmersiveColorPolicyState != nil { + RefreshImmersiveColorPolicyState() + } + + // Flush menu themes to force a refresh + if FlushMenuThemes != nil { + FlushMenuThemes() + } + + // Set dark mode for the window + if AllowDarkModeForWindow != nil { + AllowDarkModeForWindow(HWND(hwnd), useDarkMode) + } + + // Force a redraw + InvalidateRect(HWND(hwnd), nil, true) +} + func SetTheme(hwnd uintptr, useDarkMode bool) { if SupportsThemes() { attr := DwmwaUseImmersiveDarkModeBefore20h1 @@ -60,22 +235,25 @@ func SetTheme(hwnd uintptr, useDarkMode bool) { winDark = 1 } dwmSetWindowAttribute(hwnd, attr, unsafe.Pointer(&winDark), unsafe.Sizeof(winDark)) + SetMenuTheme(hwnd, useDarkMode) } } -func EnableTranslucency(hwnd uintptr, backdrop int32) { +func EnableTranslucency(hwnd uintptr, backdrop uint32) { dwmSetWindowAttribute(hwnd, DwmwaSystemBackdropType, unsafe.Pointer(&backdrop), unsafe.Sizeof(backdrop)) } -func SetTitleBarColour(hwnd uintptr, titleBarColour int32) { +func SetTitleBarColour(hwnd uintptr, titleBarColour uint32) { + // Debug: Print the color value being set + // fmt.Printf("Setting titlebar color to: 0x%08X (RGB: %d, %d, %d)\n", titleBarColour, titleBarColour&0xFF, (titleBarColour>>8)&0xFF, (titleBarColour>>16)&0xFF) dwmSetWindowAttribute(hwnd, DwmwaCaptionColor, unsafe.Pointer(&titleBarColour), unsafe.Sizeof(titleBarColour)) } -func SetTitleTextColour(hwnd uintptr, titleTextColour int32) { +func SetTitleTextColour(hwnd uintptr, titleTextColour uint32) { dwmSetWindowAttribute(hwnd, DwmwaTextColor, unsafe.Pointer(&titleTextColour), unsafe.Sizeof(titleTextColour)) } -func SetBorderColour(hwnd uintptr, titleBorderColour int32) { +func SetBorderColour(hwnd uintptr, titleBorderColour uint32) { dwmSetWindowAttribute(hwnd, DwmwaBorderColor, unsafe.Pointer(&titleBorderColour), unsafe.Sizeof(titleBorderColour)) } @@ -110,3 +288,58 @@ func IsCurrentlyHighContrastMode() bool { r := result.DwFlags&HCF_HIGHCONTRASTON == HCF_HIGHCONTRASTON return r } + +func GetAccentColor() (string, error) { + key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\DWM`, registry.QUERY_VALUE) + if err != nil { + return "", err + } + defer key.Close() + + accentColor, _, err := key.GetIntegerValue("AccentColor") + if err != nil { + return "", err + } + + // Extract RGB components from ABGR format (Alpha, Blue, Green, Red) + red := uint8(accentColor & 0xFF) + green := uint8((accentColor >> 8) & 0xFF) + blue := uint8((accentColor >> 16) & 0xFF) + + return fmt.Sprintf("rgb(%d,%d,%d)", red, green, blue), nil +} + +// OpenThemeData opens theme data for a window and its class +func OpenThemeData(hwnd HWND, pszClassList string) HTHEME { + ret, _, _ := procOpenThemeData.Call( + uintptr(hwnd), + uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(pszClassList)))) + return HTHEME(ret) +} + +// CloseThemeData closes theme data handle +func CloseThemeData(hTheme HTHEME) error { + ret, _, err := procCloseThemeData.Call(uintptr(hTheme)) + if ret != 0 { + return err + } + return nil +} + +// DrawThemeTextEx draws theme text with extended options +func DrawThemeTextEx(hTheme HTHEME, hdc HDC, iPartId int32, iStateId int32, pszText []uint16, cchText int32, dwTextFlags uint32, pRect *RECT, pOptions *DTTOPTS) error { + ret, _, err := procDrawThemeTextEx.Call( + uintptr(hTheme), + uintptr(hdc), + uintptr(iPartId), + uintptr(iStateId), + uintptr(unsafe.Pointer(&pszText[0])), + uintptr(cchText), + uintptr(dwTextFlags), + uintptr(unsafe.Pointer(pRect)), + uintptr(unsafe.Pointer(pOptions))) + if ret != 0 { + return err + } + return nil +} diff --git a/v3/pkg/w32/timer.go b/v3/pkg/w32/timer.go new file mode 100644 index 000000000..c95253e53 --- /dev/null +++ b/v3/pkg/w32/timer.go @@ -0,0 +1,19 @@ +//go:build windows + +package w32 + +func SetTimer(hwnd HWND, nIDEvent uintptr, uElapse uint32, lpTimerFunc uintptr) uintptr { + ret, _, _ := procSetTimer.Call( + uintptr(hwnd), + nIDEvent, + uintptr(uElapse), + lpTimerFunc) + return ret +} + +func KillTimer(hwnd HWND, nIDEvent uintptr) bool { + ret, _, _ := procKillTimer.Call( + uintptr(hwnd), + nIDEvent) + return ret != 0 +} diff --git a/v3/pkg/w32/typedef.go b/v3/pkg/w32/typedef.go index bbd6aa9d2..cf1c13c09 100644 --- a/v3/pkg/w32/typedef.go +++ b/v3/pkg/w32/typedef.go @@ -241,6 +241,13 @@ func (r *RECT) String() string { return fmt.Sprintf("RECT (%p): Left: %d, Top: %d, Right: %d, Bottom: %d", r, r.Left, r.Top, r.Right, r.Bottom) } +func RectToPoints(rect *RECT) []POINT { + return []POINT{ + {X: rect.Left, Y: rect.Top}, // Top-left + {X: rect.Right, Y: rect.Bottom}, // Bottom-right + } +} + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms633577.aspx type WNDCLASSEX struct { Size uint32 @@ -780,48 +787,6 @@ type ACCENT_POLICY struct { AnimationId DWORD } -type WINDOWCOMPOSITIONATTRIBDATA struct { - Attrib WINDOWCOMPOSITIONATTRIB - PvData PVOID - CbData SIZE_T -} - -type WINDOWCOMPOSITIONATTRIB DWORD - -const ( - WCA_UNDEFINED WINDOWCOMPOSITIONATTRIB = 0 - WCA_NCRENDERING_ENABLED WINDOWCOMPOSITIONATTRIB = 1 - WCA_NCRENDERING_POLICY WINDOWCOMPOSITIONATTRIB = 2 - WCA_TRANSITIONS_FORCEDISABLED WINDOWCOMPOSITIONATTRIB = 3 - WCA_ALLOW_NCPAINT WINDOWCOMPOSITIONATTRIB = 4 - WCA_CAPTION_BUTTON_BOUNDS WINDOWCOMPOSITIONATTRIB = 5 - WCA_NONCLIENT_RTL_LAYOUT WINDOWCOMPOSITIONATTRIB = 6 - WCA_FORCE_ICONIC_REPRESENTATION WINDOWCOMPOSITIONATTRIB = 7 - WCA_EXTENDED_FRAME_BOUNDS WINDOWCOMPOSITIONATTRIB = 8 - WCA_HAS_ICONIC_BITMAP WINDOWCOMPOSITIONATTRIB = 9 - WCA_THEME_ATTRIBUTES WINDOWCOMPOSITIONATTRIB = 10 - WCA_NCRENDERING_EXILED WINDOWCOMPOSITIONATTRIB = 11 - WCA_NCADORNMENTINFO WINDOWCOMPOSITIONATTRIB = 12 - WCA_EXCLUDED_FROM_LIVEPREVIEW WINDOWCOMPOSITIONATTRIB = 13 - WCA_VIDEO_OVERLAY_ACTIVE WINDOWCOMPOSITIONATTRIB = 14 - WCA_FORCE_ACTIVEWINDOW_APPEARANCE WINDOWCOMPOSITIONATTRIB = 15 - WCA_DISALLOW_PEEK WINDOWCOMPOSITIONATTRIB = 16 - WCA_CLOAK WINDOWCOMPOSITIONATTRIB = 17 - WCA_CLOAKED WINDOWCOMPOSITIONATTRIB = 18 - WCA_ACCENT_POLICY WINDOWCOMPOSITIONATTRIB = 19 - WCA_FREEZE_REPRESENTATION WINDOWCOMPOSITIONATTRIB = 20 - WCA_EVER_UNCLOAKED WINDOWCOMPOSITIONATTRIB = 21 - WCA_VISUAL_OWNER WINDOWCOMPOSITIONATTRIB = 22 - WCA_HOLOGRAPHIC WINDOWCOMPOSITIONATTRIB = 23 - WCA_EXCLUDED_FROM_DDA WINDOWCOMPOSITIONATTRIB = 24 - WCA_PASSIVEUPDATEMODE WINDOWCOMPOSITIONATTRIB = 25 - WCA_USEDARKMODECOLORS WINDOWCOMPOSITIONATTRIB = 26 - WCA_CORNER_STYLE WINDOWCOMPOSITIONATTRIB = 27 - WCA_PART_COLOR WINDOWCOMPOSITIONATTRIB = 28 - WCA_DISABLE_MOVESIZE_FEEDBACK WINDOWCOMPOSITIONATTRIB = 29 - WCA_LAST WINDOWCOMPOSITIONATTRIB = 30 -) - // ------------------------- // http://msdn.microsoft.com/en-us/library/windows/desktop/ms684225.aspx diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index 96701dffb..8988b786e 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -26,6 +26,7 @@ var ( procShowWindow = moduser32.NewProc("ShowWindow") procGetDesktopWindow = moduser32.NewProc("GetDesktopWindow") procShowWindowAsync = moduser32.NewProc("ShowWindowAsync") + procIsZoomed = moduser32.NewProc("IsZoomed") procUpdateWindow = moduser32.NewProc("UpdateWindow") procCreateWindowEx = moduser32.NewProc("CreateWindowExW") procAdjustWindowRect = moduser32.NewProc("AdjustWindowRect") @@ -106,6 +107,7 @@ var ( procTranslateAccelerator = moduser32.NewProc("TranslateAcceleratorW") procSetWindowPos = moduser32.NewProc("SetWindowPos") procFillRect = moduser32.NewProc("FillRect") + procFrameRect = moduser32.NewProc("FrameRect") procDrawText = moduser32.NewProc("DrawTextW") procAddClipboardFormatListener = moduser32.NewProc("AddClipboardFormatListener") procRemoveClipboardFormatListener = moduser32.NewProc("RemoveClipboardFormatListener") @@ -145,6 +147,8 @@ var ( procEnumWindows = moduser32.NewProc("EnumWindows") procEnumChildWindows = moduser32.NewProc("EnumChildWindows") procChangeDisplaySettingsEx = moduser32.NewProc("ChangeDisplaySettingsExW") + procSetTimer = moduser32.NewProc("SetTimer") + procKillTimer = moduser32.NewProc("KillTimer") procSendInput = moduser32.NewProc("SendInput") procSetWindowsHookEx = moduser32.NewProc("SetWindowsHookExW") procUnhookWindowsHookEx = moduser32.NewProc("UnhookWindowsHookEx") @@ -162,12 +166,17 @@ var ( procAppendMenu = moduser32.NewProc("AppendMenuW") procSetMenuItemInfo = moduser32.NewProc("SetMenuItemInfoW") procDrawMenuBar = moduser32.NewProc("DrawMenuBar") + procTrackPopupMenu = moduser32.NewProc("TrackPopupMenu") procTrackPopupMenuEx = moduser32.NewProc("TrackPopupMenuEx") + procGetMenuBarInfo = moduser32.NewProc("GetMenuBarInfo") + procMapWindowPoints = moduser32.NewProc("MapWindowPoints") procGetKeyState = moduser32.NewProc("GetKeyState") procGetSysColorBrush = moduser32.NewProc("GetSysColorBrush") + procGetSysColor = moduser32.NewProc("GetSysColor") procGetWindowPlacement = moduser32.NewProc("GetWindowPlacement") procSetWindowPlacement = moduser32.NewProc("SetWindowPlacement") + procGetWindowDC = moduser32.NewProc("GetWindowDC") procGetScrollInfo = moduser32.NewProc("GetScrollInfo") procSetScrollInfo = moduser32.NewProc("SetScrollInfo") @@ -178,6 +187,8 @@ var ( procRedrawWindow = moduser32.NewProc("RedrawWindow") + procRegisterWindowMessageW = moduser32.NewProc("RegisterWindowMessageW") + mainThread HANDLE ) @@ -186,6 +197,11 @@ func init() { mainThread = GetCurrentThreadId() } +func GetWindowDC(hwnd HWND) HDC { + ret, _, _ := procGetWindowDC.Call(hwnd) + return ret +} + func GET_X_LPARAM(lp uintptr) int32 { return int32(int16(LOWORD(uint32(lp)))) } @@ -265,6 +281,11 @@ func ShowWindow(hwnd HWND, cmdshow int) bool { return ret != 0 } +func IsZoomed(hwnd HWND) bool { + ret, _, _ := procIsZoomed.Call(uintptr(hwnd)) + return ret != 0 +} + func ShowWindowAsync(hwnd HWND, cmdshow int) bool { ret, _, _ := procShowWindowAsync.Call( uintptr(hwnd), @@ -299,9 +320,7 @@ func PostThreadMessage(threadID HANDLE, msg int, wp, lp uintptr) { } func RegisterWindowMessage(name *uint16) uint32 { - ret, _, _ := procRegisterWindowMessageA.Call( - uintptr(unsafe.Pointer(name))) - + ret, _, _ := procRegisterWindowMessageW.Call(uintptr(unsafe.Pointer(name))) return uint32(ret) } @@ -754,6 +773,13 @@ func GetSysColorBrush(nIndex int) HBRUSH { return HBRUSH(ret) } +func GetSysColor(nIndex int) COLORREF { + ret, _, _ := procGetSysColor.Call( + uintptr(nIndex)) + + return COLORREF(ret) +} + func CopyRect(dst, src *RECT) bool { ret, _, _ := procCopyRect.Call( uintptr(unsafe.Pointer(dst)), @@ -1057,10 +1083,14 @@ func FillRect(hDC HDC, lprc *RECT, hbr HBRUSH) bool { return ret != 0 } -func DrawText(hDC HDC, text string, uCount int, lpRect *RECT, uFormat uint) int { +func DrawText(hDC HDC, text []uint16, uCount int, lpRect *RECT, uFormat uint32) int { + + // Convert the string to a UTF16 pointer + // This is necessary because the DrawText function expects a UTF16 pointer + ret, _, _ := procDrawText.Call( uintptr(hDC), - uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))), + uintptr(unsafe.Pointer(&text[0])), uintptr(uCount), uintptr(unsafe.Pointer(lpRect)), uintptr(uFormat)) @@ -1424,3 +1454,41 @@ func RedrawWindow(hwnd HWND, lprcUpdate *RECT, hrgnUpdate HRGN, flags uint32) bo uintptr(flags)) return ret != 0 } + +func FrameRect(hDC HDC, lprc *RECT, hbr HBRUSH) int { + ret, _, _ := procFrameRect.Call( + uintptr(hDC), + uintptr(unsafe.Pointer(lprc)), + uintptr(hbr)) + return int(ret) +} + +func GetMenuBarInfo(hwnd HWND, idObject int32, idItem uint32, pmbi *MENUBARINFO) bool { + ret, _, _ := procGetMenuBarInfo.Call( + uintptr(hwnd), + uintptr(idObject), + uintptr(idItem), + uintptr(unsafe.Pointer(pmbi))) + return ret != 0 +} + +func MapWindowPoints(hwndFrom, hwndTo HWND, lpPoints *POINT, cPoints uint) int { + ret, _, _ := procMapWindowPoints.Call( + uintptr(hwndFrom), + uintptr(hwndTo), + uintptr(unsafe.Pointer(lpPoints)), + uintptr(cPoints)) + return int(ret) +} + +func TrackPopupMenu(hmenu HMENU, flags uint32, x, y int32, reserved int32, hwnd HWND, prcRect *RECT) bool { + ret, _, _ := procTrackPopupMenu.Call( + uintptr(hmenu), + uintptr(flags), + uintptr(x), + uintptr(y), + uintptr(reserved), + uintptr(hwnd), + uintptr(unsafe.Pointer(prcRect))) + return ret != 0 +} diff --git a/v3/pkg/w32/uxtheme.go b/v3/pkg/w32/uxtheme.go deleted file mode 100644 index ed80d487f..000000000 --- a/v3/pkg/w32/uxtheme.go +++ /dev/null @@ -1,152 +0,0 @@ -//go:build windows - -/* - * Copyright (C) 2019 Tad Vizbaras. All Rights Reserved. - * Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved. - */ - -package w32 - -import ( - "syscall" - "unsafe" -) - -// LISTVIEW parts -const ( - LVP_LISTITEM = 1 - LVP_LISTGROUP = 2 - LVP_LISTDETAIL = 3 - LVP_LISTSORTEDDETAIL = 4 - LVP_EMPTYTEXT = 5 - LVP_GROUPHEADER = 6 - LVP_GROUPHEADERLINE = 7 - LVP_EXPANDBUTTON = 8 - LVP_COLLAPSEBUTTON = 9 - LVP_COLUMNDETAIL = 10 -) - -// LVP_LISTITEM states -const ( - LISS_NORMAL = 1 - LISS_HOT = 2 - LISS_SELECTED = 3 - LISS_DISABLED = 4 - LISS_SELECTEDNOTFOCUS = 5 - LISS_HOTSELECTED = 6 -) - -// TREEVIEW parts -const ( - TVP_TREEITEM = 1 - TVP_GLYPH = 2 - TVP_BRANCH = 3 - TVP_HOTGLYPH = 4 -) - -// TVP_TREEITEM states -const ( - TREIS_NORMAL = 1 - TREIS_HOT = 2 - TREIS_SELECTED = 3 - TREIS_DISABLED = 4 - TREIS_SELECTEDNOTFOCUS = 5 - TREIS_HOTSELECTED = 6 -) - -type HTHEME HANDLE - -var ( - // Library - libuxtheme uintptr - - // Functions - closeThemeData uintptr - drawThemeBackground uintptr - drawThemeText uintptr - getThemeTextExtent uintptr - openThemeData uintptr - setWindowTheme uintptr -) - -func init() { - // Library - libuxtheme = MustLoadLibrary("uxtheme.dll") - - // Functions - closeThemeData = MustGetProcAddress(libuxtheme, "CloseThemeData") - drawThemeBackground = MustGetProcAddress(libuxtheme, "DrawThemeBackground") - drawThemeText = MustGetProcAddress(libuxtheme, "DrawThemeText") - getThemeTextExtent = MustGetProcAddress(libuxtheme, "GetThemeTextExtent") - openThemeData = MustGetProcAddress(libuxtheme, "OpenThemeData") - setWindowTheme = MustGetProcAddress(libuxtheme, "SetWindowTheme") -} - -func CloseThemeData(hTheme HTHEME) HRESULT { - ret, _, _ := syscall.SyscallN(closeThemeData, - uintptr(hTheme), - 0, - 0) - - return HRESULT(ret) -} - -func DrawThemeBackground(hTheme HTHEME, hdc HDC, iPartId, iStateId int32, pRect, pClipRect *RECT) HRESULT { - ret, _, _ := syscall.Syscall6(drawThemeBackground, 6, - uintptr(hTheme), - uintptr(hdc), - uintptr(iPartId), - uintptr(iStateId), - uintptr(unsafe.Pointer(pRect)), - uintptr(unsafe.Pointer(pClipRect))) - - return HRESULT(ret) -} - -func DrawThemeText(hTheme HTHEME, hdc HDC, iPartId, iStateId int32, pszText *uint16, iCharCount int32, dwTextFlags, dwTextFlags2 uint32, pRect *RECT) HRESULT { - ret, _, _ := syscall.Syscall9(drawThemeText, 9, - uintptr(hTheme), - uintptr(hdc), - uintptr(iPartId), - uintptr(iStateId), - uintptr(unsafe.Pointer(pszText)), - uintptr(iCharCount), - uintptr(dwTextFlags), - uintptr(dwTextFlags2), - uintptr(unsafe.Pointer(pRect))) - - return HRESULT(ret) -} - -func GetThemeTextExtent(hTheme HTHEME, hdc HDC, iPartId, iStateId int32, pszText *uint16, iCharCount int32, dwTextFlags uint32, pBoundingRect, pExtentRect *RECT) HRESULT { - ret, _, _ := syscall.Syscall9(getThemeTextExtent, 9, - uintptr(hTheme), - uintptr(hdc), - uintptr(iPartId), - uintptr(iStateId), - uintptr(unsafe.Pointer(pszText)), - uintptr(iCharCount), - uintptr(dwTextFlags), - uintptr(unsafe.Pointer(pBoundingRect)), - uintptr(unsafe.Pointer(pExtentRect))) - - return HRESULT(ret) -} - -func OpenThemeData(hwnd HWND, pszClassList *uint16) HTHEME { - ret, _, _ := syscall.SyscallN(openThemeData, - uintptr(hwnd), - uintptr(unsafe.Pointer(pszClassList)), - 0) - - return HTHEME(ret) -} - -func SetWindowTheme(hwnd HWND, pszSubAppName, pszSubIdList *uint16) HRESULT { - ret, _, _ := syscall.SyscallN(setWindowTheme, - uintptr(hwnd), - uintptr(unsafe.Pointer(pszSubAppName)), - uintptr(unsafe.Pointer(pszSubIdList))) - - return HRESULT(ret) -} diff --git a/v3/release-notes.txt b/v3/release-notes.txt new file mode 100644 index 000000000..156e62081 --- /dev/null +++ b/v3/release-notes.txt @@ -0,0 +1,2 @@ +## Fixed +- Fixed notification parsing on Windows @popaprozac in [PR](https://github.com/wailsapp/wails/pull/4450) \ No newline at end of file diff --git a/v3/release_notes.md b/v3/release_notes.md new file mode 100644 index 000000000..156e62081 --- /dev/null +++ b/v3/release_notes.md @@ -0,0 +1,2 @@ +## Fixed +- Fixed notification parsing on Windows @popaprozac in [PR](https://github.com/wailsapp/wails/pull/4450) \ No newline at end of file diff --git a/v3/scripts/validate-changelog.go b/v3/scripts/validate-changelog.go new file mode 100644 index 000000000..659285a20 --- /dev/null +++ b/v3/scripts/validate-changelog.go @@ -0,0 +1,270 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" +) + +func main() { + if len(os.Args) < 3 { + fmt.Println("Usage: go run validate-changelog.go ") + os.Exit(1) + } + + changelogPath := os.Args[1] + addedLinesPath := os.Args[2] + + // Read changelog + content, err := readFile(changelogPath) + if err != nil { + fmt.Printf("ERROR: Failed to read changelog: %v\n", err) + os.Exit(1) + } + + // Read the lines added in this PR + addedContent, err := readFile(addedLinesPath) + if err != nil { + fmt.Printf("ERROR: Failed to read PR added lines: %v\n", err) + os.Exit(1) + } + + addedLines := strings.Split(addedContent, "\n") + fmt.Printf("πŸ“ Lines added in this PR: %d\n", len(addedLines)) + + // Parse changelog to find where added lines ended up + lines := strings.Split(content, "\n") + + // Find problematic entries - only check lines that were ADDED in this PR + var issues []Issue + currentSection := "" + + for lineNum, line := range lines { + // Track current section + if strings.HasPrefix(line, "## ") { + if strings.Contains(line, "[Unreleased]") { + currentSection = "Unreleased" + } else if strings.Contains(line, "v3.0.0-alpha") { + // Extract version from line like "## v3.0.0-alpha.10 - 2025-07-06" + parts := strings.Split(strings.TrimSpace(line[3:]), " - ") + if len(parts) >= 1 { + currentSection = strings.TrimSpace(parts[0]) + } + } + } + + // Check if this line was added in this PR AND is in a released version + if currentSection != "" && currentSection != "Unreleased" && + strings.HasPrefix(strings.TrimSpace(line), "- ") && + wasAddedInThisPR(line, addedLines) { + + issues = append(issues, Issue{ + Line: lineNum, + Content: strings.TrimSpace(line), + Section: currentSection, + Category: getCurrentCategory(lines, lineNum), + }) + fmt.Printf("🚨 MISPLACED: Line added to released version %s: %s\n", currentSection, strings.TrimSpace(line)) + } + } + + if len(issues) == 0 { + fmt.Println("VALIDATION_RESULT=success") + fmt.Println("No misplaced changelog entries found βœ…") + return + } + + // Try to fix the issues + fmt.Printf("Found %d potentially misplaced entries:\n", len(issues)) + for _, issue := range issues { + fmt.Printf(" - Line %d in %s: %s\n", issue.Line+1, issue.Section, issue.Content) + } + + // Attempt automatic fix + fixed, err := attemptFix(content, issues, changelogPath) + if err != nil { + fmt.Printf("VALIDATION_RESULT=error\n") + fmt.Printf("ERROR: Failed to fix changelog: %v\n", err) + os.Exit(1) + } + + if fixed { + fmt.Println("VALIDATION_RESULT=fixed") + fmt.Println("βœ… Changelog has been automatically fixed") + } else { + fmt.Println("VALIDATION_RESULT=cannot_fix") + fmt.Println("❌ Cannot automatically fix changelog issues") + os.Exit(1) + } +} + +type Issue struct { + Line int + Content string + Section string + Category string +} + +func wasAddedInThisPR(line string, addedLines []string) bool { + trimmedLine := strings.TrimSpace(line) + for _, addedLine := range addedLines { + trimmedAdded := strings.TrimSpace(addedLine) + if trimmedAdded == trimmedLine { + return true + } + if strings.Contains(trimmedAdded, trimmedLine) && len(trimmedAdded) > 0 { + return true + } + } + return false +} + +func getCurrentCategory(lines []string, lineNum int) string { + for i := lineNum - 1; i >= 0; i-- { + line := strings.TrimSpace(lines[i]) + if strings.HasPrefix(line, "### ") { + return strings.TrimSpace(line[4:]) + } + if strings.HasPrefix(line, "## ") && + !strings.Contains(line, "[Unreleased]") && + !strings.Contains(line, "v3.0.0-alpha") { + return strings.TrimSpace(line[3:]) + } + if strings.HasPrefix(line, "## ") && + (strings.Contains(line, "[Unreleased]") || strings.Contains(line, "v3.0.0-alpha")) { + break + } + } + return "Added" +} + +func attemptFix(content string, issues []Issue, outputPath string) (bool, error) { + lines := strings.Split(content, "\n") + + // Find unreleased section + unreleasedStart := -1 + unreleasedEnd := -1 + + for i, line := range lines { + if strings.Contains(line, "[Unreleased]") { + unreleasedStart = i + for j := i + 1; j < len(lines); j++ { + if strings.HasPrefix(lines[j], "## ") && !strings.Contains(lines[j], "[Unreleased]") { + unreleasedEnd = j + break + } + } + break + } + } + + if unreleasedStart == -1 { + return false, fmt.Errorf("Could not find [Unreleased] section") + } + + // Group issues by category + issuesByCategory := make(map[string][]Issue) + for _, issue := range issues { + issuesByCategory[issue.Category] = append(issuesByCategory[issue.Category], issue) + } + + // Remove issues from original locations (in reverse order) + var linesToRemove []int + for _, issue := range issues { + linesToRemove = append(linesToRemove, issue.Line) + } + + // Sort in reverse order + for i := 0; i < len(linesToRemove); i++ { + for j := i + 1; j < len(linesToRemove); j++ { + if linesToRemove[i] < linesToRemove[j] { + linesToRemove[i], linesToRemove[j] = linesToRemove[j], linesToRemove[i] + } + } + } + + // Remove lines + for _, lineNum := range linesToRemove { + lines = append(lines[:lineNum], lines[lineNum+1:]...) + } + + // Add entries to unreleased section + for category, categoryIssues := range issuesByCategory { + categoryFound := false + insertPos := unreleasedStart + 1 + + for i := unreleasedStart + 1; i < unreleasedEnd && i < len(lines); i++ { + if strings.Contains(lines[i], "### "+category) || strings.Contains(lines[i], "## "+category) { + categoryFound = true + for j := i + 1; j < unreleasedEnd && j < len(lines); j++ { + if strings.HasPrefix(lines[j], "### ") || strings.HasPrefix(lines[j], "## ") { + insertPos = j + break + } + if j == len(lines)-1 || j == unreleasedEnd-1 { + insertPos = j + 1 + break + } + } + break + } + } + + if !categoryFound { + if unreleasedEnd > 0 { + insertPos = unreleasedEnd + } else { + insertPos = unreleasedStart + 1 + } + + newLines := []string{ + "", + "### " + category, + "", + } + lines = append(lines[:insertPos], append(newLines, lines[insertPos:]...)...) + insertPos += len(newLines) + unreleasedEnd += len(newLines) + } + + // Add entries to the category + for _, issue := range categoryIssues { + lines = append(lines[:insertPos], append([]string{issue.Content}, lines[insertPos:]...)...) + insertPos++ + unreleasedEnd++ + } + } + + // Write back to file + newContent := strings.Join(lines, "\n") + return true, writeFile(outputPath, newContent) +} + +func readFile(path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", err + } + defer file.Close() + + var content strings.Builder + scanner := bufio.NewScanner(file) + for scanner.Scan() { + content.WriteString(scanner.Text()) + content.WriteString("\n") + } + + return content.String(), scanner.Err() +} + +func writeFile(path, content string) error { + dir := filepath.Dir(path) + err := os.MkdirAll(dir, 0755) + if err != nil { + return err + } + + return os.WriteFile(path, []byte(content), 0644) +} \ No newline at end of file diff --git a/v3/tasks/cleanup/cleanup.go b/v3/tasks/cleanup/cleanup.go new file mode 100644 index 000000000..f06813abf --- /dev/null +++ b/v3/tasks/cleanup/cleanup.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// CleanupPattern represents a cleanup rule +type CleanupPattern struct { + Type string // "prefix", "suffix", "exact" + Pattern string // The pattern to match + TargetFiles bool // true = target files, false = target directories + Description string // Description for logging +} + +// Patterns to clean up during test cleanup +var cleanupPatterns = []CleanupPattern{ + // Test binaries from examples + {Type: "prefix", Pattern: "testbuild-", TargetFiles: true, Description: "test binary"}, + + // Go test binaries + {Type: "suffix", Pattern: ".test", TargetFiles: true, Description: "Go test binary"}, + + // Package artifacts from packaging tests (only in internal/commands directory) + // Note: Only clean these from the commands directory, not from test temp directories + + // Test template directories from template tests + {Type: "prefix", Pattern: "test-template-", TargetFiles: false, Description: "test template directory"}, + + // CLI test binaries (files named exactly "appimage_testfiles") + {Type: "exact", Pattern: "appimage_testfiles", TargetFiles: true, Description: "CLI test binary"}, +} + +func main() { + fmt.Println("Starting cleanup...") + cleanedCount := 0 + + // Walk through all files and directories + err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil // Continue on errors + } + + // Skip if we're in the .git directory + if strings.Contains(path, ".git") { + return nil + } + + name := info.Name() + + // Check each cleanup pattern + for _, pattern := range cleanupPatterns { + shouldClean := false + + switch pattern.Type { + case "prefix": + shouldClean = strings.HasPrefix(name, pattern.Pattern) + case "suffix": + shouldClean = strings.HasSuffix(name, pattern.Pattern) + case "exact": + shouldClean = name == pattern.Pattern + } + + if shouldClean { + // Check if the pattern targets the correct type (file or directory) + if pattern.TargetFiles && info.Mode().IsRegular() { + // This pattern targets files and this is a file + fmt.Printf("Removing %s: %s\n", pattern.Description, path) + os.Remove(path) + cleanedCount++ + break // Don't check other patterns for this file + } else if !pattern.TargetFiles && info.IsDir() { + // This pattern targets directories and this is a directory + fmt.Printf("Removing %s: %s\n", pattern.Description, path) + os.RemoveAll(path) + cleanedCount++ + return filepath.SkipDir // Don't recurse into removed directory + } + // If the pattern matches but the file type doesn't match TargetFiles, continue checking other patterns + } + } + + return nil + }) + + if err != nil { + fmt.Printf("Error during cleanup: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Cleanup completed. Removed %d items.\n", cleanedCount) +} \ No newline at end of file diff --git a/v3/tasks/events/generate.go b/v3/tasks/events/generate.go index e643db182..e9e3ec8d8 100644 --- a/v3/tasks/events/generate.go +++ b/v3/tasks/events/generate.go @@ -2,15 +2,12 @@ package main import ( "bytes" - "github.com/Masterminds/semver/v3" - "github.com/tidwall/gjson" - "github.com/tidwall/sjson" "os" "strconv" "strings" ) -var eventsGo = `package events +const eventsGo = `package events type ApplicationEventType uint type WindowEventType uint @@ -64,7 +61,7 @@ $$EVENTTOJS} ` -var darwinEventsH = `//go:build darwin +const darwinEventsH = `//go:build darwin #ifndef _events_h #define _events_h @@ -76,7 +73,7 @@ $$CHEADEREVENTS #endif` -var linuxEventsH = `//go:build linux +const linuxEventsH = `//go:build linux #ifndef _events_h #define _events_h @@ -88,34 +85,32 @@ $$CHEADEREVENTS #endif` -var eventsJS = ` -export const EventTypes = { - Windows: { -$$WINDOWSJSEVENTS }, - Mac: { -$$MACJSEVENTS }, - Linux: { -$$LINUXJSEVENTS }, - Common: { -$$COMMONJSEVENTS }, -}; -` +const eventsTS = `/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ ` + "`" + `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ -var eventsTS = ` -export declare const EventTypes: { - Windows: { -$$WINDOWSTSEVENTS }, - Mac: { -$$MACTSEVENTS }, - Linux: { -$$LINUXTSEVENTS }, - Common: { -$$COMMONTSEVENTS }, -}; +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Γ‚ MODIWL +// This file is automatically generated. DO NOT EDIT + +export const Types = Object.freeze({ + Windows: Object.freeze({ +$$WINDOWSJSEVENTS }), + Mac: Object.freeze({ +$$MACJSEVENTS }), + Linux: Object.freeze({ +$$LINUXJSEVENTS }), + Common: Object.freeze({ +$$COMMONJSEVENTS }), +}); ` func main() { - eventNames, err := os.ReadFile("../../pkg/events/events.txt") if err != nil { panic(err) @@ -138,11 +133,6 @@ func main() { commonEventsDecl := bytes.NewBufferString("") commonEventsValues := bytes.NewBufferString("") - linuxJSEvents := bytes.NewBufferString("") - macJSEvents := bytes.NewBufferString("") - windowsJSEvents := bytes.NewBufferString("") - commonJSEvents := bytes.NewBufferString("") - linuxTSEvents := bytes.NewBufferString("") macTSEvents := bytes.NewBufferString("") windowsTSEvents := bytes.NewBufferString("") @@ -197,9 +187,8 @@ func main() { } linuxEventsDecl.WriteString("\t" + eventTitle + " " + eventType + "\n") linuxEventsValues.WriteString("\t\t" + event + ": " + strconv.Itoa(id) + ",\n") - linuxJSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") - linuxTSEvents.WriteString("\t\t" + event + ": string,\n") - eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + strings.TrimSpace(string(line)) + "\",\n") + linuxTSEvents.WriteString("\t\t" + event + ": \"linux:" + event + "\",\n") + eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + event + "\",\n") maxLinuxEvents = id linuxCHeaderEvents.WriteString("#define Event" + eventTitle + " " + strconv.Itoa(id) + "\n") case "mac": @@ -212,10 +201,9 @@ func main() { } macEventsDecl.WriteString("\t" + eventTitle + " " + eventType + "\n") macEventsValues.WriteString("\t\t" + event + ": " + strconv.Itoa(id) + ",\n") - macJSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") - macTSEvents.WriteString("\t\t" + event + ": string,\n") + macTSEvents.WriteString("\t\t" + event + ": \"mac:" + event + "\",\n") macCHeaderEvents.WriteString("#define Event" + eventTitle + " " + strconv.Itoa(id) + "\n") - eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + strings.TrimSpace(string(line)) + "\",\n") + eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + event + "\",\n") maxMacEvents = id if ignoreEvent { continue @@ -261,9 +249,8 @@ func main() { } commonEventsDecl.WriteString("\t" + eventTitle + " " + eventType + "\n") commonEventsValues.WriteString("\t\t" + event + ": " + strconv.Itoa(id) + ",\n") - commonJSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") - commonTSEvents.WriteString("\t\t" + event + ": string,\n") - eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + strings.TrimSpace(string(line)) + "\",\n") + commonTSEvents.WriteString("\t\t" + event + ": \"common:" + event + "\",\n") + eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + event + "\",\n") case "windows": eventType := "ApplicationEventType" if strings.HasPrefix(event, "Window") { @@ -274,9 +261,8 @@ func main() { } windowsEventsDecl.WriteString("\t" + eventTitle + " " + eventType + "\n") windowsEventsValues.WriteString("\t\t" + event + ": " + strconv.Itoa(id) + ",\n") - windowsJSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") - windowsTSEvents.WriteString("\t\t" + event + ": string,\n") - eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + strings.TrimSpace(string(line)) + "\",\n") + windowsTSEvents.WriteString("\t\t" + event + ": \"windows:" + event + "\",\n") + eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + event + "\",\n") } } @@ -299,22 +285,12 @@ func main() { panic(err) } - // Save the eventsJS template substituting the values and decls - templateToWrite = strings.ReplaceAll(eventsJS, "$$MACJSEVENTS", macJSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$WINDOWSJSEVENTS", windowsJSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$LINUXJSEVENTS", linuxJSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$COMMONJSEVENTS", commonJSEvents.String()) - err = os.WriteFile("../../internal/runtime/desktop/@wailsio/runtime/src/event_types.js", []byte(templateToWrite), 0644) - if err != nil { - panic(err) - } - // Save the eventsTS template substituting the values and decls - templateToWrite = strings.ReplaceAll(eventsTS, "$$MACTSEVENTS", macTSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$WINDOWSTSEVENTS", windowsTSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$LINUXTSEVENTS", linuxTSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$COMMONTSEVENTS", commonTSEvents.String()) - err = os.WriteFile("../../internal/runtime/desktop/@wailsio/runtime/types/event_types.d.ts", []byte(templateToWrite), 0644) + templateToWrite = strings.ReplaceAll(eventsTS, "$$MACJSEVENTS", macTSEvents.String()) + templateToWrite = strings.ReplaceAll(templateToWrite, "$$WINDOWSJSEVENTS", windowsTSEvents.String()) + templateToWrite = strings.ReplaceAll(templateToWrite, "$$LINUXJSEVENTS", linuxTSEvents.String()) + templateToWrite = strings.ReplaceAll(templateToWrite, "$$COMMONJSEVENTS", commonTSEvents.String()) + err = os.WriteFile("../../internal/runtime/desktop/@wailsio/runtime/src/event_types.ts", []byte(templateToWrite), 0644) if err != nil { panic(err) } @@ -402,36 +378,4 @@ func main() { if err != nil { panic(err) } - - // Load the runtime package.json - packageJsonFilename := "../../internal/runtime/desktop/@wailsio/runtime/package.json" - packageJSON, err := os.ReadFile(packageJsonFilename) - if err != nil { - panic(err) - } - version := gjson.Get(string(packageJSON), "version").String() - // Parse and increment version - v := semver.MustParse(version) - prerelease := v.Prerelease() - // Split the prerelease by the "." and increment the last part by 1 - parts := strings.Split(prerelease, ".") - prereleaseDigits, err := strconv.Atoi(parts[len(parts)-1]) - if err != nil { - panic(err) - } - prereleaseNumber := strconv.Itoa(prereleaseDigits + 1) - parts[len(parts)-1] = prereleaseNumber - prerelease = strings.Join(parts, ".") - newVersion, err := v.SetPrerelease(prerelease) - if err != nil { - panic(err) - } - - // Set new version using sjson - newJSON, err := sjson.Set(string(packageJSON), "version", newVersion.String()) - if err != nil { - panic(err) - } - - err = os.WriteFile(packageJsonFilename, []byte(newJSON), 0644) } diff --git a/v3/tasks/events/go.mod b/v3/tasks/events/go.mod index 8b0f46531..55ed969ca 100644 --- a/v3/tasks/events/go.mod +++ b/v3/tasks/events/go.mod @@ -1,14 +1,3 @@ module events -go 1.22 - -require ( - github.com/Masterminds/semver/v3 v3.3.0 - github.com/tidwall/gjson v1.18.0 - github.com/tidwall/sjson v1.2.5 -) - -require ( - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect -) +go 1.24 diff --git a/v3/tasks/events/go.sum b/v3/tasks/events/go.sum deleted file mode 100644 index 2379260c6..000000000 --- a/v3/tasks/events/go.sum +++ /dev/null @@ -1,11 +0,0 @@ -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= diff --git a/v3/tasks/release/RELEASE_NOTES_CREATION.md b/v3/tasks/release/RELEASE_NOTES_CREATION.md new file mode 100644 index 000000000..0fe04d7e0 --- /dev/null +++ b/v3/tasks/release/RELEASE_NOTES_CREATION.md @@ -0,0 +1,118 @@ +# Release Notes Creation Documentation + +## Overview + +The `release.go` script now supports a `--create-release-notes` flag that extracts changelog content from `UNRELEASED_CHANGELOG.md` and creates a clean `release_notes.md` file suitable for GitHub releases. + +## How It Works + +### 1. Command Usage + +```bash +go run release.go --create-release-notes [output_path] +``` + +- `output_path` is optional, defaults to `../../release_notes.md` +- The script expects `UNRELEASED_CHANGELOG.md` to be at `../../UNRELEASED_CHANGELOG.md` (relative to the script location) + +### 2. Extraction Process + +The `extractChangelogContent()` function: + +1. Reads the UNRELEASED_CHANGELOG.md file +2. Filters out: + - The main "# Unreleased Changes" header + - HTML comments (``) + - Empty sections (sections with no bullet points) + - Everything after the `---` separator (example entries) +3. Preserves: + - Section headers (## Added, ## Changed, etc.) that have content + - Bullet points with actual text (both `-` and `*` styles) + - Proper spacing between sections + +### 3. File Structure Expected + +```markdown +# Unreleased Changes + + + +## Added + +- Actual content preserved +- More content + +## Changed + + +## Fixed +- Bug fixes included + +--- + +### Example Entries: +Everything after the --- is ignored +``` + +### 4. Output Format + +The generated `release_notes.md` contains only the actual changelog entries: + +```markdown +## Added +- Actual content preserved +- More content + +## Fixed +- Bug fixes included +``` + +## Testing Results + +### βœ… Successful Tests + +1. **Valid Content Extraction**: Successfully extracts and formats changelog entries +2. **Empty Changelog Detection**: Properly fails when no content exists +3. **Comment Filtering**: Correctly removes HTML comments +4. **Mixed Bullet Styles**: Handles both `-` and `*` bullet points +5. **Custom Output Path**: Supports specifying custom output file location +6. **Flag Compatibility**: Works with `--check-only` and `--extract-changelog` flags + +### Test Commands Run + +```bash +# Create release notes with default path +go run release.go --create-release-notes + +# Create with custom path +go run release.go --create-release-notes /path/to/output.md + +# Check if content exists +go run release.go --check-only + +# Extract content to stdout +go run release.go --extract-changelog +``` + +## Integration with GitHub Workflow + +The nightly release workflow should: + +1. Run `go run release.go --create-release-notes` before the main release task +2. Use the generated `release_notes.md` for the GitHub release body +3. The main release task will clear UNRELEASED_CHANGELOG.md after processing + +## Error Handling + +The script will exit with status 1 if: +- UNRELEASED_CHANGELOG.md doesn't exist +- No actual content is found (only template/comments) +- File write operations fail + +## Benefits + +1. **Separation of Concerns**: Changelog extraction happens before the file is cleared +2. **Clean Output**: No template text or comments in release notes +3. **Testable**: Can be run and tested independently +4. **Flexible**: Supports custom output paths +5. **Consistent**: Same extraction logic used by all flags \ No newline at end of file diff --git a/v3/tasks/release/release.go b/v3/tasks/release/release.go index a053b8778..dc584a570 100644 --- a/v3/tasks/release/release.go +++ b/v3/tasks/release/release.go @@ -1,15 +1,21 @@ package main import ( + "fmt" "os" "strconv" "strings" "time" - - "github.com/wailsapp/wails/v3/internal/s" ) -const versionFile = "../../internal/version/version.txt" +const ( + versionFile = "../../internal/version/version.txt" + changelogFile = "../../../docs/src/content/docs/changelog.mdx" +) + +var ( + unreleasedChangelogFile = "../../UNRELEASED_CHANGELOG.md" +) func checkError(err error) { if err != nil { @@ -18,17 +24,302 @@ func checkError(err error) { } } -// TODO:This can be replaced with "https://github.com/coreos/go-semver/blob/main/semver/semver.go" +// getUnreleasedChangelogTemplate returns the template content for UNRELEASED_CHANGELOG.md +func getUnreleasedChangelogTemplate() string { + return `# Unreleased Changes + + + +## Added + + +## Changed + + +## Fixed + + +## Deprecated + + +## Removed + + +## Security + + +--- + +### Example Entries: + +**Added:** +- Add support for custom window icons in application options +- Add new ` + "`SetWindowIcon()`" + ` method to runtime API (#1234) + +**Changed:** +- Update minimum Go version requirement to 1.21 +- Improve error messages for invalid configuration files + +**Fixed:** +- Fix memory leak in event system during window close operations (#5678) +- Fix crash when using context menus on Linux with Wayland + +**Security:** +- Update dependencies to address CVE-2024-12345 in third-party library +` +} + +// clearUnreleasedChangelog clears the UNRELEASED_CHANGELOG.md file and resets it with the template +func clearUnreleasedChangelog() error { + template := getUnreleasedChangelogTemplate() + + // Write the template back to the file + err := os.WriteFile(unreleasedChangelogFile, []byte(template), 0o644) + if err != nil { + return fmt.Errorf("failed to reset UNRELEASED_CHANGELOG.md: %w", err) + } + + fmt.Printf("Successfully reset %s with template content\n", unreleasedChangelogFile) + return nil +} + +// extractChangelogContent extracts the actual changelog content from UNRELEASED_CHANGELOG.md +// It returns the content between the section headers and the example section +func extractChangelogContent() (string, error) { + content, err := os.ReadFile(unreleasedChangelogFile) + if err != nil { + return "", fmt.Errorf("failed to read %s: %w", unreleasedChangelogFile, err) + } + + contentStr := string(content) + lines := strings.Split(contentStr, "\n") + + var result []string + var inExampleSection bool + var inCommentBlock bool + var hasActualContent bool + var currentSection string + + for i, line := range lines { + trimmedLine := strings.TrimSpace(line) + + // Track comment blocks (handle multi-line comments) + if strings.Contains(line, "") { + inCommentBlock = false + } + continue + } + if inCommentBlock { + if strings.Contains(line, "-->") { + inCommentBlock = false + } + continue + } + + // Skip the main title + if strings.HasPrefix(trimmedLine, "# Unreleased Changes") { + continue + } + + // Check if we're entering the example section + if strings.HasPrefix(trimmedLine, "---") || strings.HasPrefix(trimmedLine, "### Example Entries") { + inExampleSection = true + continue + } + + // Skip example section content + if inExampleSection { + continue + } + + // Handle section headers + if strings.HasPrefix(trimmedLine, "##") { + currentSection = trimmedLine + // Only include section headers that have content after them + // We'll add it later if we find content + continue + } + + // Handle bullet points + if strings.HasPrefix(trimmedLine, "-") || strings.HasPrefix(trimmedLine, "*") { + // Check if this is actual content (not empty) + content := strings.TrimSpace(trimmedLine[1:]) + if content != "" { + // If this is the first content in a section, add the section header first + if currentSection != "" { + // Only add empty line if this isn't the first section + if len(result) > 0 { + result = append(result, "") + } + result = append(result, currentSection) + currentSection = "" // Reset so we don't add it again + } + result = append(result, line) + hasActualContent = true + } + } else if trimmedLine != "" && !strings.HasPrefix(trimmedLine, " + +## Added +- New feature 1 +- New feature 2 + +## Changed +- Changed item 1 + +## Fixed +- Bug fix 1 +- Bug fix 2 + +## Deprecated +- Deprecated feature + +## Removed +- Removed feature + +## Security +- Security fix + +--- + +### Example Entries: + +**Added:** +- Example entry +`, + expected: `## Added +- New feature 1 +- New feature 2 + +## Changed +- Changed item 1 + +## Fixed +- Bug fix 1 +- Bug fix 2 + +## Deprecated +- Deprecated feature + +## Removed +- Removed feature + +## Security +- Security fix`, + wantErr: false, + }, + { + name: "Only Added section with content", + content: `# Unreleased Changes + +## Added + +- Add Windows dark theme support +- Add new flag to release script + +## Changed + + +## Fixed + + +--- +### Example Entries: +`, + expected: `## Added +- Add Windows dark theme support +- Add new flag to release script`, + wantErr: false, + }, + { + name: "Empty sections should not be included", + content: `# Unreleased Changes + +## Added + + +## Changed + +- Update Go version to 1.23 + +## Fixed + + +--- +`, + expected: `## Changed +- Update Go version to 1.23`, + wantErr: false, + }, + { + name: "No content returns empty string", + content: `# Unreleased Changes + +## Added + + +## Changed + + +## Fixed + + +--- +`, + expected: "", + wantErr: false, + }, + { + name: "Comments should be excluded", + content: `# Unreleased Changes + + + +## Added + +- Real content here + +- More content + +--- +`, + expected: `## Added +- Real content here +- More content`, + wantErr: false, + }, + { + name: "Multi-line comments handled correctly", + content: `# Unreleased Changes + + + +## Added +- Feature 1 + +- Feature 2 + +--- +`, + expected: `## Added +- Feature 1 +- Feature 2`, + wantErr: false, + }, + { + name: "Mixed bullet point styles", + content: `# Unreleased Changes + +## Added +- Dash bullet point +* Asterisk bullet point +- Another dash + +--- +`, + expected: `## Added +- Dash bullet point +* Asterisk bullet point +- Another dash`, + wantErr: false, + }, + { + name: "Trailing empty lines removed", + content: `# Unreleased Changes + +## Added +- Feature 1 + + +## Changed +- Change 1 + + + +--- +`, + expected: `## Added +- Feature 1 + +## Changed +- Change 1`, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "UNRELEASED_CHANGELOG.md") + err := os.WriteFile(tmpFile, []byte(tt.content), 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Save original path and update it + originalPath := unreleasedChangelogFile + unreleasedChangelogFile = tmpFile + defer func() { + unreleasedChangelogFile = originalPath + }() + + // Test the function + got, err := extractChangelogContent() + if (err != nil) != tt.wantErr { + t.Errorf("extractChangelogContent() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if got != tt.expected { + t.Errorf("extractChangelogContent() = %q, want %q", got, tt.expected) + } + }) + } +} + +// TestCreateReleaseNotes tests the --create-release-notes functionality +func TestCreateReleaseNotes(t *testing.T) { + tests := []struct { + name string + changelogContent string + expectSuccess bool + expectedNotes string + }{ + { + name: "Valid changelog creates release notes", + changelogContent: `# Unreleased Changes + +## Added +- New feature X +- New feature Y + +## Fixed +- Bug fix A + +--- +### Examples: +`, + expectSuccess: true, + expectedNotes: `## Added +- New feature X +- New feature Y + +## Fixed +- Bug fix A`, + }, + { + name: "Empty changelog fails", + changelogContent: `# Unreleased Changes + +## Added + + +## Changed + + +--- +`, + expectSuccess: false, + expectedNotes: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary directory + tmpDir := t.TempDir() + + // Create UNRELEASED_CHANGELOG.md + changelogPath := filepath.Join(tmpDir, "UNRELEASED_CHANGELOG.md") + err := os.WriteFile(changelogPath, []byte(tt.changelogContent), 0644) + if err != nil { + t.Fatalf("Failed to create changelog file: %v", err) + } + + // Create release notes path + releaseNotesPath := filepath.Join(tmpDir, "release_notes.md") + + // Save original path + originalPath := unreleasedChangelogFile + unreleasedChangelogFile = changelogPath + defer func() { + unreleasedChangelogFile = originalPath + }() + + // Test the create release notes flow + content, err := extractChangelogContent() + if err != nil && tt.expectSuccess { + t.Fatalf("Failed to extract content: %v", err) + } + + if tt.expectSuccess { + if content == "" { + t.Error("Expected content but got empty string") + } else { + // Write the release notes + err = os.WriteFile(releaseNotesPath, []byte(content), 0644) + if err != nil { + t.Fatalf("Failed to write release notes: %v", err) + } + + // Verify the file was created + data, err := os.ReadFile(releaseNotesPath) + if err != nil { + t.Fatalf("Failed to read release notes: %v", err) + } + + if string(data) != tt.expectedNotes { + t.Errorf("Release notes = %q, want %q", string(data), tt.expectedNotes) + } + } + } else { + if content != "" { + t.Errorf("Expected no content but got: %q", content) + } + } + }) + } +} + +// TestHasUnreleasedContent tests the hasUnreleasedContent function +func TestHasUnreleasedContent(t *testing.T) { + tests := []struct { + name string + content string + expected bool + }{ + { + name: "Has content", + content: `# Unreleased Changes + +## Added +- New feature + +--- +`, + expected: true, + }, + { + name: "No content", + content: `# Unreleased Changes + +## Added + + +## Changed + + +--- +`, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "UNRELEASED_CHANGELOG.md") + err := os.WriteFile(tmpFile, []byte(tt.content), 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Save original path + originalPath := unreleasedChangelogFile + unreleasedChangelogFile = tmpFile + defer func() { + unreleasedChangelogFile = originalPath + }() + + // Test the function + got, err := hasUnreleasedContent() + if err != nil { + t.Fatalf("hasUnreleasedContent() error = %v", err) + } + + if got != tt.expected { + t.Errorf("hasUnreleasedContent() = %v, want %v", got, tt.expected) + } + }) + } +} + +// TestVersionIncrement tests version incrementing logic +func TestVersionIncrement(t *testing.T) { + tests := []struct { + name string + currentVersion string + expectedNext string + }{ + { + name: "Alpha version increment", + currentVersion: "v3.0.0-alpha.15", + expectedNext: "v3.0.0-alpha.16", + }, + { + name: "Beta version increment", + currentVersion: "v3.0.0-beta.5", + expectedNext: "v3.0.0-beta.6", + }, + { + name: "Regular version increment", + currentVersion: "v3.0.0", + expectedNext: "v3.0.1", + }, + { + name: "Patch version increment", + currentVersion: "v3.1.5", + expectedNext: "v3.1.6", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test incrementPatchVersion for regular versions + if !strings.Contains(tt.currentVersion, "-") { + got := incrementPatchVersion(tt.currentVersion) + // Note: incrementPatchVersion writes to file, so we just check the return value + if got != tt.expectedNext { + t.Errorf("incrementPatchVersion(%s) = %s, want %s", tt.currentVersion, got, tt.expectedNext) + } + } + }) + } +} + +// TestClearUnreleasedChangelog tests the changelog reset functionality +func TestClearUnreleasedChangelog(t *testing.T) { + // Create temporary file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "UNRELEASED_CHANGELOG.md") + + // Write some content + err := os.WriteFile(tmpFile, []byte("Some content"), 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Save original path + originalPath := unreleasedChangelogFile + unreleasedChangelogFile = tmpFile + defer func() { + unreleasedChangelogFile = originalPath + }() + + // Clear the changelog + err = clearUnreleasedChangelog() + if err != nil { + t.Fatalf("clearUnreleasedChangelog() error = %v", err) + } + + // Read the file + content, err := os.ReadFile(tmpFile) + if err != nil { + t.Fatalf("Failed to read cleared file: %v", err) + } + + // Check it contains the template + if !strings.Contains(string(content), "# Unreleased Changes") { + t.Error("Cleared file doesn't contain template header") + } + if !strings.Contains(string(content), "## Added") { + t.Error("Cleared file doesn't contain Added section") + } + if !strings.Contains(string(content), "### Example Entries:") { + t.Error("Cleared file doesn't contain example section") + } +} \ No newline at end of file diff --git a/v3/tasks/release/release_test.go b/v3/tasks/release/release_test.go new file mode 100644 index 000000000..e598446fc --- /dev/null +++ b/v3/tasks/release/release_test.go @@ -0,0 +1,1043 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +// setupTestEnvironment creates a proper directory structure for tests +// It returns the cleanup function and the project root directory +func setupTestEnvironment(t *testing.T) (cleanup func(), projectRoot string) { + // Save current directory + originalDir, _ := os.Getwd() + + // Create a temporary directory for testing + tmpDir := t.TempDir() + + // Create the wails project structure within temp directory + projectRoot = filepath.Join(tmpDir, "wails") + v3Dir := filepath.Join(projectRoot, "v3") + releaseDir := filepath.Join(v3Dir, "tasks", "release") + + // Create all necessary directories + err := os.MkdirAll(releaseDir, 0755) + if err != nil { + t.Fatalf("Failed to create test directory structure: %v", err) + } + + // Change to the release directory (where the script would run from) + os.Chdir(releaseDir) + + // Return cleanup function and project root + cleanup = func() { + os.Chdir(originalDir) + } + return cleanup, projectRoot +} + +func TestExtractChangelogContent_EmptySections(t *testing.T) { + cleanup, _ := setupTestEnvironment(t) + defer cleanup() + + // Create a test file with only one section having content + testContent := `# Unreleased Changes + + + +## Added + + +## Changed + + +## Fixed +- Fix critical bug in the system + +## Deprecated + + +## Removed + + +## Security + + +--- + +### Example Entries: + +**Added:** +- Example entry that should not be included` + + err := os.WriteFile(unreleasedChangelogFile, []byte(testContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Extract the content + content, err := extractChangelogContent() + if err != nil { + t.Fatalf("extractChangelogContent() failed: %v", err) + } + + // Verify we got content + if content == "" { + t.Fatal("Expected to extract content, but got empty string") + } + + // Verify ONLY the Fixed section is included (the only one with content) + if !strings.Contains(content, "## Fixed") { + t.Error("Expected to find '## Fixed' section header") + } + + if !strings.Contains(content, "Fix critical bug in the system") { + t.Error("Expected to find the Fixed content") + } + + // Verify empty sections are NOT included + sections := []string{"## Added", "## Changed", "## Deprecated", "## Removed", "## Security"} + for _, section := range sections { + if strings.Contains(content, section) { + t.Errorf("Expected NOT to find empty section '%s'", section) + } + } + + // Verify comments are not included + if strings.Contains(content, "") { + t.Error("Expected NOT to find HTML comments") + } + + // Verify example content is not included + if strings.Contains(content, "Example entry that should not be included") { + t.Error("Expected NOT to find example content") + } +} + +func TestExtractChangelogContent_AllEmpty(t *testing.T) { + cleanup, _ := setupTestEnvironment(t) + defer cleanup() + + // Create a test file with all empty sections (just the template) + testContent := getUnreleasedChangelogTemplate() + + err := os.WriteFile(unreleasedChangelogFile, []byte(testContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Extract the content + content, err := extractChangelogContent() + if err != nil { + t.Fatalf("extractChangelogContent() failed: %v", err) + } + + // Verify we got empty string (no content) + if content != "" { + t.Fatalf("Expected empty string for template-only file, got: %s", content) + } +} + +func TestExtractChangelogContent_MixedSections(t *testing.T) { + cleanup, _ := setupTestEnvironment(t) + defer cleanup() + + // Create a test file with some sections having content, others empty + testContent := `# Unreleased Changes + +## Added +- New feature A +- New feature B + +## Changed + + +## Fixed + + +## Deprecated +- Deprecated feature X + +## Removed + + +## Security +- Security fix for CVE-2024-1234` + + err := os.WriteFile(unreleasedChangelogFile, []byte(testContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Extract the content + content, err := extractChangelogContent() + if err != nil { + t.Fatalf("extractChangelogContent() failed: %v", err) + } + + // Verify we got content + if content == "" { + t.Fatal("Expected to extract content, but got empty string") + } + + // Verify sections WITH content are included + if !strings.Contains(content, "## Added") { + t.Error("Expected to find '## Added' section header") + } + if !strings.Contains(content, "New feature A") { + t.Error("Expected to find Added content") + } + + if !strings.Contains(content, "## Deprecated") { + t.Error("Expected to find '## Deprecated' section header") + } + if !strings.Contains(content, "Deprecated feature X") { + t.Error("Expected to find Deprecated content") + } + + if !strings.Contains(content, "## Security") { + t.Error("Expected to find '## Security' section header") + } + if !strings.Contains(content, "Security fix for CVE-2024-1234") { + t.Error("Expected to find Security content") + } + + // Verify empty sections are NOT included + emptyHeaders := []string{"## Changed", "## Fixed", "## Removed"} + for _, header := range emptyHeaders { + if strings.Contains(content, header) { + t.Errorf("Expected NOT to find empty section '%s'", header) + } + } + + // Print the extracted content for debugging + t.Logf("Extracted content:\n%s", content) +} + +func TestGetUnreleasedChangelogTemplate(t *testing.T) { + template := getUnreleasedChangelogTemplate() + + // Check that template contains required sections + requiredSections := []string{ + "# Unreleased Changes", + "## Added", + "## Changed", + "## Fixed", + "## Deprecated", + "## Removed", + "## Security", + "### Example Entries", + } + + for _, section := range requiredSections { + if !strings.Contains(template, section) { + t.Errorf("Template missing required section: %s", section) + } + } + + // Check that template contains guidelines + if !strings.Contains(template, "Guidelines:") { + t.Error("Template missing guidelines section") + } + + // Check that template contains example entries + if !strings.Contains(template, "Add support for custom window icons") { + t.Error("Template missing example entries") + } +} + +func TestHasUnreleasedContent_WithContent(t *testing.T) { + cleanup, _ := setupTestEnvironment(t) + defer cleanup() + + // Create a file with actual content + testContent := `# Unreleased Changes + +## Added +- Add new feature for testing +- Add another important feature + +## Fixed +- Fix critical bug in system` + + err := os.WriteFile(unreleasedChangelogFile, []byte(testContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + hasContent, err := hasUnreleasedContent() + if err != nil { + t.Fatalf("hasUnreleasedContent() failed: %v", err) + } + + if !hasContent { + t.Error("Expected hasUnreleasedContent() to return true for file with content") + } +} + +func TestHasUnreleasedContent_WithoutContent(t *testing.T) { + cleanup, _ := setupTestEnvironment(t) + defer cleanup() + + // Create a file with only template content (no actual entries) + template := getUnreleasedChangelogTemplate() + err := os.WriteFile(unreleasedChangelogFile, []byte(template), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + hasContent, err := hasUnreleasedContent() + if err != nil { + t.Fatalf("hasUnreleasedContent() failed: %v", err) + } + + if hasContent { + t.Error("Expected hasUnreleasedContent() to return false for template-only file") + } +} + +func TestHasUnreleasedContent_WithEmptyBullets(t *testing.T) { + cleanup, _ := setupTestEnvironment(t) + defer cleanup() + + // Create a file with empty bullet points + testContent := `# Unreleased Changes + +## Added +- +- + +## Fixed +` + + err := os.WriteFile(unreleasedChangelogFile, []byte(testContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + hasContent, err := hasUnreleasedContent() + if err != nil { + t.Fatalf("hasUnreleasedContent() failed: %v", err) + } + + if hasContent { + t.Error("Expected hasUnreleasedContent() to return false for file with empty bullets") + } +} + +func TestHasUnreleasedContent_NonexistentFile(t *testing.T) { + cleanup, _ := setupTestEnvironment(t) + defer cleanup() + + // Don't create the file + hasContent, err := hasUnreleasedContent() + if err == nil { + t.Error("Expected hasUnreleasedContent() to return an error for nonexistent file") + } + + if hasContent { + t.Error("Expected hasUnreleasedContent() to return false for nonexistent file") + } +} + +func TestSafeFileOperation_Success(t *testing.T) { + // Create a temporary directory for testing + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.txt") + + // Create initial file + initialContent := "initial content" + err := os.WriteFile(testFile, []byte(initialContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Perform safe operation that succeeds + newContent := "new content" + err = safeFileOperation(testFile, func() error { + return os.WriteFile(testFile, []byte(newContent), 0o644) + }) + + if err != nil { + t.Fatalf("safeFileOperation() failed: %v", err) + } + + // Verify the file has new content + content, err := os.ReadFile(testFile) + if err != nil { + t.Fatalf("Failed to read file after operation: %v", err) + } + + if string(content) != newContent { + t.Errorf("Expected file content '%s', got '%s'", newContent, string(content)) + } + + // Verify backup file was cleaned up + backupFile := testFile + ".backup" + if _, err := os.Stat(backupFile); !os.IsNotExist(err) { + t.Error("Backup file was not cleaned up after successful operation") + } +} + +func TestSafeFileOperation_Failure(t *testing.T) { + // Create a temporary directory for testing + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.txt") + + // Create initial file + initialContent := "initial content" + err := os.WriteFile(testFile, []byte(initialContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Perform safe operation that fails + err = safeFileOperation(testFile, func() error { + // First write something to the file + os.WriteFile(testFile, []byte("corrupted content"), 0o644) + // Then return an error to simulate failure + return os.ErrInvalid + }) + + if err == nil { + t.Error("Expected safeFileOperation() to return error") + } + + // Verify the file was restored to original content + content, err := os.ReadFile(testFile) + if err != nil { + t.Fatalf("Failed to read file after failed operation: %v", err) + } + + if string(content) != initialContent { + t.Errorf("Expected file content to be restored to '%s', got '%s'", initialContent, string(content)) + } + + // Verify backup file was cleaned up + backupFile := testFile + ".backup" + if _, err := os.Stat(backupFile); !os.IsNotExist(err) { + t.Error("Backup file was not cleaned up after failed operation") + } +} + +func TestSafeFileOperation_NewFile(t *testing.T) { + // Create a temporary directory for testing + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "newfile.txt") + + // Perform safe operation on non-existent file + content := "new file content" + err := safeFileOperation(testFile, func() error { + return os.WriteFile(testFile, []byte(content), 0o644) + }) + + if err != nil { + t.Fatalf("safeFileOperation() failed: %v", err) + } + + // Verify the file was created with correct content + fileContent, err := os.ReadFile(testFile) + if err != nil { + t.Fatalf("Failed to read created file: %v", err) + } + + if string(fileContent) != content { + t.Errorf("Expected file content '%s', got '%s'", content, string(fileContent)) + } +} + +func TestCopyFile(t *testing.T) { + // Create a temporary directory for testing + tmpDir := t.TempDir() + srcFile := filepath.Join(tmpDir, "source.txt") + dstFile := filepath.Join(tmpDir, "destination.txt") + + // Create source file + content := "test content for copying" + err := os.WriteFile(srcFile, []byte(content), 0o644) + if err != nil { + t.Fatalf("Failed to create source file: %v", err) + } + + // Copy the file + err = copyFile(srcFile, dstFile) + if err != nil { + t.Fatalf("copyFile() failed: %v", err) + } + + // Verify destination file exists and has correct content + dstContent, err := os.ReadFile(dstFile) + if err != nil { + t.Fatalf("Failed to read destination file: %v", err) + } + + if string(dstContent) != content { + t.Errorf("Expected destination content '%s', got '%s'", content, string(dstContent)) + } +} + +func TestCopyFile_NonexistentSource(t *testing.T) { + // Create a temporary directory for testing + tmpDir := t.TempDir() + srcFile := filepath.Join(tmpDir, "nonexistent.txt") + dstFile := filepath.Join(tmpDir, "destination.txt") + + // Try to copy non-existent file + err := copyFile(srcFile, dstFile) + if err == nil { + t.Error("Expected copyFile() to return error for non-existent source") + } + + // Verify destination file was not created + if _, err := os.Stat(dstFile); !os.IsNotExist(err) { + t.Error("Destination file should not exist after failed copy") + } +} + +func TestUpdateVersion(t *testing.T) { + tests := []struct { + name string + currentVersion string + expectedVersion string + }{ + { + name: "Alpha version increment", + currentVersion: "v3.0.0-alpha.12", + expectedVersion: "v3.0.0-alpha.13", + }, + { + name: "Beta version increment", + currentVersion: "v3.0.0-beta.5", + expectedVersion: "v3.0.0-beta.6", + }, + { + name: "RC version increment", + currentVersion: "v2.5.0-rc.1", + expectedVersion: "v2.5.0-rc.2", + }, + { + name: "Patch version increment", + currentVersion: "v3.0.0", + expectedVersion: "v3.0.1", + }, + { + name: "Patch version with higher number", + currentVersion: "v1.2.15", + expectedVersion: "v1.2.16", + }, + { + name: "Pre-release without number becomes patch", + currentVersion: "v3.0.0-alpha", + expectedVersion: "v3.0.1", + }, + { + name: "Version without v prefix", + currentVersion: "3.0.0", + expectedVersion: "v3.0.1", + }, + { + name: "Alpha with large number", + currentVersion: "v3.0.0-alpha.999", + expectedVersion: "v3.0.0-alpha.1000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary directory for this test + tmpDir := t.TempDir() + tempVersionFile := filepath.Join(tmpDir, "version.txt") + + // Save original versionFile path + originalVersionFile := versionFile + defer func() { + // Restore original value + _ = originalVersionFile + }() + + // Write the current version to temp file + err := os.WriteFile(tempVersionFile, []byte(tt.currentVersion), 0o644) + if err != nil { + t.Fatalf("Failed to write test version file: %v", err) + } + + // Test the updateVersion function logic directly + result := func() string { + currentVersionData, err := os.ReadFile(tempVersionFile) + if err != nil { + t.Fatalf("Failed to read version file: %v", err) + } + currentVersion := strings.TrimSpace(string(currentVersionData)) + + // Check if it has a pre-release suffix (e.g., -alpha.12, -beta.1) + if strings.Contains(currentVersion, "-") { + // Split on the dash to separate version and pre-release + parts := strings.SplitN(currentVersion, "-", 2) + baseVersion := parts[0] + preRelease := parts[1] + + // Check if pre-release has a numeric suffix (e.g., alpha.12) + lastDotIndex := strings.LastIndex(preRelease, ".") + if lastDotIndex != -1 { + preReleaseTag := preRelease[:lastDotIndex] + numberStr := preRelease[lastDotIndex+1:] + + // Try to parse the number + if number, err := strconv.Atoi(numberStr); err == nil { + // Increment the pre-release number + number++ + newVersion := fmt.Sprintf("%s-%s.%d", baseVersion, preReleaseTag, number) + return newVersion + } + } + + // If we can't parse the pre-release format, just increment patch version + // and remove pre-release suffix + return testIncrementPatchVersion(baseVersion) + } + + // No pre-release suffix, just increment patch version + return testIncrementPatchVersion(currentVersion) + }() + + if result != tt.expectedVersion { + t.Errorf("updateVersion() = %v, want %v", result, tt.expectedVersion) + } + }) + } +} + +// testIncrementPatchVersion is a test version of incrementPatchVersion that doesn't write to file +func testIncrementPatchVersion(version string) string { + // Remove 'v' prefix if present + versionWithoutV := strings.TrimPrefix(version, "v") + + // Split into major.minor.patch + parts := strings.Split(versionWithoutV, ".") + if len(parts) != 3 { + // Not a valid semver, return as-is + return version + } + + // Parse patch version + patch, err := strconv.Atoi(parts[2]) + if err != nil { + return version + } + + // Increment patch + patch++ + + // Reconstruct version + return fmt.Sprintf("v%s.%s.%d", parts[0], parts[1], patch) +} + +// extractTestContent is a test helper that extracts changelog content using the same logic as extractChangelogContent +func extractTestContent(contentStr string) string { + lines := strings.Split(contentStr, "\n") + + var result []string + var inExampleSection bool + var inCommentBlock bool + var hasActualContent bool + var currentSection string + + for i, line := range lines { + trimmedLine := strings.TrimSpace(line) + + // Track comment blocks (handle multi-line comments) + if strings.Contains(line, "") { + inCommentBlock = false + } + continue + } + if inCommentBlock { + if strings.Contains(line, "-->") { + inCommentBlock = false + } + continue + } + + // Skip the main title + if strings.HasPrefix(trimmedLine, "# Unreleased Changes") { + continue + } + + // Check if we're entering the example section + if strings.HasPrefix(trimmedLine, "---") || strings.HasPrefix(trimmedLine, "### Example Entries") { + inExampleSection = true + continue + } + + // Skip example section content + if inExampleSection { + continue + } + + // Handle section headers + if strings.HasPrefix(trimmedLine, "##") { + currentSection = trimmedLine + // Only include section headers that have content after them + // We'll add it later if we find content + continue + } + + // Handle bullet points + if strings.HasPrefix(trimmedLine, "-") || strings.HasPrefix(trimmedLine, "*") { + // Check if this is actual content (not empty) + content := strings.TrimSpace(trimmedLine[1:]) + if content != "" { + // If this is the first content in a section, add the section header first + if currentSection != "" { + // Only add empty line if this isn't the first section + if len(result) > 0 { + result = append(result, "") + } + result = append(result, currentSection) + currentSection = "" // Reset so we don't add it again + } + result = append(result, line) + hasActualContent = true + } + } else if trimmedLine != "" && !strings.HasPrefix(trimmedLine, " + +## Fixed + + +--- + +### Example Entries: + +**Added:** +- Add support for custom window icons in application options +- Add new SetWindowIcon() method to runtime API (#1234)` + + err := os.WriteFile(unreleasedChangelogFile, []byte(testContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + hasContent, err := hasUnreleasedContent() + if err != nil { + t.Fatalf("hasUnreleasedContent() failed: %v", err) + } + + if hasContent { + t.Error("Expected hasUnreleasedContent() to return false when content is only in example section") + } +} + +func TestHasUnreleasedContent_WithMixedContent(t *testing.T) { + cleanup, _ := setupTestEnvironment(t) + defer cleanup() + + // Create a file with both real content and example content + testContent := `# Unreleased Changes + +## Added +- Real feature addition here + +## Fixed + + +--- + +### Example Entries: + +**Added:** +- Add support for custom window icons in application options` + + err := os.WriteFile(unreleasedChangelogFile, []byte(testContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + hasContent, err := hasUnreleasedContent() + if err != nil { + t.Fatalf("hasUnreleasedContent() failed: %v", err) + } + + if !hasContent { + t.Error("Expected hasUnreleasedContent() to return true when file has real content") + } +} + +// Integration test for the complete cleanup workflow +func TestCleanupWorkflow_Integration(t *testing.T) { + cleanup, _ := setupTestEnvironment(t) + defer cleanup() + + // Create a changelog file with actual content + testContent := `# Unreleased Changes + +## Added +- Add comprehensive changelog processing system +- Add validation for Keep a Changelog format compliance + +## Fixed +- Fix parsing issues with various markdown bullet styles +- Fix validation edge cases for empty content sections` + + err := os.WriteFile(unreleasedChangelogFile, []byte(testContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Step 1: Check that file has content + hasContent, err := hasUnreleasedContent() + if err != nil { + t.Fatalf("hasUnreleasedContent() failed: %v", err) + } + if !hasContent { + t.Fatal("Expected file to have content") + } + + // Step 2: Perform safe cleanup operation + err = safeFileOperation(unreleasedChangelogFile, func() error { + return clearUnreleasedChangelog() + }) + if err != nil { + t.Fatalf("Safe cleanup operation failed: %v", err) + } + + // Step 3: Verify file was reset to template + content, err := os.ReadFile(unreleasedChangelogFile) + if err != nil { + t.Fatalf("Failed to read file after cleanup: %v", err) + } + + template := getUnreleasedChangelogTemplate() + if string(content) != template { + t.Error("File was not properly reset to template") + } + + // Step 4: Verify original content is gone + if strings.Contains(string(content), "Add comprehensive changelog processing system") { + t.Error("Original content still present after cleanup") + } + + // Step 5: Verify file no longer has content + hasContentAfter, err := hasUnreleasedContent() + if err != nil { + t.Fatalf("hasUnreleasedContent() failed after cleanup: %v", err) + } + if hasContentAfter { + t.Error("File should not have content after cleanup") + } +} + +func TestFullReleaseWorkflow_OnlyNonEmptySections(t *testing.T) { + cleanup, projectRoot := setupTestEnvironment(t) + defer cleanup() + + // Create subdirectories to match expected structure + err := os.MkdirAll(filepath.Join(projectRoot, "v3", "internal", "version"), 0755) + if err != nil { + t.Fatalf("Failed to create version directory: %v", err) + } + + err = os.MkdirAll(filepath.Join(projectRoot, "docs", "src", "content", "docs"), 0755) + if err != nil { + t.Fatalf("Failed to create docs directory: %v", err) + } + + // Create version file + versionFile := filepath.Join(projectRoot, "v3", "internal", "version", "version.txt") + err = os.WriteFile(versionFile, []byte("v1.0.0-alpha.5"), 0644) + if err != nil { + t.Fatalf("Failed to create version file: %v", err) + } + + // Create initial changelog + changelogFile := filepath.Join(projectRoot, "docs", "src", "content", "docs", "changelog.mdx") + initialChangelog := `--- +title: Changelog +--- + +## [Unreleased] + +## v1.0.0-alpha.4 - 2024-01-01 + +### Added +- Previous feature + +` + err = os.WriteFile(changelogFile, []byte(initialChangelog), 0644) + if err != nil { + t.Fatalf("Failed to create changelog file: %v", err) + } + + // Create UNRELEASED_CHANGELOG.md with mixed content + unreleasedContent := `# Unreleased Changes + +## Added +- New amazing feature +- Another cool addition + +## Changed + + +## Fixed + + +## Deprecated +- Old API method + +## Removed + + +## Security + +` + // The script expects the file at ../../UNRELEASED_CHANGELOG.md relative to release dir + unreleasedFile := filepath.Join(projectRoot, "v3", "UNRELEASED_CHANGELOG.md") + err = os.WriteFile(unreleasedFile, []byte(unreleasedContent), 0644) + if err != nil { + t.Fatalf("Failed to create unreleased changelog: %v", err) + } + + // Run the release process simulation + // Read and process the files manually since we can't override constants + content, err := os.ReadFile(unreleasedFile) + if err != nil { + t.Fatalf("Failed to read unreleased file: %v", err) + } + + // Extract content using the same logic as extractChangelogContent + changelogContent := extractTestContent(string(content)) + if changelogContent == "" { + t.Fatal("Failed to extract any content") + } + + // Verify only non-empty sections were extracted + if !strings.Contains(changelogContent, "## Added") { + t.Error("Expected '## Added' section to be included") + } + if !strings.Contains(changelogContent, "## Deprecated") { + t.Error("Expected '## Deprecated' section to be included") + } + + // Verify empty sections were NOT extracted + emptySections := []string{"## Changed", "## Fixed", "## Removed", "## Security"} + for _, section := range emptySections { + if strings.Contains(changelogContent, section) { + t.Errorf("Expected empty section '%s' to NOT be included", section) + } + } + + // Simulate updating the main changelog + changelogData, _ := os.ReadFile(changelogFile) + changelog := string(changelogData) + changelogSplit := strings.Split(changelog, "## [Unreleased]") + + newVersion := "v1.0.0-alpha.6" + today := "2024-01-15" + newChangelog := changelogSplit[0] + "## [Unreleased]\n\n## " + newVersion + " - " + today + "\n\n" + changelogContent + changelogSplit[1] + + // Verify the final changelog format + if !strings.Contains(newChangelog, "## v1.0.0-alpha.6 - 2024-01-15") { + t.Error("Expected new version header in changelog") + } + + // Count occurrences of section headers in the new version section + newVersionSection := strings.Split(newChangelog, "## v1.0.0-alpha.4")[0] + + addedCount := strings.Count(newVersionSection, "## Added") + if addedCount != 1 { + t.Errorf("Expected exactly 1 '## Added' section, got %d", addedCount) + } + + deprecatedCount := strings.Count(newVersionSection, "## Deprecated") + if deprecatedCount != 1 { + t.Errorf("Expected exactly 1 '## Deprecated' section, got %d", deprecatedCount) + } + + // Ensure no empty sections in the new version section + for _, section := range []string{"## Changed", "## Fixed", "## Removed", "## Security"} { + count := strings.Count(newVersionSection, section) + if count > 0 { + t.Errorf("Expected 0 occurrences of empty section '%s', got %d", section, count) + } + } +} + +// Test error handling during cleanup +func TestCleanupWorkflow_ErrorHandling(t *testing.T) { + cleanup, _ := setupTestEnvironment(t) + defer cleanup() + + // Create a changelog file with content + testContent := `# Unreleased Changes + +## Added +- Test content that should be preserved on error` + + err := os.WriteFile(unreleasedChangelogFile, []byte(testContent), 0o644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Simulate an error during cleanup by making the operation fail + err = safeFileOperation(unreleasedChangelogFile, func() error { + // First corrupt the file + os.WriteFile(unreleasedChangelogFile, []byte("corrupted"), 0o644) + // Then return an error + return os.ErrPermission + }) + + if err == nil { + t.Error("Expected safeFileOperation to return error") + } + + // Verify original content was restored + content, err := os.ReadFile(unreleasedChangelogFile) + if err != nil { + t.Fatalf("Failed to read file after error: %v", err) + } + + if string(content) != testContent { + t.Error("Original content was not restored after error") + } +} diff --git a/v3/tasks/release/test_create_release_notes.go b/v3/tasks/release/test_create_release_notes.go new file mode 100644 index 000000000..a9ac66d25 --- /dev/null +++ b/v3/tasks/release/test_create_release_notes.go @@ -0,0 +1,172 @@ +// +build ignore + +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func main() { + fmt.Println("Testing release.go --create-release-notes functionality") + fmt.Println("=" + strings.Repeat("=", 50)) + + // Test cases + testCases := []struct { + name string + content string + expected string + shouldFail bool + }{ + { + name: "Valid changelog with content", + content: `# Unreleased Changes + + + +## Added +- Add Windows dark theme support for menus +- Add new --create-release-notes flag + +## Changed +- Update Go version to 1.23 +- Improve error handling + +## Fixed +- Fix nightly release workflow +- Fix changelog extraction + +--- + +### Example Entries: +Example content here`, + expected: `## Added +- Add Windows dark theme support for menus +- Add new --create-release-notes flag + +## Changed +- Update Go version to 1.23 +- Improve error handling + +## Fixed +- Fix nightly release workflow +- Fix changelog extraction`, + shouldFail: false, + }, + { + name: "Empty changelog", + content: `# Unreleased Changes + +## Added + + +## Changed + + +## Fixed + + +---`, + expected: "", + shouldFail: true, + }, + { + name: "Only one section with content", + content: `# Unreleased Changes + +## Added + + +## Changed +- Single change item here + +## Fixed + + +---`, + expected: `## Changed +- Single change item here`, + shouldFail: false, + }, + } + + // Create a temporary directory for testing + tmpDir, err := os.MkdirTemp("", "release-test-*") + if err != nil { + fmt.Printf("Failed to create temp dir: %v\n", err) + os.Exit(1) + } + defer os.RemoveAll(tmpDir) + + // Save current directory + originalDir, _ := os.Getwd() + + for i, tc := range testCases { + fmt.Printf("\nTest %d: %s\n", i+1, tc.name) + fmt.Println("-" + strings.Repeat("-", 40)) + + // Create test changelog + changelogPath := filepath.Join(tmpDir, "UNRELEASED_CHANGELOG.md") + err = os.WriteFile(changelogPath, []byte(tc.content), 0644) + if err != nil { + fmt.Printf("❌ Failed to write test changelog: %v\n", err) + continue + } + + // Create release notes path + releaseNotesPath := filepath.Join(tmpDir, fmt.Sprintf("release_notes_%d.md", i)) + + // Change to temp dir (so relative paths work) + os.Chdir(tmpDir) + + // Run the command + cmd := exec.Command("go", "run", filepath.Join(originalDir, "release.go"), "--create-release-notes", releaseNotesPath) + output, err := cmd.CombinedOutput() + + // Change back + os.Chdir(originalDir) + + if tc.shouldFail { + if err == nil { + fmt.Printf("❌ Expected failure but command succeeded\n") + fmt.Printf("Output: %s\n", output) + } else { + fmt.Printf("βœ… Failed as expected: %v\n", err) + } + } else { + if err != nil { + fmt.Printf("❌ Command failed: %v\n", err) + fmt.Printf("Output: %s\n", output) + } else { + fmt.Printf("βœ… Command succeeded\n") + + // Read and verify the output + content, err := os.ReadFile(releaseNotesPath) + if err != nil { + fmt.Printf("❌ Failed to read release notes: %v\n", err) + } else { + actualContent := strings.TrimSpace(string(content)) + expectedContent := strings.TrimSpace(tc.expected) + + if actualContent == expectedContent { + fmt.Printf("βœ… Content matches expected\n") + } else { + fmt.Printf("❌ Content mismatch\n") + fmt.Printf("Expected:\n%s\n", expectedContent) + fmt.Printf("Actual:\n%s\n", actualContent) + } + } + } + } + + // Clean up + os.Remove(changelogPath) + os.Remove(releaseNotesPath) + } + + fmt.Println("\n" + "=" + strings.Repeat("=", 50)) + fmt.Println("Testing complete!") +} \ No newline at end of file diff --git a/v3/tasks/release/test_edge_cases.bat b/v3/tasks/release/test_edge_cases.bat new file mode 100644 index 000000000..390a972ab --- /dev/null +++ b/v3/tasks/release/test_edge_cases.bat @@ -0,0 +1,111 @@ +@echo off +echo Testing edge cases for release.go +echo ================================= + +set ORIGINAL_DIR=%CD% +cd ..\.. + +REM Backup existing file +if exist UNRELEASED_CHANGELOG.md ( + copy UNRELEASED_CHANGELOG.md UNRELEASED_CHANGELOG.md.backup > nul +) + +echo. +echo Test 1: Empty changelog (should fail) +echo ------------------------------------- + +REM Create empty changelog +echo # Unreleased Changes > UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Added >> UNRELEASED_CHANGELOG.md +echo ^<^!-- New features --^> >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Changed >> UNRELEASED_CHANGELOG.md +echo ^<^!-- Changes --^> >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Fixed >> UNRELEASED_CHANGELOG.md +echo ^<^!-- Bug fixes --^> >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo --- >> UNRELEASED_CHANGELOG.md + +cd tasks\release +go run release.go --create-release-notes 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo SUCCESS: Command failed as expected for empty changelog +) else ( + echo FAIL: Command should have failed for empty changelog +) + +echo. +echo Test 2: Only comments (should fail) +echo ----------------------------------- + +cd ..\.. +echo # Unreleased Changes > UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Added >> UNRELEASED_CHANGELOG.md +echo ^<^!-- This is just a comment --^> >> UNRELEASED_CHANGELOG.md +echo ^<^!-- Another comment --^> >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo --- >> UNRELEASED_CHANGELOG.md + +cd tasks\release +go run release.go --create-release-notes 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo SUCCESS: Command failed as expected for comment-only changelog +) else ( + echo FAIL: Command should have failed for comment-only changelog +) + +echo. +echo Test 3: Mixed bullet styles +echo --------------------------- + +cd ..\.. +echo # Unreleased Changes > UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Added >> UNRELEASED_CHANGELOG.md +echo - Dash bullet point >> UNRELEASED_CHANGELOG.md +echo * Asterisk bullet point >> UNRELEASED_CHANGELOG.md +echo - Another dash >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo --- >> UNRELEASED_CHANGELOG.md + +cd tasks\release +go run release.go --create-release-notes +if %ERRORLEVEL% EQU 0 ( + echo SUCCESS: Mixed bullet styles handled + echo Content: + type ..\..\release_notes.md +) else ( + echo FAIL: Mixed bullet styles should work +) + +echo. +echo Test 4: Custom output path +echo -------------------------- + +go run release.go --create-release-notes ..\..\custom_notes.md +if %ERRORLEVEL% EQU 0 ( + if exist "..\..\custom_notes.md" ( + echo SUCCESS: Custom path works + del ..\..\custom_notes.md + ) else ( + echo FAIL: Custom path file not created + ) +) else ( + echo FAIL: Custom path should work +) + +REM Clean up +cd ..\.. +if exist release_notes.md del release_notes.md +if exist UNRELEASED_CHANGELOG.md.backup ( + move /Y UNRELEASED_CHANGELOG.md.backup UNRELEASED_CHANGELOG.md > nul +) + +cd %ORIGINAL_DIR% + +echo. +echo ================================= +echo Edge case testing complete! \ No newline at end of file diff --git a/v3/tasks/release/test_simple.bat b/v3/tasks/release/test_simple.bat new file mode 100644 index 000000000..149655c0f --- /dev/null +++ b/v3/tasks/release/test_simple.bat @@ -0,0 +1,115 @@ +@echo off +echo Testing release.go --create-release-notes functionality +echo ====================================================== + +REM Save current directory +set ORIGINAL_DIR=%CD% + +REM Go to v3 root (where UNRELEASED_CHANGELOG.md should be) +cd ..\.. + +REM Backup existing UNRELEASED_CHANGELOG.md if it exists +if exist UNRELEASED_CHANGELOG.md ( + copy UNRELEASED_CHANGELOG.md UNRELEASED_CHANGELOG.md.backup > nul +) + +REM Create a test UNRELEASED_CHANGELOG.md +echo # Unreleased Changes > UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ^<^!-- >> UNRELEASED_CHANGELOG.md +echo This file is used to collect changelog entries for the next v3-alpha release. >> UNRELEASED_CHANGELOG.md +echo --^> >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Added >> UNRELEASED_CHANGELOG.md +echo ^<^!-- New features, capabilities, or enhancements --^> >> UNRELEASED_CHANGELOG.md +echo - Add Windows dark theme support for menus and menubar >> UNRELEASED_CHANGELOG.md +echo - Add `--create-release-notes` flag to release script >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Changed >> UNRELEASED_CHANGELOG.md +echo ^<^!-- Changes in existing functionality --^> >> UNRELEASED_CHANGELOG.md +echo - Update Go version to 1.23 in workflow >> UNRELEASED_CHANGELOG.md +echo - Improve error handling in release process >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Fixed >> UNRELEASED_CHANGELOG.md +echo ^<^!-- Bug fixes --^> >> UNRELEASED_CHANGELOG.md +echo - Fix nightly release workflow changelog extraction >> UNRELEASED_CHANGELOG.md +echo - Fix Go cache configuration in GitHub Actions >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Deprecated >> UNRELEASED_CHANGELOG.md +echo ^<^!-- Soon-to-be removed features --^> >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Removed >> UNRELEASED_CHANGELOG.md +echo ^<^!-- Features removed in this release --^> >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ## Security >> UNRELEASED_CHANGELOG.md +echo ^<^!-- Security-related changes --^> >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo --- >> UNRELEASED_CHANGELOG.md +echo. >> UNRELEASED_CHANGELOG.md +echo ### Example Entries: >> UNRELEASED_CHANGELOG.md + +echo. +echo Test 1: Running with valid content +echo ----------------------------------- + +REM Run the release script +cd tasks\release +go run release.go --create-release-notes + +if %ERRORLEVEL% EQU 0 ( + echo SUCCESS: Command succeeded + + REM Check if release_notes.md was created + if exist "..\..\release_notes.md" ( + echo SUCCESS: release_notes.md was created + echo. + echo Content: + echo -------- + type ..\..\release_notes.md + echo. + echo -------- + ) else ( + echo FAIL: release_notes.md was NOT created + ) +) else ( + echo FAIL: Command failed +) + +echo. +echo Test 2: Check --check-only flag +echo -------------------------------- + +REM Test the check-only flag +go run release.go --check-only +if %ERRORLEVEL% EQU 0 ( + echo SUCCESS: --check-only detected content +) else ( + echo FAIL: --check-only did not detect content +) + +echo. +echo Test 3: Check --extract-changelog flag +echo -------------------------------------- + +REM Test the extract-changelog flag +go run release.go --extract-changelog +if %ERRORLEVEL% EQU 0 ( + echo SUCCESS: --extract-changelog succeeded +) else ( + echo FAIL: --extract-changelog failed +) + +REM Clean up +cd ..\.. +if exist release_notes.md del release_notes.md + +REM Restore original UNRELEASED_CHANGELOG.md if it exists +if exist UNRELEASED_CHANGELOG.md.backup ( + move /Y UNRELEASED_CHANGELOG.md.backup UNRELEASED_CHANGELOG.md > nul +) + +cd %ORIGINAL_DIR% + +echo. +echo ====================================================== +echo Testing complete! \ No newline at end of file diff --git a/v3/tasks/release/test_simple.sh b/v3/tasks/release/test_simple.sh new file mode 100644 index 000000000..a04446367 --- /dev/null +++ b/v3/tasks/release/test_simple.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +echo "Testing release.go --create-release-notes functionality" +echo "======================================================" + +# Save current directory +ORIGINAL_DIR=$(pwd) + +# Go to v3 root (where UNRELEASED_CHANGELOG.md should be) +cd ../.. + +# Create a test UNRELEASED_CHANGELOG.md +cat > UNRELEASED_CHANGELOG.md << 'EOF' +# Unreleased Changes + + + +## Added + +- Add Windows dark theme support for menus and menubar +- Add `--create-release-notes` flag to release script + +## Changed + +- Update Go version to 1.23 in workflow +- Improve error handling in release process + +## Fixed + +- Fix nightly release workflow changelog extraction +- Fix Go cache configuration in GitHub Actions + +## Deprecated + + +## Removed + + +## Security + + +--- + +### Example Entries: + +**Added:** +- Example content +EOF + +echo "" +echo "Test 1: Running with valid content" +echo "-----------------------------------" + +# Run the release script +cd tasks/release +if go run release.go --create-release-notes; then + echo "βœ… Command succeeded" + + # Check if release_notes.md was created + if [ -f "../../release_notes.md" ]; then + echo "βœ… release_notes.md was created" + echo "" + echo "Content:" + echo "--------" + cat ../../release_notes.md + echo "" + echo "--------" + else + echo "❌ release_notes.md was NOT created" + fi +else + echo "❌ Command failed" +fi + +echo "" +echo "Test 2: Check --check-only flag" +echo "--------------------------------" + +# Test the check-only flag +if go run release.go --check-only; then + echo "βœ… --check-only detected content" +else + echo "❌ --check-only did not detect content" +fi + +echo "" +echo "Test 3: Check --extract-changelog flag" +echo "--------------------------------------" + +# Test the extract-changelog flag +OUTPUT=$(go run release.go --extract-changelog 2>&1) +if [ $? -eq 0 ]; then + echo "βœ… --extract-changelog succeeded" + echo "Output:" + echo "-------" + echo "$OUTPUT" + echo "-------" +else + echo "❌ --extract-changelog failed" + echo "Error: $OUTPUT" +fi + +# Clean up +cd ../.. +rm -f release_notes.md + +# Restore original UNRELEASED_CHANGELOG.md if it exists +if [ -f "UNRELEASED_CHANGELOG.md.backup" ]; then + mv UNRELEASED_CHANGELOG.md.backup UNRELEASED_CHANGELOG.md +fi + +cd "$ORIGINAL_DIR" + +echo "" +echo "======================================================" +echo "Testing complete!" \ No newline at end of file diff --git a/v3/test/docker/Dockerfile.linux-arm64 b/v3/test/docker/Dockerfile.linux-arm64 new file mode 100644 index 000000000..fb6e4054b --- /dev/null +++ b/v3/test/docker/Dockerfile.linux-arm64 @@ -0,0 +1,185 @@ +# Build Linux binaries for Wails v3 examples using Ubuntu 24.04 (ARM64 native) +FROM ubuntu:24.04 + +# Avoid interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install system dependencies for Wails v3 (ARM64 native) +RUN apt-get update && apt-get install -y \ + --no-install-recommends \ + # Core build tools for ARM64 + build-essential \ + gcc \ + g++ \ + pkg-config \ + # GTK and WebKit dependencies for Ubuntu 24.04 + libgtk-3-dev \ + libwebkit2gtk-4.1-dev \ + # Additional dependencies that might be needed + libayatana-appindicator3-dev \ + # Git for go mod operations + git \ + # CA certificates for HTTPS + ca-certificates \ + # wget for downloading Go + wget \ + # Clean up apt cache and lists + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# Install Go 1.24 ARM64 version +ENV GO_VERSION=1.24.0 +RUN cd /tmp && \ + wget https://go.dev/dl/go${GO_VERSION}.linux-arm64.tar.gz && \ + wget https://go.dev/dl/go${GO_VERSION}.linux-arm64.tar.gz.sha256 && \ + echo "$(cat go${GO_VERSION}.linux-arm64.tar.gz.sha256) go${GO_VERSION}.linux-arm64.tar.gz" | sha256sum -c - && \ + tar -C /usr/local -xzf go${GO_VERSION}.linux-arm64.tar.gz && \ + rm go${GO_VERSION}.linux-arm64.tar.gz go${GO_VERSION}.linux-arm64.tar.gz.sha256 + +# Set Go environment for ARM64 native compilation +ENV PATH="/usr/local/go/bin:${PATH}" +ENV GOPATH="/go" +ENV PATH="${GOPATH}/bin:${PATH}" +ENV CGO_ENABLED=1 +ENV GOOS=linux +ENV GOARCH=arm64 + +# Set working directory +WORKDIR /build + +# Copy the entire v3 directory structure +COPY . /build/ + +# Create build script for ARM64 native compilation +# hadolint ignore=DL1000 +RUN cat > /build/build-linux-arm64.sh << 'EOF' +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "🐧 Building Wails v3 examples for Linux ARM64 (Ubuntu 24.04)" +echo "Go version: $(go version)" +echo "Architecture: $(uname -m)" +echo "Build directory: $(pwd)" +echo "" + +# Function to build a single example +build_example() { + local example_name="$1" + local example_dir="/build/examples/$example_name" + + if [ ! -d "$example_dir" ]; then + echo -e "${RED}βœ—${NC} Example directory $example_name not found" + return 1 + fi + + if [ ! -f "$example_dir/main.go" ]; then + echo -e "${YELLOW}⚠${NC} Skipping $example_name (no main.go)" + return 0 + fi + + echo -e "Building ${YELLOW}$example_name${NC} for ARM64..." + cd "$example_dir" + + # Update go.mod for Docker environment + if [ -f go.mod ]; then + go mod edit -dropreplace github.com/wailsapp/wails/v3 2>/dev/null || true + go mod edit -replace github.com/wailsapp/wails/v3=/build + fi + + # Tidy dependencies + echo " Running go mod tidy..." + if ! go mod tidy; then + echo -e "${RED}βœ—${NC} go mod tidy failed for $example_name" + return 1 + fi + + # Build the example for ARM64 + echo " Compiling for ARM64..." + if go build -o "testbuild-$example_name-linux-arm64"; then + echo -e "${GREEN}βœ“${NC} Successfully built $example_name for ARM64" + ls -la "testbuild-$example_name-linux-arm64" + return 0 + else + echo -e "${RED}βœ—${NC} Build failed for $example_name" + return 1 + fi +} + +# Main execution +if [ $# -eq 0 ]; then + # Build all examples + echo "Building all examples for ARM64..." + cd /build/examples + + failed_examples=() + successful_examples=() + skipped_examples=() + + for example_dir in */; do + example_name=$(basename "$example_dir") + + if build_example "$example_name"; then + if [ -f "/build/examples/$example_name/testbuild-$example_name-linux-arm64" ]; then + successful_examples+=("$example_name") + else + skipped_examples+=("$example_name") + fi + else + failed_examples+=("$example_name") + fi + + echo "" + done + + echo "==============================" + echo "πŸ—οΈ ARM64 BUILD SUMMARY" + echo "==============================" + echo -e "${GREEN}βœ“ Successful builds (${#successful_examples[@]}):${NC}" + for example in "${successful_examples[@]}"; do + echo " $example" + done + + if [ ${#skipped_examples[@]} -gt 0 ]; then + echo -e "${YELLOW}⚠ Skipped (${#skipped_examples[@]}):${NC}" + for example in "${skipped_examples[@]}"; do + echo " $example (no main.go)" + done + fi + + if [ ${#failed_examples[@]} -gt 0 ]; then + echo -e "${RED}βœ— Failed builds (${#failed_examples[@]}):${NC}" + for example in "${failed_examples[@]}"; do + echo " $example" + done + fi + + echo "" + echo "Total: ${#successful_examples[@]} successful, ${#failed_examples[@]} failed, ${#skipped_examples[@]} skipped" + + if [ ${#failed_examples[@]} -eq 0 ]; then + echo -e "${GREEN}πŸŽ‰ All buildable examples compiled successfully for ARM64!${NC}" + exit 0 + else + echo -e "${RED}❌ Some examples failed to build for ARM64${NC}" + exit 1 + fi + +else + # Build specific example + example_name="$1" + echo "Building specific example for ARM64: $example_name" + build_example "$example_name" +fi +EOF + +# Make the script executable +RUN chmod +x /build/build-linux-arm64.sh + +# Default command +CMD ["/build/build-linux-arm64.sh"] \ No newline at end of file diff --git a/v3/test/docker/Dockerfile.linux-x86_64 b/v3/test/docker/Dockerfile.linux-x86_64 new file mode 100644 index 000000000..b3a1dcf80 --- /dev/null +++ b/v3/test/docker/Dockerfile.linux-x86_64 @@ -0,0 +1,185 @@ +# Build Linux binaries for Wails v3 examples using Ubuntu 24.04 (x86_64 native) +FROM --platform=linux/amd64 ubuntu:24.04 + +# Avoid interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install system dependencies for Wails v3 (x86_64 native) +RUN apt-get update && apt-get install -y \ + --no-install-recommends \ + # Core build tools for x86_64 + build-essential \ + gcc \ + g++ \ + pkg-config \ + # GTK and WebKit dependencies for Ubuntu 24.04 + libgtk-3-dev \ + libwebkit2gtk-4.1-dev \ + # Additional dependencies that might be needed + libayatana-appindicator3-dev \ + # Git for go mod operations + git \ + # CA certificates for HTTPS + ca-certificates \ + # wget for downloading Go + wget \ + # Clean up apt cache and lists + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# Install Go 1.24 x86_64 version +ENV GO_VERSION=1.24.0 +RUN cd /tmp && \ + wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz && \ + wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz.sha256 && \ + echo "$(cat go${GO_VERSION}.linux-amd64.tar.gz.sha256) go${GO_VERSION}.linux-amd64.tar.gz" | sha256sum -c - && \ + tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz && \ + rm go${GO_VERSION}.linux-amd64.tar.gz go${GO_VERSION}.linux-amd64.tar.gz.sha256 + +# Set Go environment for x86_64 native compilation +ENV PATH="/usr/local/go/bin:${PATH}" +ENV GOPATH="/go" +ENV PATH="${GOPATH}/bin:${PATH}" +ENV CGO_ENABLED=1 +ENV GOOS=linux +ENV GOARCH=amd64 + +# Set working directory +WORKDIR /build + +# Copy the entire v3 directory structure +COPY . /build/ + +# Create build script for x86_64 native compilation +# hadolint ignore=DL1000 +RUN cat > /build/build-linux-x86_64.sh << 'EOF' +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "🐧 Building Wails v3 examples for Linux x86_64 (Ubuntu 24.04)" +echo "Go version: $(go version)" +echo "Architecture: $(uname -m)" +echo "Build directory: $(pwd)" +echo "" + +# Function to build a single example +build_example() { + local example_name="$1" + local example_dir="/build/examples/$example_name" + + if [ ! -d "$example_dir" ]; then + echo -e "${RED}βœ—${NC} Example directory $example_name not found" + return 1 + fi + + if [ ! -f "$example_dir/main.go" ]; then + echo -e "${YELLOW}⚠${NC} Skipping $example_name (no main.go)" + return 0 + fi + + echo -e "Building ${YELLOW}$example_name${NC} for x86_64..." + cd "$example_dir" + + # Update go.mod for Docker environment + if [ -f go.mod ]; then + go mod edit -dropreplace github.com/wailsapp/wails/v3 2>/dev/null || true + go mod edit -replace github.com/wailsapp/wails/v3=/build + fi + + # Tidy dependencies + echo " Running go mod tidy..." + if ! go mod tidy; then + echo -e "${RED}βœ—${NC} go mod tidy failed for $example_name" + return 1 + fi + + # Build the example for x86_64 + echo " Compiling for x86_64..." + if go build -o "testbuild-$example_name-linux-x86_64"; then + echo -e "${GREEN}βœ“${NC} Successfully built $example_name for x86_64" + ls -la "testbuild-$example_name-linux-x86_64" + return 0 + else + echo -e "${RED}βœ—${NC} Build failed for $example_name" + return 1 + fi +} + +# Main execution +if [ $# -eq 0 ]; then + # Build all examples + echo "Building all examples for x86_64..." + cd /build/examples + + failed_examples=() + successful_examples=() + skipped_examples=() + + for example_dir in */; do + example_name=$(basename "$example_dir") + + if build_example "$example_name"; then + if [ -f "/build/examples/$example_name/testbuild-$example_name-linux-x86_64" ]; then + successful_examples+=("$example_name") + else + skipped_examples+=("$example_name") + fi + else + failed_examples+=("$example_name") + fi + + echo "" + done + + echo "==============================" + echo "πŸ—οΈ x86_64 BUILD SUMMARY" + echo "==============================" + echo -e "${GREEN}βœ“ Successful builds (${#successful_examples[@]}):${NC}" + for example in "${successful_examples[@]}"; do + echo " $example" + done + + if [ ${#skipped_examples[@]} -gt 0 ]; then + echo -e "${YELLOW}⚠ Skipped (${#skipped_examples[@]}):${NC}" + for example in "${skipped_examples[@]}"; do + echo " $example (no main.go)" + done + fi + + if [ ${#failed_examples[@]} -gt 0 ]; then + echo -e "${RED}βœ— Failed builds (${#failed_examples[@]}):${NC}" + for example in "${failed_examples[@]}"; do + echo " $example" + done + fi + + echo "" + echo "Total: ${#successful_examples[@]} successful, ${#failed_examples[@]} failed, ${#skipped_examples[@]} skipped" + + if [ ${#failed_examples[@]} -eq 0 ]; then + echo -e "${GREEN}πŸŽ‰ All buildable examples compiled successfully for x86_64!${NC}" + exit 0 + else + echo -e "${RED}❌ Some examples failed to build for x86_64${NC}" + exit 1 + fi + +else + # Build specific example + example_name="$1" + echo "Building specific example for x86_64: $example_name" + build_example "$example_name" +fi +EOF + +# Make the script executable +RUN chmod +x /build/build-linux-x86_64.sh + +# Default command +CMD ["/build/build-linux-x86_64.sh"] \ No newline at end of file diff --git a/v3/tests/window-visibility-test/.gitignore b/v3/tests/window-visibility-test/.gitignore new file mode 100644 index 000000000..dc734a8b8 --- /dev/null +++ b/v3/tests/window-visibility-test/.gitignore @@ -0,0 +1,19 @@ +# Compiled binary +window-visibility-test +window-visibility-test.exe + +# Build artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binaries +*.test + +# Output of the go coverage tool +*.out + +# Go workspace file +go.work \ No newline at end of file diff --git a/v3/tests/window-visibility-test/README.md b/v3/tests/window-visibility-test/README.md new file mode 100644 index 000000000..04b4ec49e --- /dev/null +++ b/v3/tests/window-visibility-test/README.md @@ -0,0 +1,146 @@ +# Window Visibility Test - Issue #2861 + +This example demonstrates and tests the fixes implemented for [Wails v3 Issue #2861](https://github.com/wailsapp/wails/issues/2861) regarding application windows not showing on Windows 10 Pro due to efficiency mode. + +## Problem Background + +On Windows systems, the "efficiency mode" feature could prevent Wails applications from displaying windows properly. This occurred because: + +1. **WebView2 NavigationCompleted events** could be delayed or missed in efficiency mode +2. **Window visibility was gated** behind WebView2 navigation completion +3. **No fallback mechanisms** existed for delayed or failed navigation events + +## Solution Implemented + +The fix implements a **robust cross-platform window visibility pattern**: + +### Windows Improvements +- βœ… **Decouple window container from WebView state** - Windows show immediately +- βœ… **3-second timeout fallback** - Shows WebView if navigation is delayed +- βœ… **Efficiency mode prevention** - Sets WebView2 `IsVisible=true` per Microsoft guidance +- βœ… **Enhanced state tracking** - Proper visibility state management + +### Cross-Platform Consistency +- βœ… **macOS** - Already robust, documented best practices +- βœ… **Linux** - Added missing show/hide methods for both CGO and purego builds + +## Test Scenarios + +This example provides comprehensive testing for: + +### 1. **Basic Window Tests** +- **Normal Window**: Standard window creation - should appear immediately +- **Delayed Content Window**: Simulates heavy content loading (like Vue.js apps) +- **Hidden β†’ Show Test**: Tests delayed showing after initial creation + +### 2. **Stress Tests** +- **Multiple Windows**: Creates 3 windows simultaneously +- **Rapid Creation**: Creates windows in quick succession + +### 3. **Critical Issue #2861 Test** +- **Efficiency Mode Test**: Specifically designed to reproduce and verify the fix +- Tests window container vs content loading timing +- Includes heavy content simulation + +## How to Run + +```bash +cd /path/to/wails/v3/examples/window-visibility-test +wails dev +``` + +## Testing Instructions + +### What to Look For +1. **Immediate Window Appearance** - Windows should appear within 100ms of clicking buttons +2. **Progressive Loading** - Content may load progressively, but window container visible immediately +3. **No Efficiency Mode Issues** - Windows appear even if Task Manager shows "efficiency mode" +4. **Consistent Cross-Platform Behavior** - Similar behavior on Windows, macOS, and Linux + +### How to Test +1. **Note the current time** displayed in the app +2. **Click any test button** or use menu items +3. **Immediately observe** if a window appears (should be within 100ms) +4. **Wait for content** to load and check reported timing +5. **Try multiple tests** in sequence to test robustness +6. **Test both buttons and menu items** for comprehensive coverage + +### Expected Results +- βœ… Window containers appear immediately upon button click +- βœ… Content loads progressively within 2-3 seconds +- βœ… No blank or invisible windows, even under efficiency mode +- βœ… Activity log shows sub-100ms window creation times +- βœ… All test scenarios work consistently + +## Manual Testing Checklist + +### Windows 10 Pro (Primary Target) +- [ ] Test with efficiency mode enabled in Task Manager +- [ ] Create windows while system is under load +- [ ] Test rapid window creation scenarios +- [ ] Verify WebView2 content loads after container appears +- [ ] Check activity log for sub-100ms creation times + +### Windows 11 +- [ ] Verify consistent behavior with Windows 10 Pro fixes +- [ ] Test efficiency mode scenarios +- [ ] Validate timeout fallback mechanisms + +### macOS +- [ ] Confirm existing robust behavior maintained +- [ ] Test all window creation scenarios +- [ ] Verify no regressions introduced + +### Linux +- [ ] Test both CGO and purego builds +- [ ] Verify new show/hide methods work correctly +- [ ] Test window positioning and timing + +## Technical Implementation Details + +### Window Creation Flow +``` +1. User clicks button β†’ JavaScript calls Go backend +2. Go creates WebviewWindow β†’ Sets properties +3. Go calls window.Show() β†’ IMMEDIATE window container display +4. WebView2 starts navigation β†’ Progressive content loading +5. Timeout fallback ensures WebView shows even if navigation delayed +``` + +### Key Code Changes +- **Windows**: `/v3/pkg/application/webview_window_windows.go` +- **macOS**: `/v3/pkg/application/webview_window_darwin.go` +- **Linux**: `/v3/pkg/application/webview_window_linux.go`, `linux_cgo.go`, `linux_purego.go` + +## Reporting Test Results + +When testing, please report: + +1. **Platform & OS Version** (e.g., "Windows 10 Pro 21H2", "macOS 13.1", "Ubuntu 22.04") +2. **Window Creation Timing** (from activity log) +3. **Any Delayed or Missing Windows** +4. **Efficiency Mode Status** (Windows only - check Task Manager) +5. **Content Loading Behavior** (immediate container vs progressive content) +6. **Any Error Messages** in activity log or console + +### Sample Test Report Format +``` +Platform: Windows 10 Pro 21H2 +Efficiency Mode: Enabled +Results: +- Normal Window: βœ… Appeared immediately (<50ms) +- Delayed Content: βœ… Container immediate, content loaded in 2.1s +- Multiple Windows: βœ… All 3 appeared simultaneously +- Critical Test: βœ… Window appeared immediately, content progressive +Notes: No issues observed, all windows visible immediately +``` + +## Architecture Notes + +This example demonstrates the **preferred window visibility pattern** for web-based desktop applications: + +1. **Separate Concerns**: Window container vs web content readiness +2. **Immediate Feedback**: Users see window immediately +3. **Progressive Enhancement**: Content loads and appears when ready +4. **Robust Fallbacks**: Multiple strategies for edge cases +5. **Cross-Platform Consistency**: Same behavior on all platforms diff --git a/v3/tests/window-visibility-test/TESTING_GUIDE.md b/v3/tests/window-visibility-test/TESTING_GUIDE.md new file mode 100644 index 000000000..dbb3bfd04 --- /dev/null +++ b/v3/tests/window-visibility-test/TESTING_GUIDE.md @@ -0,0 +1,159 @@ +# Testing Guide - Window Visibility Issue #2861 + +## Quick Start + +1. **Build and run the application:** + ```bash + cd v3/examples/window-visibility-test + ./build.sh + # OR + wails dev + ``` + +2. **Main testing interface:** + - The app opens with a comprehensive testing dashboard + - Contains multiple test scenarios accessible via buttons + - Also provides menu-based testing (File, Tests, Help menus) + - Real-time activity logging with precise timing + +## Critical Test Cases + +### 🎯 **Issue #2861 Reproduction Test** (Most Important) +**Button:** "Efficiency Mode Test" +**Expected:** Window container appears immediately, content loads progressively +**Watch for:** +- Window visible within 100ms of button click +- Content loading message appears initially +- Content completes loading after 2-3 seconds +- No blank or invisible windows + +### ⏳ **Delayed Content Simulation** +**Button:** "Create Delayed Content Window" +**Expected:** Tests navigation completion timing +**Watch for:** +- Window container appears immediately +- Loading spinner visible initially +- Content loads after 3-second delay +- Window remains visible throughout + +### πŸ”„ **Hidden β†’ Show Robustness** +**Button:** "Hidden β†’ Show Test" +**Expected:** Tests delayed show() calls +**Watch for:** +- Initial response in activity log +- Window appears after exactly 2 seconds +- No timing issues or failures + +## Platform-Specific Testing + +### Windows 10 Pro (Primary Target) +**Enable Efficiency Mode Testing:** +1. Open Task Manager β†’ Processes tab +2. Find the test application process +3. Right-click β†’ "Efficiency mode" (if available) +4. Run all test scenarios +5. Verify windows still appear immediately + +**Key Metrics:** +- Window creation: < 100ms +- Content loading: 2-3 seconds +- No invisible windows under any conditions + +### Windows 11 +**Similar to Windows 10 Pro but also test:** +- New Windows 11 efficiency features +- Multiple monitor scenarios +- High DPI scaling + +### macOS +**Focus on consistency:** +- All scenarios should work identical to Windows +- No regressions in existing robust behavior +- Test across different macOS versions if possible + +### Linux +**Test both build variants:** +```bash +# CGO build (default) +wails dev + +# Purego build +CGO_ENABLED=0 wails dev +``` +- Verify both variants behave identically +- Test across different Linux distributions + +## Success Criteria + +### βœ… **Pass Conditions** +- All windows appear within 100ms of button click +- Activity log shows consistent sub-100ms timing +- Content loads progressively without blocking window visibility +- No blank, invisible, or delayed windows under any test scenario +- Efficiency mode (Windows) does not prevent window appearance +- Menu and button testing yield identical results + +### ❌ **Fail Conditions** +- Any window takes >200ms to appear +- Blank or invisible windows under any condition +- Window visibility blocked by content loading +- Efficiency mode prevents window appearance +- Inconsistent behavior between test methods +- Platform-specific failures + +## Reporting Results + +**Please provide this information:** + +``` +Platform: [Windows 10 Pro/Windows 11/macOS/Linux distro + version] +Build Type: [CGO/Purego] (Linux only) +Efficiency Mode: [Enabled/Disabled/N/A] (Windows only) + +Test Results: +- Normal Window: [βœ… Pass / ❌ Fail] - [timing in ms] +- Delayed Content: [βœ… Pass / ❌ Fail] - [container timing / content timing] +- Hiddenβ†’Show: [βœ… Pass / ❌ Fail] - [notes] +- Multiple Windows: [βœ… Pass / ❌ Fail] - [notes] +- Efficiency Mode Test: [βœ… Pass / ❌ Fail] - [critical timing results] + +Notes: +[Any additional observations, error messages, or unexpected behavior] +``` + +## Advanced Testing Scenarios + +### **Rapid Stress Testing** +1. Click "Rapid Creation Test" multiple times quickly +2. Use keyboard shortcuts to rapidly access menu items +3. Create multiple windows then close them rapidly +4. Test system under load (other applications running) + +### **Edge Case Testing** +1. Test during system startup (high load) +2. Test with multiple monitors +3. Test with different DPI scaling settings +4. Test while other WebView2 applications are running + +### **Timing Verification** +1. Use browser dev tools (F12) to check console timing +2. Compare activity log timing with system clock +3. Test on slower/older hardware if available +4. Verify timing consistency across multiple runs + +## Troubleshooting + +### **Common Issues** +- **Blank window**: Check activity log for error messages +- **Slow timing**: Verify system isn't under heavy load +- **Build failures**: Ensure Wails v3 CLI is latest version +- **Import errors**: Run `go mod tidy` in example directory + +### **Debug Information** +The application provides extensive logging: +- Browser console (F12) shows JavaScript timing +- Activity log shows backend call timing +- Go application logs show window creation details +- Check system Task Manager for process efficiency mode status + +This comprehensive testing should validate that the window visibility fixes successfully resolve issue #2861 across all supported platforms. \ No newline at end of file diff --git a/v3/tests/window-visibility-test/assets/index.html b/v3/tests/window-visibility-test/assets/index.html new file mode 100644 index 000000000..22023a5e3 --- /dev/null +++ b/v3/tests/window-visibility-test/assets/index.html @@ -0,0 +1,431 @@ + + + + + + Window Visibility Test - Issue #2861 + + + +
+
+

πŸͺŸ Window Visibility Test

+

Testing fixes for Wails v3 Issue #2861 - Windows 10 Pro Efficiency Mode

+
+ Current Time: --:--:-- +
+
+ +
+

βœ…Basic Window Tests

+

These tests verify that windows appear immediately when requested, regardless of WebView content loading state.

+
+ + + +
+
+ +
+

⚑Stress Tests

+

These tests push the window system to verify robustness under load and timing edge cases.

+
+ + +
+
+ +
+

πŸ”¬Critical Issue #2861 Test

+

This test specifically targets the Windows efficiency mode bug where WebView2 NavigationCompleted events could be delayed or missed.

+ +
+

Expected Behavior:

+
    +
  • Window container appears immediately upon button click
  • +
  • Content loads progressively (may take 2-3 seconds)
  • +
  • No blank or invisible windows, even under efficiency mode
  • +
+
+ +
+ +
+
+ +
+

πŸ“ŠTest Results & Timing

+

Monitor window creation timing and behavior. Each button click should result in immediate window visibility.

+ +
+

Activity Log

+
+
Ready for testing... Click any button to begin.
+
+
+
+ +
+

πŸ“Testing Instructions

+
+

What to Look For:

+
    +
  • Immediate Window Appearance: Windows should appear within 100ms of clicking
  • +
  • Progressive Loading: Content may load progressively, but window container should be visible immediately
  • +
  • No Efficiency Mode Issues: On Windows, windows should still appear even if Task Manager shows "efficiency mode"
  • +
  • Consistent Behavior: All platforms (Windows, macOS, Linux) should behave similarly
  • +
+
+ +
+

How to Test:

+
    +
  1. Note the current time displayed above
  2. +
  3. Click any test button
  4. +
  5. Immediately observe if a window appears (should be within 100ms)
  6. +
  7. Wait for content to load and check the reported timing
  8. +
  9. Try multiple tests in sequence
  10. +
  11. Test both buttons and menu items
  12. +
+
+
+
+ + + + \ No newline at end of file diff --git a/v3/tests/window-visibility-test/go.mod b/v3/tests/window-visibility-test/go.mod new file mode 100644 index 000000000..04d244468 --- /dev/null +++ b/v3/tests/window-visibility-test/go.mod @@ -0,0 +1,55 @@ +module window-visibility-test + +go 1.24.0 + +toolchain go1.24.4 + +replace github.com/wailsapp/wails/v3 => ../../ + +require github.com/wailsapp/wails/v3 v3.0.0-alpha.0 + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/cloudflare/circl v1.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.13.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/lmittmann/tint v1.0.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/wailsapp/go-webview2 v1.0.21 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) + +// Add any other dependencies that might be needed +// These will be resolved when the user runs go mod tidy diff --git a/v3/tests/window-visibility-test/go.sum b/v3/tests/window-visibility-test/go.sum new file mode 100644 index 000000000..cbadfe003 --- /dev/null +++ b/v3/tests/window-visibility-test/go.sum @@ -0,0 +1,146 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= +github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= +github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= +github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= +github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v3/tests/window-visibility-test/main.go b/v3/tests/window-visibility-test/main.go new file mode 100644 index 000000000..247c1035c --- /dev/null +++ b/v3/tests/window-visibility-test/main.go @@ -0,0 +1,308 @@ +package main + +import ( + "embed" + "fmt" + "log" + "time" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +// WindowTestService provides methods for testing window visibility scenarios +type WindowTestService struct { + app *application.App +} + +// NewWindowTestService creates a new window test service +func NewWindowTestService() *WindowTestService { + return &WindowTestService{} +} + +// SetApp sets the application reference (internal method, not exposed to frontend) +func (w *WindowTestService) setApp(app *application.App) { + w.app = app +} + +// CreateNormalWindow creates a standard window - should show immediately +func (w *WindowTestService) CreateNormalWindow() string { + log.Println("Creating normal window...") + + w.app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Normal Window - Should Show Immediately", + Width: 600, + Height: 400, + X: 100, + Y: 100, + HTML: "Normal Window

βœ… Normal Window

This window should have appeared immediately after clicking the button.

Timestamp: " + time.Now().Format("15:04:05") + "

", + }) + + return "Normal window created" +} + +// CreateDelayedContentWindow creates a window with delayed content to test navigation timing +func (w *WindowTestService) CreateDelayedContentWindow() string { + log.Println("Creating delayed content window...") + + // Use HTML that will take time to load (simulates heavy Vue app) + delayedHTML := ` + + + Delayed Content + + + +

⏳ Delayed Content Window

+

This window tests navigation completion timing.

+
+

Loading... (simulates heavy content)

+ +

Window container should be visible immediately, even during load.

+ + ` + + w.app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Delayed Content Window - Test Navigation Timing", + Width: 600, + Height: 400, + X: 150, + Y: 150, + HTML: delayedHTML, + }) + + return "Delayed content window created" +} + +// CreateHiddenThenShowWindow creates a hidden window then shows it after delay +func (w *WindowTestService) CreateHiddenThenShowWindow() string { + log.Println("Creating hidden then show window...") + + window := w.app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Hidden Then Show Window - Test Show() Robustness", + Width: 600, + Height: 400, + X: 200, + Y: 200, + HTML: "Hidden Then Show

πŸ”„ Hidden Then Show Window

This window was created hidden and then shown after 2 seconds.

Should test the robustness of the show() method.

Created at: " + time.Now().Format("15:04:05") + "

", + Hidden: true, // Start hidden + }) + + // Show after 2 seconds to test delayed showing + go func() { + time.Sleep(2 * time.Second) + log.Println("Showing previously hidden window...") + window.Show() + }() + + return "Hidden window created, will show in 2 seconds" +} + +// CreateMultipleWindows creates multiple windows simultaneously to test performance +func (w *WindowTestService) CreateMultipleWindows() string { + log.Println("Creating multiple windows...") + + for i := 0; i < 3; i++ { + bgColors := []string{"#ff9a9e,#fecfef", "#a18cd1,#fbc2eb", "#fad0c4,#ffd1ff"} + content := fmt.Sprintf(` + + + Batch Window %d + + + +

πŸ”’ Batch Window %d

+

Part of multiple windows stress test

+

All windows should appear quickly and simultaneously

+

Created at: %s

+ + `, i+1, bgColors[i], i+1, time.Now().Format("15:04:05")) + + w.app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: fmt.Sprintf("Batch Window %d - Stress Test", i+1), + Width: 400, + Height: 300, + X: 250 + (i * 50), + Y: 250 + (i * 50), + HTML: content, + }) + } + + return "Created 3 windows simultaneously" +} + +// CreateEfficiencyModeTestWindow creates a window designed to trigger efficiency mode issues +func (w *WindowTestService) CreateEfficiencyModeTestWindow() string { + log.Println("Creating efficiency mode test window...") + + // Create content that might trigger efficiency mode or WebView2 delays + heavyHTML := ` + + + Efficiency Mode Test + + + +

⚑ Efficiency Mode Test Window

+

This window tests the fix for Windows 10 Pro efficiency mode issue #2861

+ +
+

Window Container Status

+

βœ… Window container is visible (this text proves it)

+
+ +
+

WebView2 Status

+

⏳ WebView2 navigation in progress...

+ +
+ +
+

Heavy Content (simulates Vue.js app)

+
+ Loading heavy content... +
+
+ + + + ` + + w.app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Efficiency Mode Test - Issue #2861 Reproduction", + Width: 700, + Height: 500, + X: 300, + Y: 300, + HTML: heavyHTML, + }) + + return "Efficiency mode test window created" +} + +// GetWindowCount returns the current number of windows +func (w *WindowTestService) GetWindowCount() int { + // This would need to be implemented based on the app's window tracking + // For now, return a placeholder + return 1 // Main window +} + +//go:embed assets/* +var assets embed.FS + +func main() { + // Create the service + service := NewWindowTestService() + + // Create application with menu + app := application.New(application.Options{ + Name: "Window Visibility Test", + Description: "Test application for window visibility robustness (Issue #2861)", + Services: []application.Service{ + application.NewService(service), + }, + Assets: application.AssetOptions{ + Handler: application.BundledAssetFileServer(assets), + }, + }) + + // Set app reference in service + service.setApp(app) + + // Create application menu + menu := app.NewMenu() + + // File menu + fileMenu := menu.AddSubmenu("File") + fileMenu.Add("New Normal Window").OnClick(func(ctx *application.Context) { + service.CreateNormalWindow() + }) + fileMenu.Add("New Delayed Content Window").OnClick(func(ctx *application.Context) { + service.CreateDelayedContentWindow() + }) + fileMenu.AddSeparator() + fileMenu.Add("Quit").OnClick(func(ctx *application.Context) { + app.Quit() + }) + + // Test menu + testMenu := menu.AddSubmenu("Tests") + testMenu.Add("Hidden Then Show Window").OnClick(func(ctx *application.Context) { + service.CreateHiddenThenShowWindow() + }) + testMenu.Add("Multiple Windows Stress Test").OnClick(func(ctx *application.Context) { + service.CreateMultipleWindows() + }) + testMenu.Add("Efficiency Mode Test").OnClick(func(ctx *application.Context) { + service.CreateEfficiencyModeTestWindow() + }) + + // Help menu + helpMenu := menu.AddSubmenu("Help") + helpMenu.Add("About").OnClick(func(ctx *application.Context) { + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "About Window Visibility Test", + Width: 500, + Height: 400, + X: 400, + Y: 300, + HTML: "About

Window Visibility Test

This application tests the fixes for Wails v3 issue #2861

Windows 10 Pro Efficiency Mode Fix

Tests window container vs WebView content visibility


Created for testing robust window visibility patterns

", + }) + }) + + app.Menu.Set(menu) + + // Create main window + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Window Visibility Test - Issue #2861", + Width: 800, + Height: 600, + X: 50, + Y: 50, + URL: "/index.html", + }) + + err := app.Run() + if err != nil { + log.Fatal(err) + } +} diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index d1b405813..c5f51220e 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Fixed locking issue on Windows when multiselect dialog returns an error. Fixed in [PR](https://github.com/wailsapp/wails/pull/4156) by @johannes-luebke + ## v2.9.1 - 2024-06-18 ### Fixed