diff --git a/.all-contributorsrc b/.all-contributorsrc index 753598d6c..ced3b155b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -158,7 +158,7 @@ ] }, { - "login": "codydbentley", + "login": "sircodemane", "name": "Cody Bentley", "avatar_url": "https://avatars.githubusercontent.com/u/6968902?v=4", "profile": "https://codybentley.dev/", diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 84b7cb6dc..9faf71704 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -70,7 +70,7 @@ body: validations: required: false - type: textarea - id: systemetails + id: systemdetails attributes: label: System Details description: Please add the output of `wails doctor`. 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 44bf5eef6..d73efffa8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,21 @@ + + + # Description Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. @@ -6,7 +24,7 @@ Fixes # (issue) ## Type of change -Please delete options that are not relevant. +Please select the option that is relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) @@ -20,6 +38,8 @@ Please describe the tests that you ran to verify your changes. Provide instructi - [ ] Windows - [ ] macOS - [ ] Linux + +If you checked Linux, please specify the distro and version. ## Test Configuration 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..3d7a86450 --- /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: + 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/build-and-test-v3.yml b/.github/workflows/build-and-test-v3.yml new file mode 100644 index 000000000..bfcef85a3 --- /dev/null +++ b/.github/workflows/build-and-test-v3.yml @@ -0,0 +1,201 @@ +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: + - name: Check if PR is approved + id: check + run: | + if [[ "${{ github.event.review.state }}" == "approved" || "${{ github.event.pull_request.approved }}" == "true" ]]; then + echo "approved=true" >> $GITHUB_OUTPUT + else + echo "approved=false" >> $GITHUB_OUTPUT + fi + + test_go: + name: Run Go Tests v3 + needs: check_approval + runs-on: ${{ matrix.os }} + if: github.base_ref == 'v3-alpha' + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + go-version: [1.24] + + 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 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-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 (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 + + 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] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + working-directory: v2/internal/frontend/runtime + + - name: Run tests + run: npm test + working-directory: v2/internal/frontend/runtime + + 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 + 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 Wails3 CLI + working-directory: v3 + run: | + task install + wails3 doctor + + - name: Generate template '${{ matrix.template }}' + run: | + mkdir -p ./test-${{ matrix.template }} + cd ./test-${{ matrix.template }} + wails3 init -n ${{ matrix.template }} -t ${{ matrix.template }} + cd ${{ matrix.template }} + 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/build-and-test.yml b/.github/workflows/build-and-test.yml index f208e446a..8fe647c6f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,7 +2,7 @@ name: Build + Test v2 on: push: - branches: [release/*, master] + branches: [release/*, master, bugfix/*] workflow_dispatch: jobs: @@ -12,21 +12,30 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - go-version: [1.18, 1.19] + os: [ubuntu-22.04, ubuntu-24.04, windows-latest, macos-latest] + go-version: ['1.22'] steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Install linux dependencies - if: matrix.os == 'ubuntu-latest' - run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config + - uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-22.04' + with: + packages: libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config + version: 1.0 + + - uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-24.04' + with: + packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config libegl1 + version: 1.0 - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} + cache-dependency-path: ./v2/go.sum - name: Run tests (mac) if: matrix.os == 'macos-latest' @@ -36,21 +45,26 @@ jobs: run: go test -v ./... - name: Run tests (!mac) - if: matrix.os != 'macos-latest' + if: matrix.os != 'macos-latest' && matrix.os != 'ubuntu-24.04' working-directory: ./v2 run: go test -v ./... + - name: Run tests (Ubuntu 24.04) + if: matrix.os == 'ubuntu-24.04' + working-directory: ./v2 + run: go test -v -tags webkit2_41 ./... + test_js: name: Run JS Tests if: github.repository == 'wailsapp/wails' runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x] + node-version: [20.x] steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 @@ -72,7 +86,7 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-22.04, windows-latest, macos-latest, ubuntu-24.04] template: [ svelte, @@ -89,15 +103,16 @@ jobs: vanilla-ts, plain, ] - go-version: [1.18, 1.19] + go-version: ['1.22'] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} + cache-dependency-path: ./v2/go.sum - name: Build Wails CLI run: | @@ -105,14 +120,41 @@ jobs: go install wails -help - - name: Install linux dependencies - if: matrix.os == 'ubuntu-latest' - run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config + - uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-22.04' + with: + packages: libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config + version: 1.0 - - name: Generate template '${{ matrix.template }}' +# - 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 + + - uses: awalsh128/cache-apt-pkgs-action@latest + if: matrix.os == 'ubuntu-24.04' + with: + packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config libegl1 + version: 1.0 + +# - 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 + + - name: Generate & Build template '${{ matrix.template }}' + if: matrix.os != 'ubuntu-24.04' run: | mkdir -p ./test-${{ matrix.template }} cd ./test-${{ matrix.template }} wails init -n ${{ matrix.template }} -t ${{ matrix.template }} -ci cd ${{ matrix.template }} wails build -v 2 + + - name: Generate & Build template '${{ matrix.template }}' (ubuntu-24.04) + if: matrix.os == 'ubuntu-24.04' + run: | + mkdir -p ./test-${{ matrix.template }} + cd ./test-${{ matrix.template }} + wails init -n ${{ matrix.template }} -t ${{ matrix.template }} -ci + cd ${{ matrix.template }} + wails build -v 2 -tags webkit2_41 + diff --git a/.github/workflows/build-cross-image.yml b/.github/workflows/build-cross-image.yml new file mode 100644 index 000000000..83b40f2be --- /dev/null +++ b/.github/workflows/build-cross-image.yml @@ -0,0 +1,423 @@ +name: Build Cross-Compiler Image + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch containing Dockerfile' + required: true + default: 'v3-alpha' + sdk_version: + description: 'macOS SDK version' + required: true + default: '14.5' + zig_version: + description: 'Zig version' + required: true + default: '0.14.0' + image_version: + description: 'Image version tag' + required: true + default: 'latest' + skip_tests: + description: 'Skip cross-compilation tests' + required: false + default: 'false' + type: boolean + push: + branches: + - v3-alpha + paths: + - 'v3/internal/commands/build_assets/docker/Dockerfile.cross' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: wailsapp/wails-cross + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + image_tag: ${{ steps.vars.outputs.image_version }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set build variables + id: vars + run: | + echo "sdk_version=${{ inputs.sdk_version || '14.5' }}" >> $GITHUB_OUTPUT + echo "zig_version=${{ inputs.zig_version || '0.14.0' }}" >> $GITHUB_OUTPUT + echo "image_version=${{ inputs.image_version || 'latest' }}" >> $GITHUB_OUTPUT + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest + type=raw,value=${{ steps.vars.outputs.image_version }} + type=raw,value=sdk-${{ steps.vars.outputs.sdk_version }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: v3/internal/commands/build_assets/docker + file: v3/internal/commands/build_assets/docker/Dockerfile.cross + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: | + ${{ steps.meta.outputs.labels }} + io.wails.zig.version=${{ steps.vars.outputs.zig_version }} + io.wails.sdk.version=${{ steps.vars.outputs.sdk_version }} + build-args: | + ZIG_VERSION=${{ steps.vars.outputs.zig_version }} + MACOS_SDK_VERSION=${{ steps.vars.outputs.sdk_version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Test cross-compilation for all platforms + test-cross-compile: + needs: build + if: ${{ inputs.skip_tests != 'true' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # Darwin targets (Zig + macOS SDK) - no platform emulation needed + - os: darwin + arch: arm64 + platform: "" + expected_file: "Mach-O 64-bit.*arm64" + - os: darwin + arch: amd64 + platform: "" + expected_file: "Mach-O 64-bit.*x86_64" + # Linux targets (GCC) - need platform to match architecture + - os: linux + arch: amd64 + platform: "linux/amd64" + expected_file: "ELF 64-bit LSB.*x86-64" + - os: linux + arch: arm64 + platform: "linux/arm64" + expected_file: "ELF 64-bit LSB.*ARM aarch64" + # Windows targets (Zig + mingw) - no platform emulation needed + - os: windows + arch: amd64 + platform: "" + expected_file: "PE32\\+ executable.*x86-64" + - os: windows + arch: arm64 + platform: "" + expected_file: "PE32\\+ executable.*Aarch64" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Set up QEMU + if: matrix.platform != '' + uses: docker/setup-qemu-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create test CGO project + run: | + mkdir -p test-project + cd test-project + + # Create a minimal CGO test program + cat > main.go << 'EOF' + package main + + /* + #include + + int add(int a, int b) { + return a + b; + } + */ + import "C" + import "fmt" + + func main() { + result := C.add(1, 2) + fmt.Printf("CGO test: 1 + 2 = %d\n", result) + } + EOF + + cat > go.mod << 'EOF' + module test-cgo + + go 1.21 + EOF + + - name: Build ${{ matrix.os }}/${{ matrix.arch }} (CGO) + run: | + cd test-project + PLATFORM_FLAG="" + if [ -n "${{ matrix.platform }}" ]; then + PLATFORM_FLAG="--platform ${{ matrix.platform }}" + fi + + docker run --rm $PLATFORM_FLAG \ + -v "$(pwd):/app" \ + -e APP_NAME="test-cgo" \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \ + ${{ matrix.os }} ${{ matrix.arch }} + + - name: Verify binary format + run: | + cd test-project/bin + ls -la + + # Find the built binary + if [ "${{ matrix.os }}" = "windows" ]; then + BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }}.exe 2>/dev/null || ls *.exe | head -1) + else + BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1) + fi + + echo "Binary: $BINARY" + FILE_OUTPUT=$(file "$BINARY") + echo "File output: $FILE_OUTPUT" + + # Verify the binary format matches expected + if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then + echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }}" + else + echo "❌ Binary format mismatch!" + echo "Expected pattern: ${{ matrix.expected_file }}" + echo "Got: $FILE_OUTPUT" + exit 1 + fi + + - name: Check library dependencies (Linux only) + if: matrix.os == 'linux' + run: | + cd test-project/bin + BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1) + + echo "## Library Dependencies for $BINARY" + echo "" + + # Use readelf to show dynamic dependencies + echo "### NEEDED libraries:" + readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)" + + # Verify expected libraries are linked + echo "" + echo "### Verifying required libraries..." + NEEDED=$(readelf -d "$BINARY" | grep NEEDED) + + MISSING="" + for lib in libwebkit2gtk-4.1.so libgtk-3.so libglib-2.0.so libc.so; do + if echo "$NEEDED" | grep -q "$lib"; then + echo "✅ $lib" + else + echo "❌ $lib MISSING" + MISSING="$MISSING $lib" + fi + done + + if [ -n "$MISSING" ]; then + echo "" + echo "ERROR: Missing required libraries:$MISSING" + exit 1 + fi + + # Test non-CGO builds (pure Go cross-compilation) + test-non-cgo: + needs: build + if: ${{ inputs.skip_tests != 'true' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - os: darwin + arch: arm64 + expected_file: "Mach-O 64-bit.*arm64" + - os: darwin + arch: amd64 + expected_file: "Mach-O 64-bit.*x86_64" + - os: linux + arch: amd64 + expected_file: "ELF 64-bit LSB" + - os: linux + arch: arm64 + expected_file: "ELF 64-bit LSB.*ARM aarch64" + - os: windows + arch: amd64 + expected_file: "PE32\\+ executable.*x86-64" + - os: windows + arch: arm64 + expected_file: "PE32\\+ executable.*Aarch64" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create test non-CGO project + run: | + mkdir -p test-project + cd test-project + + # Create a pure Go test program (no CGO) + cat > main.go << 'EOF' + package main + + import "fmt" + + func main() { + fmt.Println("Pure Go cross-compilation test") + } + EOF + + cat > go.mod << 'EOF' + module test-pure-go + + go 1.21 + EOF + + - name: Build ${{ matrix.os }}/${{ matrix.arch }} (non-CGO) + run: | + cd test-project + + # For non-CGO, we can use any platform since Go handles cross-compilation + # We set CGO_ENABLED=0 to ensure pure Go build + docker run --rm \ + -v "$(pwd):/app" \ + -e APP_NAME="test-pure-go" \ + -e CGO_ENABLED=0 \ + --entrypoint /bin/sh \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \ + -c "GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o bin/test-pure-go-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} ." + + - name: Verify binary format + run: | + cd test-project/bin + ls -la + + # Find the built binary + if [ "${{ matrix.os }}" = "windows" ]; then + BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}.exe" + else + BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}" + fi + + echo "Binary: $BINARY" + FILE_OUTPUT=$(file "$BINARY") + echo "File output: $FILE_OUTPUT" + + # Verify the binary format matches expected + if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then + echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)" + else + echo "❌ Binary format mismatch!" + echo "Expected pattern: ${{ matrix.expected_file }}" + echo "Got: $FILE_OUTPUT" + exit 1 + fi + + - name: Check library dependencies (Linux only) + if: matrix.os == 'linux' + run: | + cd test-project/bin + BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}" + + echo "## Library Dependencies for $BINARY (non-CGO)" + echo "" + + # Non-CGO builds should have minimal dependencies (just libc or statically linked) + echo "### NEEDED libraries:" + readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)" + + # Verify NO GTK/WebKit libraries (since CGO is disabled) + NEEDED=$(readelf -d "$BINARY" | grep NEEDED || true) + if echo "$NEEDED" | grep -q "libwebkit\|libgtk"; then + echo "❌ ERROR: Non-CGO binary should not link to GTK/WebKit!" + exit 1 + else + echo "✅ Confirmed: No GTK/WebKit dependencies (expected for non-CGO)" + fi + + # Summary job + test-summary: + needs: [build, test-cross-compile, test-non-cgo] + if: always() && inputs.skip_tests != 'true' + runs-on: ubuntu-latest + steps: + - name: Check test results + run: | + echo "## Cross-Compilation Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ needs.test-cross-compile.result }}" = "success" ]; then + echo "✅ **CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.test-non-cgo.result }}" = "success" ]; then + echo "✅ **Non-CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Non-CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Tested Platforms" >> $GITHUB_STEP_SUMMARY + echo "| Platform | Architecture | CGO | Non-CGO |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------------|-----|---------|" >> $GITHUB_STEP_SUMMARY + echo "| Darwin | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Darwin | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Linux | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Linux | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Windows | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Windows | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + + # Fail if any test failed + if [ "${{ needs.test-cross-compile.result }}" != "success" ] || [ "${{ needs.test-non-cgo.result }}" != "success" ]; then + echo "" + echo "❌ Some tests failed. Check the individual job logs for details." + exit 1 + fi diff --git a/.github/workflows/changelog-v3.yml b/.github/workflows/changelog-v3.yml new file mode 100644 index 000000000..688959b9e --- /dev/null +++ b/.github/workflows/changelog-v3.yml @@ -0,0 +1,216 @@ +name: Changelog Validation (v3) + +on: + pull_request: + branches: [ v3-alpha ] + paths: + - 'docs/src/content/docs/changelog.mdx' + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to validate' + required: true + type: string + +jobs: + validate: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + actions: write + + steps: + - name: Checkout PR 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 || github.token }} + + - name: Get REAL validation script from v3-alpha + run: | + echo "Fetching the REAL validation script from v3-alpha branch..." + git fetch origin v3-alpha + git checkout origin/v3-alpha -- v3/scripts/validate-changelog.go + + echo "Validation script fetched successfully:" + ls -la v3/scripts/ + + - 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 changelog modifications + id: changelog_check + run: | + echo "Checking PR #${{ steps.pr_info.outputs.pr_number }} for changelog changes" + git fetch origin ${{ steps.pr_info.outputs.base_ref }} + + if git diff --name-only origin/${{ steps.pr_info.outputs.base_ref }}..HEAD | grep -q "docs/src/content/docs/changelog.mdx"; then + echo "changelog_modified=true" >> $GITHUB_OUTPUT + echo "✅ Changelog was modified in this PR" + else + echo "changelog_modified=false" >> $GITHUB_OUTPUT + echo "ℹ️ Changelog was not modified - skipping validation" + fi + + - name: Get changelog diff + id: get_diff + if: steps.changelog_check.outputs.changelog_modified == 'true' + run: | + echo "Getting diff for changelog changes..." + git diff origin/${{ steps.pr_info.outputs.base_ref }}..HEAD docs/src/content/docs/changelog.mdx | grep "^+" | grep -v "^+++" | sed 's/^+//' > /tmp/pr_added_lines.txt + + echo "Lines added in this PR:" + cat /tmp/pr_added_lines.txt + echo "Total lines added: $(wc -l < /tmp/pr_added_lines.txt)" + + - name: Validate changelog + id: validate + if: steps.changelog_check.outputs.changelog_modified == 'true' + run: | + echo "Running changelog validation..." + cd v3/scripts + OUTPUT=$(go run validate-changelog.go ../../docs/src/content/docs/changelog.mdx /tmp/pr_added_lines.txt 2>&1) + echo "$OUTPUT" + + RESULT=$(echo "$OUTPUT" | grep "VALIDATION_RESULT=" | cut -d'=' -f2) + echo "result=$RESULT" >> $GITHUB_OUTPUT + + - name: Commit fixes + id: commit_fixes + if: steps.validate.outputs.result == 'fixed' + run: | + echo "Committing automatic fixes..." + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Check only the changelog file for changes + if git diff --quiet docs/src/content/docs/changelog.mdx; then + echo "No changes to commit" + echo "committed=false" >> $GITHUB_OUTPUT + else + # Ensure validation script doesn't get committed + echo "v3/scripts/validate-changelog.go" >> .git/info/exclude + # Get the correct branch name to push to + REPO_OWNER="wailsapp" # Always wailsapp for this repo + + if [ "${{ github.event_name }}" = "pull_request" ]; then + BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + else + # For manual workflow dispatch, get PR info + PR_INFO=$(gh pr view ${{ steps.pr_info.outputs.pr_number }} --json headRefName,headRepository) + BRANCH_NAME=$(echo "$PR_INFO" | jq -r '.headRefName') + HEAD_REPO=$(echo "$PR_INFO" | jq -r '.headRepository.name') + + echo "🔍 PR source branch: $BRANCH_NAME" + echo "🔍 Head repository: $HEAD_REPO" + + # Don't push if this is from a fork or if branch is v3-alpha (main branch) + if [ "$HEAD_REPO" != "wails" ] || [ "$BRANCH_NAME" = "v3-alpha" ]; then + echo "⚠️ Cannot push - either fork or direct v3-alpha branch. Manual fix required." + echo "committed=false" >> $GITHUB_OUTPUT + exit 0 + fi + fi + + echo "Pushing to branch: $BRANCH_NAME in repo: $REPO_OWNER" + + # Only commit the changelog changes, not the validation script + git add docs/src/content/docs/changelog.mdx + git commit -m "🤖 Fix changelog: move entries to Unreleased section" + + # Only push if running on the main wailsapp repository + if [ "${{ github.repository }}" = "wailsapp/wails" ]; then + # Pull latest changes and rebase our commit + git fetch origin $BRANCH_NAME + git rebase origin/$BRANCH_NAME + git push origin HEAD:$BRANCH_NAME + else + echo "⚠️ Running on fork (${{ github.repository }}). Skipping push - manual fix required." + echo "committed=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "committed=true" >> $GITHUB_OUTPUT + echo "✅ Changes committed and pushed" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get PR author for tagging + id: pr_author + if: steps.validate.outputs.result && github.event.inputs.pr_number + run: | + PR_AUTHOR=$(gh pr view ${{ steps.pr_info.outputs.pr_number }} --json author --jq '.author.login') + echo "author=$PR_AUTHOR" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Comment on PR + if: steps.validate.outputs.result && github.event.inputs.pr_number + uses: actions/github-script@v7 + with: + script: | + const result = '${{ steps.validate.outputs.result }}'; + const committed = '${{ steps.commit_fixes.outputs.committed }}'; + const author = '${{ steps.pr_author.outputs.author }}'; + + let message; + if (result === 'success') { + message = '## ✅ Changelog Validation Passed\n\nNo misplaced changelog entries detected.'; + } else if (result === 'fixed' && committed === 'true') { + message = '## 🔧 Changelog Updated\n\nMisplaced entries were automatically moved to the `[Unreleased]` section. The changes have been committed to this PR.'; + } else if (result === 'fixed' || result === 'cannot_fix' || result === 'error') { + // Read the fixed changelog content + const fs = require('fs'); + let fixedContent = ''; + try { + fixedContent = fs.readFileSync('docs/src/content/docs/changelog.mdx', 'utf8'); + } catch (error) { + fixedContent = 'Error reading fixed changelog content'; + } + + message = '## ⚠️ Changelog Validation Issue\\n\\n' + + '@' + author + ' Your PR contains changelog entries that were added to already-released versions. These need to be moved to the `[Unreleased]` section.\\n\\n' + + (committed === 'true' ? + '✅ **Auto-fix applied**: The changes have been automatically committed to this PR.' : + '❌ **Manual fix required**: Please apply the changes shown below manually.') + '\\n\\n' + + '
\\n' + + '📝 Click to see the corrected changelog content\\n\\n' + + '```mdx\\n' + + fixedContent + + '\\n```\\n\\n' + + '
\\n\\n' + + '**What happened?** \\n' + + 'The validation script detected that you added changelog entries to a version section that has already been released (like `v3.0.0-alpha.10`). All new entries should go in the `[Unreleased]` section under the appropriate category (`### Added`, `### Fixed`, etc.).\\n\\n' + + (committed !== 'true' ? '**Action needed:** Please copy the corrected content from above and replace your changelog file.' : ''); + } + + if (message) { + await github.rest.issues.createComment({ + issue_number: ${{ steps.pr_info.outputs.pr_number }}, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }); + } + + - name: Fail if validation failed + if: steps.validate.outputs.result == 'cannot_fix' || steps.validate.outputs.result == 'error' + run: | + echo "❌ Changelog validation failed" + exit 1 \ No newline at end of file diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 000000000..b5e8cfd4d --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 000000000..d300267f1 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + diff --git a/.github/workflows/generate-sponsor-image.yml b/.github/workflows/generate-sponsor-image.yml index 5f3006d7e..56548ab43 100644 --- a/.github/workflows/generate-sponsor-image.yml +++ b/.github/workflows/generate-sponsor-image.yml @@ -16,7 +16,7 @@ jobs: - name: Set Node uses: actions/setup-node@v2 with: - node-version: 16.x + node-version: 20.x - name: Update Sponsors run: cd scripts/sponsors && chmod 755 ./generate-sponsor-image.sh && ./generate-sponsor-image.sh @@ -25,11 +25,16 @@ jobs: SPONSORKIT_GITHUB_LOGIN: wailsapp - name: Create Pull Request - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v6 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..99159a2f5 --- /dev/null +++ b/.github/workflows/issue-triage-automation.yml @@ -0,0 +1,77 @@ +name: Issue Triage Automation + +on: + issues: + types: [opened] + +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..ae56ba7bc --- /dev/null +++ b/.github/workflows/nightly-release-v3.yml @@ -0,0 +1,210 @@ +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' + env: + WAILS_REPO_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }} + GITHUB_TOKEN: ${{ secrets.WAILS_REPO_TOKEN || github.token }} + run: | + cd v3/tasks/release + ARGS=() + if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then + ARGS+=(--dry-run) + fi + go run release.go "${ARGS[@]}" + + - name: Summary + if: always() + run: | + if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then + echo "## 🧪 DRY RUN Release Summary" >> $GITHUB_STEP_SUMMARY + else + echo "## 🚀 Nightly Release Summary" >> $GITHUB_STEP_SUMMARY + fi + echo "================================" >> $GITHUB_STEP_SUMMARY + + if [ -n "${{ steps.release.outputs.release_version }}" ]; then + echo "- **Version:** ${{ steps.release.outputs.release_version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Tag:** ${{ steps.release.outputs.release_tag }}" >> $GITHUB_STEP_SUMMARY + echo "- **Status:** ${{ steps.release.outcome == 'success' && '✅ Success' || '⚠️ Failed' }}" >> $GITHUB_STEP_SUMMARY + echo "- **Mode:** ${{ steps.release.outputs.release_dry_run == 'true' && '🧪 Dry Run' || '🚀 Live release' }}" >> $GITHUB_STEP_SUMMARY + if [ -n "${{ steps.release.outputs.release_url }}" ]; then + echo "- **Release URL:** ${{ steps.release.outputs.release_url }}" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Changelog" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.changelog_check.outputs.has_unreleased_content }}" == "true" ]; then + echo "✅ Unreleased changelog processed and reset." >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ No unreleased changelog content detected." >> $GITHUB_STEP_SUMMARY + fi + else + echo "- Release script did not run (skipped or failed before execution)." >> $GITHUB_STEP_SUMMARY + fi + diff --git a/.github/workflows/pr-master.yml b/.github/workflows/pr-master.yml new file mode 100644 index 000000000..c961b4434 --- /dev/null +++ b/.github/workflows/pr-master.yml @@ -0,0 +1,104 @@ +# Updated to ensure "Run Go Tests" runs for pull requests as expected. +# Key fix: the test_go job previously required github.event.review.state == 'approved' +# which only exists on pull_request_review events. That prevented the job from +# running for regular pull_request events (opened / synchronize / reopened). +# New logic: run tests for pull_request events, and also allow running when a +# pull_request_review is submitted with state == 'approved'. +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - master + pull_request_review: + types: [submitted] + branches: + - master + workflow_dispatch: {} + +name: PR Checks (master) + +jobs: + check_docs: + name: Check Docs + if: ${{ github.repository == 'wailsapp/wails' && github.base_ref == 'master' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Verify Changed files + uses: step-security/changed-files@3dbe17c78367e7d60f00d78ae6781a35be47b4a1 # v45.0.1 + id: verify-changed-files + with: + files: | + website/**/*.mdx + website/**/*.md + - name: Run step only when files change. + if: steps.verify-changed-files.outputs.files_changed != 'true' + run: | + echo "::warning::Feature branch does not contain any changes to the website." + + test_go: + name: Run Go Tests + runs-on: ${{ matrix.os }} + # Run when: + # - the event is a pull_request (opened/synchronize/reopened) OR + # - the event is a pull_request_review AND the review state is 'approved' + # plus other existing filters (not the update-sponsors branch, repo and base_ref) + if: > + github.repository == 'wailsapp/wails' && + github.base_ref == 'master' && + github.event.pull_request.head.ref != 'update-sponsors' && + ( + github.event_name == 'pull_request' || + (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') + ) + strategy: + matrix: + os: [ubuntu-22.04, windows-latest, macos-latest, ubuntu-24.04] + go-version: ['1.23'] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - 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) + 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 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + + - name: Run tests (mac) + if: matrix.os == 'macos-latest' + env: + CGO_LDFLAGS: -framework UniformTypeIdentifiers -mmacosx-version-min=10.13 + working-directory: ./v2 + run: go test -v ./... + + - name: Run tests (!mac) + if: matrix.os != 'macos-latest' && matrix.os != 'ubuntu-24.04' + working-directory: ./v2 + run: go test -v ./... + + - name: Run tests (Ubuntu 24.04) + if: matrix.os == 'ubuntu-24.04' + working-directory: ./v2 + run: go test -v -tags webkit2_41 ./... + + # This job will run instead of test_go for the update-sponsors branch + skip_tests: + name: Skip Tests (Sponsor Update) + if: github.event.pull_request.head.ref == 'update-sponsors' + runs-on: ubuntu-latest + steps: + - name: Skip tests for sponsor updates + run: | + echo "Skipping tests for sponsor update branch" + echo "This is an automated update of the sponsors image." + continue-on-error: true diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index 6db750b73..000000000 --- a/.github/workflows/pr.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: PR Checks - -on: - pull_request: - pull_request_review: - types: [submitted] - -jobs: - check_docs: - name: Check Docs - if: ${{github.repository == 'wailsapp/wails' && contains(github.head_ref,'feature/')}} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Verify Changed files - uses: tj-actions/verify-changed-files@v11.1 - id: verify-changed-files - with: - files: | - website/**/*.mdx - website/**/*.md - - - name: Run step only when files change. - if: steps.verify-changed-files.outputs.files_changed != 'true' - run: | - echo "::warning::Feature branch does not contain any changes to the website." - - test_go: - name: Run Go Tests - runs-on: ${{ matrix.os }} - if: github.event.review.state == 'approved' - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - go-version: [1.18, 1.19] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install linux dependencies - if: matrix.os == 'ubuntu-latest' - run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config - - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: ${{ matrix.go-version }} - - - name: Run tests (mac) - if: matrix.os == 'macos-latest' - env: - CGO_LDFLAGS: -framework UniformTypeIdentifiers -mmacosx-version-min=10.13 - working-directory: ./v2 - run: go test -v ./... - - - name: Run tests (!mac) - if: matrix.os != 'macos-latest' - working-directory: ./v2 - run: go test -v ./... diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 000000000..a59818660 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,25 @@ +on: + workflow_dispatch: {} + pull_request: {} + push: + branches: + - main + - master + - v3-alpha + paths: + - .github/workflows/semgrep.yml + schedule: + # random HH:MM to avoid a load spike on GitHub Actions at 00:00 + - cron: 14 16 * * * +name: Semgrep +jobs: + semgrep: + name: semgrep/ci + runs-on: ubuntu-24.04 + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + container: + image: returntocorp/semgrep + steps: + - uses: actions/checkout@v3 + - run: semgrep ci 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/sync-translated-documents.yml b/.github/workflows/sync-translated-documents.yml index 770b76124..0aa06f11e 100644 --- a/.github/workflows/sync-translated-documents.yml +++ b/.github/workflows/sync-translated-documents.yml @@ -15,7 +15,7 @@ jobs: - name: Setup Nodejs uses: actions/setup-node@v2 with: - node-version: 18.x + node-version: 20.x - name: Install Task uses: arduino/setup-task@v1 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/unreleased-changelog-trigger.yml b/.github/workflows/unreleased-changelog-trigger.yml new file mode 100644 index 000000000..8cfe85de0 --- /dev/null +++ b/.github/workflows/unreleased-changelog-trigger.yml @@ -0,0 +1,129 @@ +name: Auto Release on Changelog Update + +on: + push: + branches: + - v3-alpha + paths: + - 'v3/UNRELEASED_CHANGELOG.md' + workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no actual release)' + required: false + default: false + type: boolean + +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 + + trigger-release: + name: Trigger v3-alpha Release + permissions: + contents: read + actions: write + runs-on: ubuntu-latest + needs: check-permissions + if: needs.check-permissions.outputs.authorized == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: v3-alpha + fetch-depth: 0 + token: ${{ secrets.WAILS_REPO_TOKEN || github.token }} + + - name: Check for unreleased changelog content + id: changelog_check + run: | + echo "🔍 Checking UNRELEASED_CHANGELOG.md for content..." + + cd v3 + # Check if UNRELEASED_CHANGELOG.md has actual content beyond the template + if [ -f "UNRELEASED_CHANGELOG.md" ]; then + # Use a simple check for actual content (bullet points starting with -) + CONTENT_LINES=$(grep -E "^\s*-\s+[^[:space:]]" UNRELEASED_CHANGELOG.md | wc -l) + if [ "$CONTENT_LINES" -gt 0 ]; then + echo "✅ Found $CONTENT_LINES content lines in UNRELEASED_CHANGELOG.md" + echo "has_content=true" >> $GITHUB_OUTPUT + else + echo "ℹ️ No actual content found in UNRELEASED_CHANGELOG.md" + echo "has_content=false" >> $GITHUB_OUTPUT + fi + else + echo "❌ UNRELEASED_CHANGELOG.md not found" + echo "has_content=false" >> $GITHUB_OUTPUT + fi + + - name: Trigger nightly release workflow + if: steps.changelog_check.outputs.has_content == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.WAILS_REPO_TOKEN || github.token }} + script: | + const response = await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'nightly-release-v3.yml', + ref: 'v3-alpha', + inputs: { + force_release: 'true', + dry_run: '${{ github.event.inputs.dry_run || "false" }}' + } + }); + + console.log('🚀 Successfully triggered nightly release workflow'); + console.log(`Workflow dispatch response status: ${response.status}`); + + // Create a summary + core.summary + .addHeading('🚀 Auto Release Triggered') + .addRaw('The v3-alpha release workflow has been automatically triggered due to changes in UNRELEASED_CHANGELOG.md') + .addTable([ + [{data: 'Trigger', header: true}, {data: 'Value', header: true}], + ['Repository', context.repo.repo], + ['Branch', 'v3-alpha'], + ['Actor', context.actor], + ['Dry Run', '${{ github.event.inputs.dry_run || "false" }}'], + ['Force Release', 'true'] + ]) + .addRaw('\n---\n*This release was automatically triggered by the unreleased-changelog-trigger workflow*') + .write(); + + - name: No content found + if: steps.changelog_check.outputs.has_content == 'false' + run: | + echo "ℹ️ No content found in UNRELEASED_CHANGELOG.md, skipping release trigger" + echo "## ℹ️ No Release Triggered" >> $GITHUB_STEP_SUMMARY + echo "**Reason:** UNRELEASED_CHANGELOG.md does not contain actual changelog content" >> $GITHUB_STEP_SUMMARY + echo "**Action:** No release workflow was triggered" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "To trigger a release, add actual changelog entries to the UNRELEASED_CHANGELOG.md file." >> $GITHUB_STEP_SUMMARY + + - name: Unauthorized user + if: needs.check-permissions.outputs.authorized == 'false' + run: | + echo "❌ User ${{ github.actor }} is not authorized to trigger releases" + echo "## ❌ Unauthorized Release Attempt" >> $GITHUB_STEP_SUMMARY + echo "**User:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY + echo "**Action:** Release trigger was blocked due to insufficient permissions" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Only authorized users can trigger automatic releases via changelog updates." >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/upload-source-documents.yml b/.github/workflows/upload-source-documents.yml index c94245ba5..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@v35 + uses: step-security/changed-files@3dbe17c78367e7d60f00d78ae6781a35be47b4a1 # v45.0.1 with: files: | website/**/*.mdx @@ -25,7 +25,7 @@ jobs: - name: Setup Nodejs uses: actions/setup-node@v2 with: - node-version: 18.x + node-version: 20.x - name: Setup Task uses: arduino/setup-task@v1 diff --git a/.replit b/.replit new file mode 100644 index 000000000..619bd7227 --- /dev/null +++ b/.replit @@ -0,0 +1,8 @@ +modules = ["go-1.21", "web", "nodejs-20"] +run = "go run v2/cmd/wails/main.go" + +[nix] +channel = "stable-24_05" + +[deployment] +run = ["sh", "-c", "go run v2/cmd/wails/main.go"] diff --git a/README.de.md b/README.de.md new file mode 100644 index 000000000..5df35de5b --- /dev/null +++ b/README.de.md @@ -0,0 +1,160 @@ +

+
+

+ +

+Erschaffe Desktop Anwendungen mit Go & Web Technologien. +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) + + + +
+ +## Inhaltsverzeichnis + +- [Inhaltsverzeichnis](#inhaltsverzeichnis) +- [Einführung](#einführung) +- [Funktionen](#funktionen) + - [Roadmap](#roadmap) +- [Loslegen](#loslegen) +- [Sponsoren](#sponsoren) +- [FAQ](#faq) +- [Sterne Überblick](#sterne-überblick) +- [Mitwirkende](#mitwirkende) +- [Lizenz](#lizenz) +- [Inspiration](#inspiration) + +## Einführung + +Die herkömmliche Methode zur Bereitstellung von Web-Interfaces für Go ist über einen eingebauten Webserver. +Wails nutzt einen anderen Weg. Es kann sowohl Go-Code als auch ein Web-Frontend in eine einzige Datei bauen. +Beigelieferte Werkzeuge übernehmen die Projekterstellung, den Kompilierungsprozess und das bauen. +Du musst nur kreativ werden. + +## Funktionen + +- Nutze Standard Go für das Backend +- Nutze eine Frontend Technologie mit der du dich bereits auskennst um dein UI zu bauen. +- Erschaffe schnell und einfach Frontends mit vorgefertigten Vorlagen für deine Go-Programme +- Nutze Javascript um Go Methoden aufzurufen +- Automatisch generierte Typescript Definitionen für deine Go Strukturen und Methoden +- Native Dialoge und Menüs +- Native Dark-/Lightmode Unterstützung +- Unterstützt moderne Transluzenz- und Milchglaseffekte +- Vereinheitlichtes Eventsystem zwischen Go und Javascript +- Leistungsstarkes CLI-Tool zum einfachen erstellen und bauen von Projekten +- Multiplattformen +- Nutze native Render-Engines - _keine eingebetteten Browser_! + +### Roadmap + +Die Projekt Roadmap kann [hier](https://github.com/wailsapp/wails/discussions/1484) gefunden werden. Bitte lies diese +durch bevor du eine Idee vorschlägst + +## Loslegen + +Die Installationsinstruktionen sind auf der [offiziellen Website](https://wails.io/docs/gettingstarted/installation). + +## Sponsoren + +Dieses Projekt wird von diesen freundlichen Leuten und Firmen unterstützt: + + +

+ +

+ +## FAQ + +- Ist das eine Alternative zu Electron? + + Hängt von deinen Anforderungen ab. Wails wurde entwickelt um das Go-Programmieren leicht zu machen und effiziente + Desktop-Anwendungen zu erstellen oder ein Frontend zu einer bestehenden Anwendung hinzuzufügen. + Wails bietet native Elemente wie Dialoge und Menüs und könnte somit als eine leichte effiziente Electron-Alternative + betrachtet werden. + +- Für wen ist dieses projekt geeignet? + + Go Entwickler, die ein HTML/CSS/JS-Frontend in ihre Anwendung integrieren möchten, ohne einen Webserver zu erstellen und + einen Browser öffnen zu müssen, um dieses zu sehen + +- Wie kam es zu diesem Namen? + + Als ich WebView sah dachte ich "Was ich wirklich will, ist ein Werkzeug für die Erstellung von WebView Anwendungen so wie Rails für Ruby". + Also war es zunächst ein Wortspiel (Webview on Rails). Zufälligerweise ist es auch ein Homophon des englischen Namens des [Landes](https://en.wikipedia.org/wiki/Wales), aus dem ich komme. + Also ist es dabei geblieben. + +## Sterne Überblick + + + + + + Star History Chart + + + +## Mitwirkende + +Die Liste der Mitwirkenden wird zu groß für diese Readme. All die fantastischen Menschen, die zu diesem +Projekt beigetragen haben, haben [hier](https://wails.io/credits#contributors) ihre eigene Seite. + +## Lizenz + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Inspiration + +Dieses Projekt wurde hauptsächlich zu den folgenden Alben entwickelt + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.es.md b/README.es.md index e34267c19..277d1c1fd 100644 --- a/README.es.md +++ b/README.es.md @@ -25,7 +25,7 @@ Awesome - Discord + Discord
@@ -42,7 +42,8 @@ [English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · [한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · -[Русский](README.ru.md) +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 000000000..61230f353 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,144 @@ +

+
+

+ +

+ Créer des applications de bureau avec Go et les technologies Web. +
+
+
+ GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## Sommaire + +- [Sommaire](#sommaire) +- [Introduction](#introduction) +- [Fonctionnalités](#fonctionnalités) + - [Feuille de route](#feuille-de-route) +- [Démarrage](#démarrage) +- [Les sponsors](#les-sponsors) +- [Foire aux questions](#foire-aux-questions) +- [Les étoiles au fil du temps](#les-étoiles-au-fil-du-temps) +- [Les contributeurs](#les-contributeurs) +- [License](#license) +- [Inspiration](#inspiration) + +## Introduction + +La méthode traditionnelle pour fournir des interfaces web aux programmes Go consiste à utiliser un serveur web intégré. Wails propose une approche différente : il offre la possibilité d'intégrer à la fois le code Go et une interface web dans un seul binaire. Des outils sont fournis pour vous faciliter la tâche en gérant la création, la compilation et le regroupement des projets. Il ne vous reste plus qu'à faire preuve de créativité! + +## Fonctionnalités + +- Utiliser Go pour le backend +- Utilisez n'importe quelle technologie frontend avec laquelle vous êtes déjà familier pour construire votre interface utilisateur. +- Créez rapidement des interfaces riches pour vos programmes Go à l'aide de modèles prédéfinis. +- Appeler facilement des méthodes Go à partir de Javascript +- Définitions Typescript auto-générées pour vos structures et méthodes Go +- Dialogues et menus natifs +- Prise en charge native des modes sombre et clair +- Prise en charge des effets modernes de translucidité et de "frosted window". +- Système d'événements unifié entre Go et Javascript +- Outil puissant pour générer et construire rapidement vos projets +- Multiplateforme +- Utilise des moteurs de rendu natifs - _pas de navigateur intégré_ ! + +### Feuille de route + +La feuille de route du projet peut être consultée [ici](https://github.com/wailsapp/wails/discussions/1484). Veuillez consulter avant d'ouvrir une demande d'amélioration. + +## Démarrage + +Les instructions d'installation se trouvent sur le site [site officiel](https://wails.io/docs/gettingstarted/installation). + +## Les sponsors + +Ce projet est soutenu par ces personnes aimables et entreprises: + + +

+ +

+ +## Foire aux questions + +- S'agit-il d'une alternative à Electron ? + + Cela dépend de vos besoins. Il est conçu pour permettre aux programmeurs Go de créer facilement des applications de bureau légères ou d'ajouter une interface à leurs applications existantes. Wails offre des éléments natifs tels que des menus et des boîtes de dialogue, il peut donc être considéré comme une alternative légère à electron. + +- À qui s'adresse ce projet ? + + Les programmeurs Go qui souhaitent intégrer une interface HTML/JS/CSS à leurs applications, sans avoir à créer un serveur et à ouvrir un navigateur pour l'afficher. + +- Pourquoi ce nom ?? + + Lorsque j'ai vu WebView, je me suis dit : "Ce que je veux vraiment, c'est un outil pour construire une application WebView, un peu comme Rails l'est pour Ruby". Au départ, il s'agissait donc d'un jeu de mots (Webview on Rails). Il se trouve que c'est aussi un homophone du nom anglais du [Pays](https://en.wikipedia.org/wiki/Wales) d'où je viens. Il s'est donc imposé. + +## Les étoiles au fil du temps + +[![Graphique de l'histoire des étoiles](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) + +## Les contributeurs + +La liste des contributeurs devient trop importante pour le readme ! Toutes les personnes extraordinaires qui ont contribué à ce projet ont leur propre page [ici](https://wails.io/credits#contributors). + +## License + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Inspiration + +Ce projet a été principalement codé sur les albums suivants : + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.ja.md b/README.ja.md index d134d31e9..ffd9f8103 100644 --- a/README.ja.md +++ b/README.ja.md @@ -27,7 +27,7 @@ Awesome - Discord + Discord
@@ -44,7 +44,8 @@ [English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · [한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · -[Русский](README.ru.md) +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) @@ -54,17 +55,16 @@ - [目次](#目次) - [はじめに](#はじめに) - - [公式サイト](#公式サイト) - - [ロードマップ](#ロードマップ) - [特徴](#特徴) -- [スポンサー](#スポンサー) + - [ロードマップ](#ロードマップ) - [始め方](#始め方) +- [スポンサー](#スポンサー) - [FAQ](#faq) - [スター数の推移](#スター数の推移) - [コントリビューター](#コントリビューター) -- [特記事項](#特記事項) -- [スペシャルサンクス](#スペシャルサンクス) - [ライセンス](#ライセンス) +- [インスピレーション](#インスピレーション) + ## はじめに @@ -72,44 +72,35 @@ Go プログラムにウェブインタフェースを提供する従来の方 Wails では Go のコードとウェブフロントエンドを単一のバイナリにまとめる機能を提供します。 また、プロジェクトの作成、コンパイル、ビルドを行うためのツールが提供されています。あなたがすべきことは創造性を発揮することです! -### 公式サイト - -Version 2: - -Wails v2 が 3 つのプラットフォームでベータ版としてリリースされました。興味のある方は[新しいウェブサイト](https://wails.io)をご覧ください。 - -レガシー版 v1: - -レガシー版 v1 のドキュメントは[https://wails.app](https://wails.app)で見ることができます。 - -### ロードマップ - -プロジェクトのロードマップは[こちら](https://github.com/wailsapp/wails/discussions/1484)になります。 -機能拡張のリクエストを出す前にご覧ください。 - ## 特徴 - バックエンドには Go を利用しています - 使い慣れたフロントエンド技術を利用して UI を構築できます -- あらかじめ用意されたテンプレートを利用することで、リッチなフロントエンドを備えた Go プログラムを作成できます +- あらかじめ用意されたテンプレートを利用することで、リッチなフロントエンドを備えた Go プログラムを素早く作成できます - JavaScript から Go のメソッドを簡単に呼び出すことができます - あなたの書いた Go の構造体やメソットに応じた TypeScript の定義が自動生成されます - ネイティブのダイアログとメニューが利用できます +- ネイティブなダーク/ライトモードをサポートします - モダンな半透明や「frosted window」エフェクトをサポートしています - Go と JavaScript 間で統一されたイベント・システムを備えています - プロジェクトを素早く生成して構築する強力な cli ツールを用意しています - マルチプラットフォームに対応しています - ネイティブなレンダリングエンジンを使用しています - _つまりブラウザを埋め込んでいるわけではありません!_ -## スポンサー +### ロードマップ -このプロジェクトは、以下の方々・企業によって支えられています。 - +プロジェクトのロードマップは[こちら](https://github.com/wailsapp/wails/discussions/1484)になります。 +機能拡張のリクエストを出す前にご覧ください。 ## 始め方 インストール方法は[公式サイト](https://wails.io/docs/gettingstarted/installation)に掲載されています。 +## スポンサー + +このプロジェクトは、以下の方々・企業によって支えられています。 + + ## FAQ - Electron の代替品になりますか? @@ -130,20 +121,18 @@ Wails v2 が 3 つのプラットフォームでベータ版としてリリー ## スター数の推移 -[![スター数の推移](https://starchart.cc/wailsapp/wails.svg)](https://starchart.cc/wailsapp/wails) +[![Star History Chart](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) ## コントリビューター 貢献してくれた方のリストが大きくなりすぎて、readme に入りきらなくなりました! このプロジェクトに貢献してくれた素晴らしい方々のページは[こちら](https://wails.io/credits#contributors)です。 -## 特記事項 +## ライセンス -このプロジェクトは以下の方々の協力がなければ、実現しなかったと思います。 +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) -- [Dustin Krysak](https://wiki.ubuntu.com/bashfulrobot) - 彼のサポートとフィードバックはとても大きいものでした。 -- [Serge Zaitsev](https://github.com/zserge) - Wails のウィンドウで使用している[Webview](https://github.com/zserge/webview)の作者です。 -- [Byron](https://github.com/bh90210) - 時には Byron が一人でこのプロジェクトを存続させてくれたこともありました。彼の素晴らしいインプットがなければ v1 に到達することはなかったでしょう。 +## インスピレーション プロジェクトを進める際に、以下のアルバムたちも支えてくれています。 @@ -161,20 +150,3 @@ Wails v2 が 3 つのプラットフォームでベータ版としてリリー - [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) - [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) -## スペシャルサンクス - -

-
- このプロジェクトを後援し、WailsをApple Siliconに移植する取り組みを支援してくれた Paceとても感謝しています!

- パワフルで素早く簡単に使えるプロジェクト管理ツールをお探しなら、ぜひチェックしてみてください!

-

- -

- ライセンスを提供していただいたJetBrains社に感謝します!

- ロゴをクリックして、感謝の気持ちを伝えてください!

- -

- -## ライセンス - -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) diff --git a/README.ko.md b/README.ko.md index 6285f9322..075e04229 100644 --- a/README.ko.md +++ b/README.ko.md @@ -27,7 +27,7 @@ Awesome - Discord + Discord
@@ -44,7 +44,8 @@ [English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · [한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · -[Русский](README.ru.md) +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) diff --git a/README.md b/README.md index 6277db3a8..5ab9309b4 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Awesome - Discord + Discord
@@ -42,7 +42,8 @@ [English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · [한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · -[Русский](README.ru.md) +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) @@ -86,7 +87,7 @@ make this easy for you by handling project creation, compilation and bundling. A ### Roadmap The project roadmap may be found [here](https://github.com/wailsapp/wails/discussions/1484). Please consult -this before open up an enhancement request. +it before creating an enhancement request. ## Getting Started @@ -97,9 +98,9 @@ The installation instructions are on the [official website](https://wails.io/doc This project is supported by these kind people / companies: -

- -

+## Powered By + +[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSource) ## FAQ @@ -122,7 +123,13 @@ This project is supported by these kind people / companies: ## Stargazers over time -[![Star History Chart](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) +
+ + + + Star History Chart + + ## Contributors diff --git a/README.pt-br.md b/README.pt-br.md index a61e62116..0e3883352 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -25,7 +25,7 @@ Awesome - Discord + Discord
@@ -41,7 +41,8 @@ [English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · -[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) diff --git a/README.ru.md b/README.ru.md index d6fcb1ee7..76fa59d07 100644 --- a/README.ru.md +++ b/README.ru.md @@ -25,7 +25,7 @@ Awesome - Discord + Discord
@@ -41,7 +41,8 @@ [English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · -[한국어](README.ko.md) · [Español](README.es.md) · [Русский](README.ru.md) +[한국어](README.ko.md) · [Español](README.es.md) · [Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) @@ -52,7 +53,7 @@ - [Содержание](#содержание) - [Вступление](#вступление) - [Особенности](#особенности) - - [Roadmap](#roadmap) + - [Roadmap](#roadmap) - [Быстрый старт](#быстрый-старт) - [Спонсоры](#спонсоры) - [FAQ](#faq) @@ -73,12 +74,12 @@ - Использование Go для backend - Поддержка любой frontend технологии, с которой вы уже знакомы для создания вашего UI - Быстрое создание frontend для ваших программ, используя готовые шаблоны -- Очень лёгкий вызов функция Go из JavaScript -- Автогене рация TypeScript типов для Go структур и функций +- Очень лёгкий вызов функций Go из JavaScript +- Автогенерация TypeScript типов для Go структур и функций - Нативные диалоги и меню - Нативная поддержка тёмной и светлой темы -- Поддержка современной прозрачности и эффекта "матового окна" -- Единая система Эвентов для Go и JavaScript +- Поддержка современных эффектов прозрачности и "матового окна" +- Единая система эвентов для Go и JavaScript - Мощный CLI для быстрого создания ваших проектов - Мультиплатформенность - Использование нативного движка рендеринга - нет встроенному браузеру! @@ -105,29 +106,28 @@ Roadmap проекта вы можете найти [здесь](https://github. - Это альтернатива Electron? - Зависит от ваших требований. Wails разработан для легкого создания Desktop приложений или расширения интерфейсной - части к своим существующим приложениям программистам Go. Wails действительно предлагает встроенные элементы, такие как - меню и диалоги, так что его можно считать облегченной альтернативой Electron. + Зависит от ваших требований. Wails разработан для легкого создания Desktop приложений или + расширения интерфейсной части существующих приложений для программистов на Go. Wails действительно + предлагает встроенные элементы, такие как меню и диалоги, так что его можно считать облегченной альтернативой Electron. -- Для кого нацелен этот проект? +- Для кого предназначен этот проект? - Для Golang программистов, которые хотят создавать приложение используя HTML JS и CSS, без создания Web сервера и - открытия браузера для его просмотра. + Для Golang программистов, которые хотят создавать приложения, используя HTML, JS и CSS, + без создания веб-сервера и открытия браузера для их просмотра. - Что это за название? Когда я увидел WebView, я подумал: "Что мне действительно нужно, так это инструменты для создания приложения WebView, - немного похожие на Rails для Ruby". Итак, изначально это была игра слов (Webview on Rails). Просто так получилось, что - это также омофон английского названия для [Страны](https://en.wikipedia.org/wiki/Wales) от куда я родом. Так что это - прижилось. + немного похожие на Rails для Ruby". Изначально это была игра слов (Webview on Rails). Просто так получилось, что это + также омофон английского названия для [Страны](https://en.wikipedia.org/wiki/Wales) от куда я родом. Так что это прижилось. -## График звёздочек репозитория, относительно времени +## График звёздочек репозитория по времени [![График звёзд](https://api.star-history.com/svg?repos=wailsapp/wails&type=Date)](https://star-history.com/#wailsapp/wails&Date) -## Контребьюторы +## Контрибьюторы -Список участников слишком большой для README! У всех замечательных людей, которые внесли свой вклад в этот +Список участников слишком велик для README! У всех замечательных людей, которые внесли свой вклад в этот проект, есть своя [страничка](https://wails.io/credits#contributors). ## Лицензия diff --git a/README.tr.md b/README.tr.md new file mode 100644 index 000000000..e9b16ca76 --- /dev/null +++ b/README.tr.md @@ -0,0 +1,156 @@ +

+
+

+ +

+ Go ve Web Teknolojilerini kullanarak masaüstü uygulamaları oluşturun. +
+
+
+ GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · +[Türkçe](README.tr.md) + + + +
+ +## İçerik + +- [İçerik](#içerik) +- [Giriş](#giriş) +- [Özellikler](#özellikler) + - [Yol Haritası](#yol-haritası) +- [Başlarken](#başlarken) +- [Sponsorlar](#sponsorlar) +- [Sıkça sorulan sorular](#sıkça-sorulan-sorular) +- [Zaman içinda yıldızlayanlar](#zaman-içinde-yıldızlayanlar) +- [Katkıda bulunanlar](#katkıda-bulunanlar) +- [Lisans](#lisans) +- [İlham](#ilham) + +## Giriş + +Go programlarına web arayüzleri sağlamak için geleneksel yöntem, yerleşik bir web sunucusu kullanmaktır. Wails, farklı bir yaklaşım sunar: Hem Go kodunu hem de bir web ön yüzünü tek bir ikili dosyada paketleme yeteneği sağlar. Proje oluşturma, derleme ve paketleme işlemlerini kolaylaştıran araçlar sunar. Tek yapmanız gereken yaratıcı olmaktır! + +## Özellikler + +- Backend için standart Go kullanın +- Kullanıcı arayüzünüzü oluşturmak için zaten aşina olduğunuz herhangi bir frontend teknolojisini kullanın +- Hazır şablonlar kullanarak Go programlarınız için hızlıca zengin ön yüzler oluşturun +- Javascript'ten Go metodlarını kolayca çağırın +- Go yapı ve metodlarınız için otomatik oluşturulan Typescript tanımları +- Yerel Diyaloglar ve Menüler +- Yerel Karanlık / Aydınlık mod desteği +- Modern saydamlık ve "buzlu cam" efektlerini destekler +- Go ve Javascript arasında birleşik olay sistemi +- Projelerinizi hızlıca oluşturmak ve derlemek için güçlü bir komut satırı aracı +- Çoklu platform desteği +- Yerel render motorlarını kullanır - _gömülü tarayıcı yok_! + + +### Yol Haritesı + +Proje yol haritasına [buradan](https://github.com/wailsapp/wails/discussions/1484) ulaşabilirsiniz. Lütfen bir iyileştirme talebi oluşturmadan önce danışın. + + +## Başlarken + +Kurulum talimatları [resmi web sitesinde](https://wails.io/docs/gettingstarted/installation) bulunmaktadır. + + +## Sponsorlar + +Bu proje, aşağıdaki nazik insanlar / şirketler tarafından desteklenmektedir: + + +

+ +

+ +## Sıkça Sorulan Sorular + +- Bu Electron'a alternatif mi? + + Gereksinimlerinize bağlıdır. Go programcılarının hafif masaüstü uygulamaları yapmasını veya mevcut uygulamalarına bir ön yüz eklemelerini kolaylaştırmak için tasarlanmıştır. Wails, menüler ve diyaloglar gibi yerel öğeler sunduğundan, hafif bir Electron alternatifi olarak kabul edilebilir. + +- Bu proje kimlere yöneliktir? + + HTML/JS/CSS ön yüzünü uygulamalarıyla birlikte paketlemek isteyen, ancak bir sunucu oluşturup bir tarayıcı açmaya başvurmadan bunu yapmak isteyen Go programcıları için. + +- İsmin anlamı nedir? + + WebView'i gördüğümde, "Aslında istediğim şey, WebView uygulaması oluşturmak için araçlar, biraz Rails'in Ruby için olduğu gibi" diye düşündüm. Bu nedenle başlangıçta kelime oyunu (Rails üzerinde Webview) olarak ortaya çıktı. Ayrıca, benim geldiğim [ülkenin](https://en.wikipedia.org/wiki/Wales) İngilizce adıyla homofon olması tesadüf oldu. Bu yüzden bu isim kaldı. + + +## Zaman içinda yıldızlayanlar + + + + + + Star History Chart + + + +## Katkıda Bulunanlar + +Katkıda bulunanların listesi, README için çok büyük hale geldi! Bu projeye katkıda bulunan tüm harika insanların kendi sayfaları [burada](https://wails.io/credits#contributors) bulunmaktadır. + + +## Lisans + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## İlham + +Bu proje esas olarak aşağıdaki albümler dinlenilerek kodlandı: + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) + diff --git a/README.uz.md b/README.uz.md new file mode 100644 index 000000000..807262405 --- /dev/null +++ b/README.uz.md @@ -0,0 +1,159 @@ +

+
+

+ +

+ Go va Web texnologiyalaridan foydalangan holda ish stoli ilovalarini yarating +
+
+ + GitHub + + + + + + Go Reference + + + CodeFactor + + + + + + Awesome + + + Discord + +
+ + Build + + + GitHub tag (latest SemVer pre-release) + +

+ +
+ + + +[English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · +[한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) + + + +
+ +## Tarkib + +- [Tarkib](#tarkib) +- [Kirish](#kirish) +- [Xususiyatlari](#xususiyatlari) + - [Yo'l xaritasi](#yol-xaritasi) +- [Ishni boshlash](#ishni-boshlash) +- [Homiylar](#homiylar) +- [FAQ](#faq) +- [Vaqt o'tishi bilan yulduzlar](#vaqt-otishi-bilan-yulduzlar) +- [Ishtirokchilar](#homiylar) +- [Litsenziya](#litsenziya) +- [Ilhomlanish](#ilhomlanish) + +## Kirish + +Odatda, Go dasturlari uchun veb-interfeyslar o'rnatilgan veb-server va veb-brauzerdir. +Walls boshqacha yondashuvni qo'llaydi: u Go kodini ham, veb-interfeysni ham bitta ikkilik (e.g: EXE)fayliga o'raydi. +Loyihalarni yaratish, kompilyatsiya qilish va birlashtirishni boshqarish orqali ilovangizni yaratishni osonlashtiradi. +Hamma narsa faqat sizning tasavvuringiz bilan cheklangan! + +## Xususiyatlari + +- Backend uchun standart Go dan foydalaning +- UI yaratish uchun siz allaqachon tanish bo'lgan har qanday frontend texnologiyasidan foydalaning +- Oldindan tayyorlangan shablonlardan foydalanib, Go dasturlaringiz uchun tezda boy frontendlarni yarating +- Javascriptdan Go methodlarini osongina chaqiring +- Go struktura va methodlari uchun avtomatik yaratilgan Typescript ta'riflari +- Mahalliy Dialoglar va Menyular +- Mahalliy Dark / Light rejimini qo'llab-quvvatlash +- Zamonaviy shaffoflik va "muzli oyna" effektlarini qo'llab-quvvatlaydi +- Go va Javascript o'rtasidagi yagona hodisa tizimi +- Loyihalaringizni tezda yaratish va qurish uchun kuchli cli vositasi +- Ko'p platformali +- Mahalliy renderlash mexanizmlaridan foydalanadi - _o'rnatilgan brauzer yo'q_! + +### Yo'l xaritasi + +Loyihaning yoʻl xaritasini [bu yerdan](https://github.com/wailsapp/wails/discussions/1484) topish mumkin. Iltimos, maslahatlashing +Buni yaxshilash so'rovini ochishdan oldin. + +## Ishni boshlash + +O'rnatish bo'yicha ko'rsatmalar [Rasmiy veb saytda](https://wails.io/docs/gettingstarted/installation) mavjud. + +## Homiylar + +Ushbu loyiha quyidagi mehribon odamlar / kompaniyalar tomonidan qo'llab-quvvatlanadi: + + +

+ +

+ +## FAQ + +- Bu Elektronga muqobilmi? + + Sizning talablaringizga bog'liq. Bu Go dasturchilariga yengil ish stoli yaratishni osonlashtirish uchun yaratilgan + ilovalar yoki ularning mavjud ilovalariga frontend qo'shing. Wails menyular kabi mahalliy elementlarni taklif qiladi + va dialoglar, shuning uchun uni yengil elektron muqobili deb hisoblash mumkin. + +- Ushbu loyiha kimlar uchun? + + Server yaratmasdan va uni ko'rish uchun brauzerni ochmasdan, o'z ilovalari bilan HTML/JS/CSS orqali frontendini birlashtirmoqchi bo'lgan dasturchilar uchun. + +- Bu qanday nom? + + Men WebViewni ko'rganimda, men shunday deb o'yladim: "Menga WebView ilovasini yaratish uchun vositalar kerak. + biroz Rails for Rubyga o'xshaydi." Demak, dastlab bu so'zlar ustida o'yin edi (Railsda Webview). Shunday bo'ldi. + u men kelgan [Mamlakat](https://en.wikipedia.org/wiki/Wales)ning inglizcha nomining omofonidir. + +## Vaqt o'tishi bilan yulduzlar + + + + + + Yulduzlar tarixi jadvali + + + +## Ishtirokchilar + +Ishtirokchilar roʻyxati oʻqish uchun juda kattalashib bormoqda! Bunga hissa qo'shgan barcha ajoyib odamlarning +loyihada o'z sahifasi bor [bu yerga](https://wails.io/credits#contributors). + +## Litsenziya + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwailsapp%2Fwails.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large) + +## Ilhomlanish + +Ushbu loyiha asosan quyidagi albomlar uchun kodlangan: + +- [Manic Street Preachers - Resistance Is Futile](https://open.spotify.com/album/1R2rsEUqXjIvAbzM0yHrxA) +- [Manic Street Preachers - This Is My Truth, Tell Me Yours](https://open.spotify.com/album/4VzCL9kjhgGQeKCiojK1YN) +- [The Midnight - Endless Summer](https://open.spotify.com/album/4Krg8zvprquh7TVn9OxZn8) +- [Gary Newman - Savage (Songs from a Broken World)](https://open.spotify.com/album/3kMfsD07Q32HRWKRrpcexr) +- [Steve Vai - Passion & Warfare](https://open.spotify.com/album/0oL0OhrE2rYVns4IGj8h2m) +- [Ben Howard - Every Kingdom](https://open.spotify.com/album/1nJsbWm3Yy2DW1KIc1OKle) +- [Ben Howard - Noonday Dream](https://open.spotify.com/album/6astw05cTiXEc2OvyByaPs) +- [Adwaith - Melyn](https://open.spotify.com/album/2vBE40Rp60tl7rNqIZjaXM) +- [Gwidaith Hen Fran - Cedors Hen Wrach](https://open.spotify.com/album/3v2hrfNGINPLuDP0YDTOjm) +- [Metallica - Metallica](https://open.spotify.com/album/2Kh43m04B1UkVcpcRa1Zug) +- [Bloc Party - Silent Alarm](https://open.spotify.com/album/6SsIdN05HQg2GwYLfXuzLB) +- [Maxthor - Another World](https://open.spotify.com/album/3tklE2Fgw1hCIUstIwPBJF) +- [Alun Tan Lan - Y Distawrwydd](https://open.spotify.com/album/0c32OywcLpdJCWWMC6vB8v) diff --git a/README.zh-Hans.md b/README.zh-Hans.md index bd1cb1dab..4c09d0c45 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -27,7 +27,7 @@ Awesome - Discord + Discord
@@ -44,7 +44,8 @@ [English](README.md) · [简体中文](README.zh-Hans.md) · [日本語](README.ja.md) · [한국어](README.ko.md) · [Español](README.es.md) · [Português](README.pt-br.md) · -[Русский](README.ru.md) +[Русский](README.ru.md) · [Francais](README.fr.md) · [Uzbek](README.uz.md) · [Deutsch](README.de.md) · +[Türkçe](README.tr.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..cb096f872 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,38 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 2.x.x | :white_check_mark: | +| 3.0.x-alpha | :x: | + + +## Reporting a Vulnerability + +If you believe you have found a security vulnerability in our project, we encourage you to let us know right away. +We will investigate all legitimate reports and do our best to quickly fix the problem. + +Before reporting though, please review our security policy below. + +### How to Report + +To report a security vulnerability, please use GitHub's [private vulnerability reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability) feature. If possible, please include as much information as possible. +This may include steps to reproduce, impact of the vulnerability, and anything else you believe would help us understand the problem. +**Please do not include any sensitive or personal information in your report**. + +### What to Expect + +When you report a vulnerability, here's what you can expect: + +- **Acknowledgement**: We will acknowledge your email within 48 hours, and you'll receive a more detailed response to your email within 72 hours indicating the next steps in handling your report. + +- **Updates**: After the initial reply to your report, our team will keep you informed of the progress being made towards a fix and full announcement. These updates will be sent at least once a week. + +- **Confidentiality**: We will maintain strict confidentiality of your report until the security issue is resolved. + +- **Issue Resolution**: If the issue is confirmed, we will release a patch as soon as possible depending on complexity of the fix. + +- **Recognition**: We recognize and appreciate every individual who helps us identify and fix vulnerabilities in our project. While we do not currently have a bounty program, we would be happy to publicly acknowledge your responsible disclosure. + +We strive to make Wails safe for everyone, and we greatly appreciate the assistance of security researchers and users in helping us identify and fix vulnerabilities. Thank you for your contribution to the security of this project. diff --git a/scripts/AUTOMATION-README.md b/scripts/AUTOMATION-README.md new file mode 100644 index 000000000..4096b1781 --- /dev/null +++ b/scripts/AUTOMATION-README.md @@ -0,0 +1,123 @@ +# Wails Issue Management Automation + +This directory contains automation workflows and scripts to help manage the Wails project with minimal time investment. + +## GitHub Workflow Files + +### 1. Auto-Label Issues (`auto-label-issues.yml`) +- Automatically labels issues and PRs based on their content and modified files +- Labels are defined in `issue-labeler.yml` and `file-labeler.yml` +- Activates when issues are opened, edited, or reopened + +### 2. Issue Triage Automation (`issue-triage-automation.yml`) +- Performs automated actions for issue triage +- Requests more info for incomplete bug reports +- Prioritizes security issues +- Adds issues to appropriate project boards + +## Configuration Files + +### 1. Issue Content Labeler (`issue-labeler.yml`) +- Defines patterns to match in issue title/body +- Categorizes by version (v2/v3), component, type, and priority +- Customize patterns as needed for your project + +### 2. File Path Labeler (`file-labeler.yml`) +- Labels PRs based on which files they modify +- Helps identify which areas of the codebase are affected +- Customize file patterns as needed + +### 3. Stale Issues Config (`stale.yml`) +- Marks issues as stale after 45 days of inactivity +- Closes stale issues after an additional 10 days +- Exempts issues with important labels + +## Helper Scripts + +### 1. Issue Triage Script (`scripts/issue-triage.ps1`) +- PowerShell script to quickly triage issues +- Lists recent issues needing attention +- Provides easy keyboard shortcuts for common actions +- Run during your dedicated issue triage time + +### 2. PR Review Helper (`scripts/pr-review-helper.ps1`) +- PowerShell script to efficiently review PRs +- Generates review checklists +- Provides easy shortcuts for common review actions +- Run during your dedicated PR review time + +## How to Use This System + +### Daily Workflow (2 hours max) + +**Monday (120 min):** +1. Run `scripts/issue-triage.ps1` (30 min) +2. Run `scripts/pr-review-helper.ps1` (30 min) +3. Check Discord for critical discussions (30 min) +4. Plan your week (30 min) + +**Tuesday-Wednesday (120 min/day):** +1. Quick check for urgent issues (10 min) +2. v3 development (110 min) + +**Thursday (120 min):** +1. v2 maintenance (90 min) +2. Documentation updates (30 min) + +**Friday (120 min):** +1. Run `scripts/pr-review-helper.ps1` (60 min) +2. Discord updates/newsletter (30 min) +3. Weekly reflection (30 min) + +## Installation + +1. The GitHub workflow files should be placed in `.github/workflows/` +2. Configuration files should be placed in `.github/` +3. Helper scripts should be placed in `scripts/` +4. Make sure you have GitHub CLI (`gh`) installed and authenticated + +## Customization + +Feel free to modify the configuration files and scripts to better suit your project's needs: + +1. **Adding New Label Categories**: + - Add new patterns to `issue-labeler.yml` for additional components or types + - Update `file-labeler.yml` if you add new directories or file types + +2. **Adjusting Automation Thresholds**: + - Modify `stale.yml` to change how long issues remain active + - Update `issue-triage-automation.yml` to change conditions for automated actions + +3. **Customizing Scripts**: + - Update the scripts with your specific GitHub username + - Add additional actions based on your workflow preferences + - Adjust time allocations based on which tasks need more attention + +## Benefits + +This automated issue management system will: + +1. **Save Time**: Reduce manual triage of most common issues +2. **Improve Consistency**: Apply the same categorization rules every time +3. **Increase Visibility**: Clear categorization helps community members find issues +4. **Focus Development**: Clearer separation of v2 and v3 work +5. **Reduce Backlog**: Better management of stale issues +6. **Streamline Reviews**: Faster PR processing with guided workflows + +## Requirements + +- GitHub CLI (`gh`) installed and authenticated +- PowerShell 5.1+ for Windows scripts +- GitHub Actions enabled on your repository +- Appropriate permissions to modify workflows + +## Maintenance + +This system requires minimal maintenance: + +- Periodically review and update label patterns as your project evolves +- Adjust time allocations based on where you need to focus +- Update scripts if GitHub CLI commands change +- Customize the workflow as you find pain points in your process + +Remember that the goal is to maximize your limited time (2 hours per day) by automating repetitive tasks and streamlining essential ones. diff --git a/scripts/issue-triage.ps1 b/scripts/issue-triage.ps1 new file mode 100644 index 000000000..6f6edd3ad --- /dev/null +++ b/scripts/issue-triage.ps1 @@ -0,0 +1,108 @@ +# issue-triage.ps1 - Script to help with quick issue triage +# Run this at the start of your GitHub time to quickly process issues + +# Set your GitHub username +$GITHUB_USERNAME = "your-username" + +# Get the latest 10 open issues that aren't assigned and aren't labeled as "awaiting feedback" +Write-Host "Fetching recent unprocessed issues..." +gh issue list --repo wailsapp/wails --limit 10 --json number,title,labels,assignees | Out-File -Encoding utf8 -FilePath "issues_temp.json" +$issues = Get-Content -Raw -Path "issues_temp.json" | ConvertFrom-Json +$newIssues = $issues | Where-Object { + $_.assignees.Count -eq 0 -and + ($_.labels.Count -eq 0 -or -not ($_.labels | Where-Object { $_.name -eq "awaiting feedback" })) +} + +# Process each issue +Write-Host "`n===== Issues Needing Triage =====`n" +foreach ($issue in $newIssues) { + $number = $issue.number + $title = $issue.title + $labelNames = $issue.labels | ForEach-Object { $_.name } + $labelsStr = if ($labelNames) { $labelNames -join ", " } else { "none" } + + Write-Host "Issue #$number`: $title" + Write-Host "Labels: $labelsStr`n" + + $continue = $true + while ($continue) { + Write-Host "Options:" + Write-Host " [v] View issue in browser" + Write-Host " [2] Add v2-only label" + Write-Host " [3] Add v3-alpha label" + Write-Host " [b] Add bug label" + Write-Host " [e] Add enhancement label" + Write-Host " [d] Add documentation label" + Write-Host " [w] Add webview2 label" + Write-Host " [f] Request more info (awaiting feedback)" + Write-Host " [c] Close issue (duplicate/invalid)" + Write-Host " [a] Assign to yourself" + Write-Host " [s] Skip to next issue" + Write-Host " [q] Quit script" + $action = Read-Host "Enter action" + + switch ($action) { + "v" { + gh issue view $number --repo wailsapp/wails --web + } + "2" { + Write-Host "Adding v2-only label..." + gh issue edit $number --repo wailsapp/wails --add-label "v2-only" + } + "3" { + Write-Host "Adding v3-alpha label..." + gh issue edit $number --repo wailsapp/wails --add-label "v3-alpha" + } + "b" { + Write-Host "Adding bug label..." + gh issue edit $number --repo wailsapp/wails --add-label "Bug" + } + "e" { + Write-Host "Adding enhancement label..." + gh issue edit $number --repo wailsapp/wails --add-label "Enhancement" + } + "d" { + Write-Host "Adding documentation label..." + gh issue edit $number --repo wailsapp/wails --add-label "Documentation" + } + "w" { + Write-Host "Adding webview2 label..." + gh issue edit $number --repo wailsapp/wails --add-label "webview2" + } + "f" { + Write-Host "Requesting more info..." + gh issue comment $number --repo wailsapp/wails --body "Thank you for reporting this issue. Could you please provide additional information to help us investigate?`n`n- [Specific details needed]`n`nThis will help us address your issue more effectively." + gh issue edit $number --repo wailsapp/wails --add-label "awaiting feedback" + } + "c" { + $reason = Read-Host "Reason for closing (duplicate/invalid/etc)" + gh issue comment $number --repo wailsapp/wails --body "Closing this issue: $reason" + gh issue close $number --repo wailsapp/wails + } + "a" { + Write-Host "Assigning to yourself..." + gh issue edit $number --repo wailsapp/wails --add-assignee "$GITHUB_USERNAME" + } + "s" { + Write-Host "Skipping to next issue..." + $continue = $false + } + "q" { + Write-Host "Exiting script." + exit + } + default { + Write-Host "Invalid option. Please try again." + } + } + + Write-Host "" + } + + Write-Host "--------------------------------`n" +} + +Write-Host "No more issues to triage!" + +# Clean up temp file +Remove-Item -Path "issues_temp.json" diff --git a/scripts/issue-triage.sh b/scripts/issue-triage.sh new file mode 100644 index 000000000..5809b43a1 --- /dev/null +++ b/scripts/issue-triage.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# issue-triage.sh - Script to help with quick issue triage +# Run this at the start of your GitHub time to quickly process issues + +# Set your GitHub username +GITHUB_USERNAME="your-username" + +# Get the latest 10 open issues that aren't assigned and aren't labeled as "awaiting feedback" +echo "Fetching recent unprocessed issues..." +gh issue list --repo wailsapp/wails --limit 10 --json number,title,labels,assignees --jq '.[] | select(.assignees | length == 0) | select(any(.labels[]; .name != "awaiting feedback"))' > new_issues.json + +# Process each issue +echo -e "\n===== Issues Needing Triage =====\n" +cat new_issues.json | jq -c '.[]' | while read -r issue; do + number=$(echo $issue | jq -r '.number') + title=$(echo $issue | jq -r '.title') + labels=$(echo $issue | jq -r '.labels[] | .name' 2>/dev/null | tr '\n' ', ' | sed 's/,$//') + + if [ -z "$labels" ]; then + labels="none" + fi + + echo -e "Issue #$number: $title" + echo -e "Labels: $labels\n" + + while true; do + echo "Options:" + echo " [v] View issue in browser" + echo " [2] Add v2-only label" + echo " [3] Add v3-alpha label" + echo " [b] Add bug label" + echo " [e] Add enhancement label" + echo " [d] Add documentation label" + echo " [w] Add webview2 label" + echo " [f] Request more info (awaiting feedback)" + echo " [c] Close issue (duplicate/invalid)" + echo " [a] Assign to yourself" + echo " [s] Skip to next issue" + echo " [q] Quit script" + read -p "Enter action: " action + + case $action in + v) + gh issue view $number --repo wailsapp/wails --web + ;; + 2) + echo "Adding v2-only label..." + gh issue edit $number --repo wailsapp/wails --add-label "v2-only" + ;; + 3) + echo "Adding v3-alpha label..." + gh issue edit $number --repo wailsapp/wails --add-label "v3-alpha" + ;; + b) + echo "Adding bug label..." + gh issue edit $number --repo wailsapp/wails --add-label "Bug" + ;; + e) + echo "Adding enhancement label..." + gh issue edit $number --repo wailsapp/wails --add-label "Enhancement" + ;; + d) + echo "Adding documentation label..." + gh issue edit $number --repo wailsapp/wails --add-label "Documentation" + ;; + w) + echo "Adding webview2 label..." + gh issue edit $number --repo wailsapp/wails --add-label "webview2" + ;; + f) + echo "Requesting more info..." + gh issue comment $number --repo wailsapp/wails --body "Thank you for reporting this issue. Could you please provide additional information to help us investigate?\n\n- [Specific details needed]\n\nThis will help us address your issue more effectively." + gh issue edit $number --repo wailsapp/wails --add-label "awaiting feedback" + ;; + c) + read -p "Reason for closing (duplicate/invalid/etc): " reason + gh issue comment $number --repo wailsapp/wails --body "Closing this issue: $reason" + gh issue close $number --repo wailsapp/wails + ;; + a) + echo "Assigning to yourself..." + gh issue edit $number --repo wailsapp/wails --add-assignee "$GITHUB_USERNAME" + ;; + s) + echo "Skipping to next issue..." + break + ;; + q) + echo "Exiting script." + exit 0 + ;; + *) + echo "Invalid option. Please try again." + ;; + esac + + echo "" + done + + echo -e "--------------------------------\n" +done + +echo "No more issues to triage!" diff --git a/scripts/pr-review-helper.ps1 b/scripts/pr-review-helper.ps1 new file mode 100644 index 000000000..75fae4c3b --- /dev/null +++ b/scripts/pr-review-helper.ps1 @@ -0,0 +1,152 @@ +# pr-review-helper.ps1 - Script to help with efficient PR reviews +# Run this during your PR review time + +# Set your GitHub username +$GITHUB_USERNAME = "your-username" + +# Get open PRs that are ready for review +Write-Host "Fetching PRs ready for review..." +gh pr list --repo wailsapp/wails --json number,title,author,labels,reviewDecision,additions,deletions,baseRefName,headRefName --limit 10 | Out-File -Encoding utf8 -FilePath "prs_temp.json" +$prs = Get-Content -Raw -Path "prs_temp.json" | ConvertFrom-Json + +# Process each PR +Write-Host "`n===== PRs Needing Review =====`n" +foreach ($pr in $prs) { + $number = $pr.number + $title = $pr.title + $author = $pr.author.login + $labels = if ($pr.labels) { $pr.labels | ForEach-Object { $_.name } | Join-String -Separator ", " } else { "none" } + $reviewState = if ($pr.reviewDecision) { $pr.reviewDecision } else { "PENDING" } + $baseRef = $pr.baseRefName + $headRef = $pr.headRefName + $changes = $pr.additions + $pr.deletions + + Write-Host "PR #$number`: $title" + Write-Host "Author: $author" + Write-Host "Labels: $labels" + Write-Host "Branch: $headRef -> $baseRef" + Write-Host "Changes: +$($pr.additions)/-$($pr.deletions) lines" + Write-Host "Review state: $reviewState`n" + + # Determine complexity based on size + $complexity = if ($changes -lt 50) { + "Quick review" + } elseif ($changes -lt 300) { + "Moderate review" + } else { + "Extensive review" + } + + Write-Host "Complexity: $complexity" + + $continue = $true + while ($continue) { + Write-Host "`nOptions:" + Write-Host " [v] View PR in browser" + Write-Host " [d] View diff in browser" + Write-Host " [c] Generate review checklist" + Write-Host " [a] Approve PR" + Write-Host " [r] Request changes" + Write-Host " [m] Add comment" + Write-Host " [l] Add labels" + Write-Host " [s] Skip to next PR" + Write-Host " [q] Quit script" + $action = Read-Host "Enter action" + + switch ($action) { + "v" { + gh pr view $number --repo wailsapp/wails --web + } + "d" { + gh pr diff $number --repo wailsapp/wails --web + } + "c" { + # Generate review checklist + $checklist = @" +## PR Review: $title + +### Basic Checks: +- [ ] PR title is descriptive +- [ ] PR description explains the changes +- [ ] Related issues are linked + +### Technical Checks: +- [ ] Code follows project style +- [ ] No unnecessary commented code +- [ ] Error handling is appropriate +- [ ] Documentation updated (if needed) +- [ ] Tests included (if needed) + +### Impact Assessment: +- [ ] Changes are backward compatible (if applicable) +- [ ] No breaking changes to public APIs +- [ ] Performance impact considered + +### Version Specific: +"@ + + if ($baseRef -eq "master") { + $checklist += @" + +- [ ] Appropriate for v2 maintenance +- [ ] No features that should be v3-only +"@ + } elseif ($baseRef -eq "v3-alpha") { + $checklist += @" + +- [ ] Appropriate for v3 development +- [ ] Aligns with v3 roadmap +"@ + } + + # Write to clipboard + $checklist | Set-Clipboard + Write-Host "`nReview checklist copied to clipboard!`n" + } + "a" { + $comment = Read-Host "Approval comment (blank for none)" + if ($comment) { + gh pr review $number --repo wailsapp/wails --approve --body $comment + } else { + gh pr review $number --repo wailsapp/wails --approve + } + } + "r" { + $comment = Read-Host "Feedback for changes requested" + gh pr review $number --repo wailsapp/wails --request-changes --body $comment + } + "m" { + $comment = Read-Host "Comment text" + gh pr comment $number --repo wailsapp/wails --body $comment + } + "l" { + $labels = Read-Host "Labels to add (comma-separated)" + $labelArray = $labels -split "," + foreach ($label in $labelArray) { + $labelTrimmed = $label.Trim() + if ($labelTrimmed) { + gh pr edit $number --repo wailsapp/wails --add-label $labelTrimmed + } + } + } + "s" { + Write-Host "Skipping to next PR..." + $continue = $false + } + "q" { + Write-Host "Exiting script." + exit + } + default { + Write-Host "Invalid option. Please try again." + } + } + } + + Write-Host "--------------------------------`n" +} + +Write-Host "No more PRs to review!" + +# Clean up temp file +Remove-Item -Path "prs_temp.json" diff --git a/scripts/sponsors/generate-sponsor-image.sh b/scripts/sponsors/generate-sponsor-image.sh index be90c8299..b034a0176 100755 --- a/scripts/sponsors/generate-sponsor-image.sh +++ b/scripts/sponsors/generate-sponsor-image.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -npm install sponsorkit@0.6.1 +npm install sponsorkit@16.4.2 npx sponsorkit -o ../../website/static/img/ diff --git a/scripts/sponsors/package-lock.json b/scripts/sponsors/package-lock.json index 5153e9cf8..2bb15b685 100644 --- a/scripts/sponsors/package-lock.json +++ b/scripts/sponsors/package-lock.json @@ -9,265 +9,469 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "sponsorkit": "^0.8.2" - } - }, - "node_modules/@antfu/utils": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.4.tgz", - "integrity": "sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "node_modules/ansi-escape-sequences": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz", - "integrity": "sha512-dzW9kHxH011uBsidTXd14JXgzye/YLb2LzeKZ4bsgl/Knwx8AtbSFkkGxagdNOoh0DlqHCmfiEjWKBaqjOanVw==", - "dependencies": { - "array-back": "^3.0.1" + "sponsorkit": "^16.5.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=22.0.0" } }, - "node_modules/ansi-escape-sequences/node_modules/array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "engines": { - "node": ">=4" - } - }, - "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==", + "node_modules/@emnapi/runtime": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", + "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "license": "MIT", + "optional": true, "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "tslib": "^2.4.0" } }, - "node_modules/array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dependencies": { - "typical": "^2.6.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmmirror.com/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmmirror.com/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, "dependencies": { - "streamsearch": "^1.1.0" + "@emnapi/runtime": "^1.4.4" }, "engines": { - "node": ">=10.16.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/camelcase": { + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@quansync/fs": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-0.1.3.tgz", + "integrity": "sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg==", + "license": "MIT", + "dependencies": { + "quansync": "^0.2.10" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ansis": { "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz", + "integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==", + "license": "ISC", "engines": { - "node": ">=4" + "node": ">=14" } }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "node_modules/cliss": { - "version": "0.0.2", - "resolved": "https://registry.npmmirror.com/cliss/-/cliss-0.0.2.tgz", - "integrity": "sha512-6rj9pgdukjT994Md13JCUAgTk91abAKrygL9sAvmHY4F6AKMOV8ccGaxhUUfcBuyg3sundWnn3JE0Mc9W6ZYqw==", - "dependencies": { - "command-line-usage": "^4.0.1", - "deepmerge": "^2.0.0", - "get-stdin": "^5.0.1", - "inspect-parameters-declaration": "0.0.9", - "object-to-arguments": "0.0.8", - "pipe-functions": "^1.3.0", - "strip-ansi": "^4.0.0", - "yargs-parser": "^7.0.0" - } - }, - "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==", - "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==", - "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==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "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==", + "license": "MIT", "engines": { "node": ">=8" } @@ -276,6 +480,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" @@ -288,6 +493,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -298,1160 +504,114 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/color-string": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/command-line-usage": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/command-line-usage/-/command-line-usage-4.1.0.tgz", - "integrity": "sha512-MxS8Ad995KpdAC0Jopo/ovGIroV/m0KHwzKfXxKag6FHOkGsH8/lv5yjgablcRxCJJC0oJeUMuO/gmaq+Wq46g==", - "dependencies": { - "ansi-escape-sequences": "^4.0.0", - "array-back": "^2.0.0", - "table-layout": "^0.4.2", - "typical": "^2.6.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, "node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmmirror.com/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmmirror.com/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deepmerge": { - "version": "2.2.1", - "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-2.2.1.tgz", - "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", - "engines": { - "node": ">=0.10.0" + "node": "^14.18.0 || >=16.10.0" } }, "node_modules/defu": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.2.tgz", - "integrity": "sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" }, "node_modules/destr": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/destr/-/destr-1.2.1.tgz", - "integrity": "sha512-ud8w0qMLlci6iFG7CNgeRr8OcbUWMsbfjtWft1eJ5Luqrz/M8Ebqk/KCzne8rKUlIQWWfLv0wD6QHrqOf4GshA==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "license": "MIT" }, "node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", "engines": { "node": ">=8" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" - } - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmmirror.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "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==" - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", - "engines": { - "node": ">=0.12" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/for-each-property": { - "version": "0.0.4", - "resolved": "https://registry.npmmirror.com/for-each-property/-/for-each-property-0.0.4.tgz", - "integrity": "sha512-xYs28PM0CKXETFzuGC6ZooH0voZlsSDZwidJcy92flQJi3PK7i3gZx23xHXCPOaD4zmet3bDo+wS7E7SujrlCw==", - "dependencies": { - "get-prototype-chain": "^1.0.1" - } - }, - "node_modules/for-each-property-deep": { - "version": "0.0.3", - "resolved": "https://registry.npmmirror.com/for-each-property-deep/-/for-each-property-deep-0.0.3.tgz", - "integrity": "sha512-qzP8QkODWVVRPpWiBZacSbBl67cTTWoBfxMG0wE46AsS1yl7qv05sGN+dHvD4s4tnvl/goe6Sp4qBI+rlVBgNg==", - "dependencies": { - "for-each-property": "0.0.4" - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmmirror.com/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/fs-extra": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz", - "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "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==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-prototype-chain": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/get-prototype-chain/-/get-prototype-chain-1.0.1.tgz", - "integrity": "sha512-2m7WZ0jveIg/dAbCbpUxEToaJ8Dmti5EkgDP8YM3UpHUT6SAORjE2odP8XQGNVGXMHi8q8cCCoy3HTByTaTVTw==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/get-stdin": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/get-stdin/-/get-stdin-5.0.1.tgz", - "integrity": "sha512-jZV7n6jGE3Gt7fgSTJoz91Ak5MuTLwMwkoYdjxuJ/AmjIsE1UC03y/IWkZCQGEvVNS9qoRNwy5BCqxImv0FVeA==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmmirror.com/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmmirror.com/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "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==", - "bin": { - "he": "bin/he" - } - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/image-data-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/image-data-uri/-/image-data-uri-2.0.1.tgz", - "integrity": "sha512-BZh721F2Q5TwBdwpiqrBrHEdj8daj8KuMZK/DOCyqQlz1CqFhhuZWbK5ZCUnAvFJr8LaKHTaWl9ja3/a3DC2Ew==", - "dependencies": { - "fs-extra": "^0.26.7", - "magicli": "0.0.8", - "mime-types": "^2.1.18", - "request": "^2.88.0" - }, - "bin": { - "image-data-uri": "bin/magicli.js" - } - }, - "node_modules/image-data-uri/node_modules/fs-extra": { - "version": "0.26.7", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-0.26.7.tgz", - "integrity": "sha512-waKu+1KumRhYv8D8gMRCKJGAMI9pRnPuEb1mvgYD0f7wBscg+h6bW4FDTmEZhB9VKxvoTtxW+Y7bnIlB7zja6Q==", - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" - } - }, - "node_modules/image-data-uri/node_modules/jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/inspect-function": { - "version": "0.3.4", - "resolved": "https://registry.npmmirror.com/inspect-function/-/inspect-function-0.3.4.tgz", - "integrity": "sha512-s0RsbJqK/sNZ+U1mykGoTickog3ea1A9Qk4mXniogOBu4PgkkZ56elScO7QC/r8D94lhGmJ2NyDI1ipOA/uq/g==", - "dependencies": { - "inspect-parameters-declaration": "0.0.8", - "magicli": "0.0.8", - "split-skip": "0.0.1", - "stringify-parameters": "0.0.4", - "unpack-string": "0.0.2" - }, - "bin": { - "inspect-function": "bin/magicli.js" - } - }, - "node_modules/inspect-function/node_modules/inspect-function": { - "version": "0.2.2", - "resolved": "https://registry.npmmirror.com/inspect-function/-/inspect-function-0.2.2.tgz", - "integrity": "sha512-becs5gzcHwPrlHawscYkyQ/ShiOiosrXPhA5RVZ3qyWH4aWdD52RnMfXq/dwQXciHwiieD8aIPwdIWYv6eL+sQ==", - "dependencies": { - "split-skip": "0.0.1", - "unpack-string": "0.0.2" - } - }, - "node_modules/inspect-function/node_modules/inspect-parameters-declaration": { - "version": "0.0.8", - "resolved": "https://registry.npmmirror.com/inspect-parameters-declaration/-/inspect-parameters-declaration-0.0.8.tgz", - "integrity": "sha512-W4QzN1LgFmasKOM+NoLlDd2OAZM3enNZlVUOXoGQKmYBDFgxoPDOyebF55ALaf8avyM9TavNwibXxg347RrzCg==", - "dependencies": { - "magicli": "0.0.5", - "split-skip": "0.0.2", - "stringify-parameters": "0.0.4", - "unpack-string": "0.0.2" - }, - "bin": { - "inspect-parameters-declaration": "bin/cli.js" - } - }, - "node_modules/inspect-function/node_modules/inspect-parameters-declaration/node_modules/magicli": { - "version": "0.0.5", - "resolved": "https://registry.npmmirror.com/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha512-wZbMtnl2v1b+Jp3xlqA9FU/O4I6YhGXR8xSY/eU2+gDAvut/F+W3gl4qs61iL4LELC7jqSAE6aAD5668EbmQHA==", - "dependencies": { - "commander": "^2.9.0", - "get-stdin": "^5.0.1", - "inspect-function": "^0.2.1", - "pipe-functions": "^1.2.0" - } - }, - "node_modules/inspect-function/node_modules/inspect-parameters-declaration/node_modules/split-skip": { - "version": "0.0.2", - "resolved": "https://registry.npmmirror.com/split-skip/-/split-skip-0.0.2.tgz", - "integrity": "sha512-weHOi8BolsDnGIwhhWHbA+wKSuSpvWwjRrdj8SdbIIis2vSwOE37CQP8x3EleuzxanUr3AK8BdUy4MkiOULPZg==" - }, - "node_modules/inspect-function/node_modules/split-skip": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/split-skip/-/split-skip-0.0.1.tgz", - "integrity": "sha512-7dkvq+gofI4M8zx4iZnEZ3O1s7FP4Y/iaIDHJh5RyWrs8idcPauFi2OZe3TBi36fLvR2j5z3kSzVtz6IhPdncQ==" - }, - "node_modules/inspect-parameters-declaration": { - "version": "0.0.9", - "resolved": "https://registry.npmmirror.com/inspect-parameters-declaration/-/inspect-parameters-declaration-0.0.9.tgz", - "integrity": "sha512-c3jrKKA1rwwrsjdGMAo2hFWV0vNe3/RKHxpE/OBt41LP3ynOVI1qmgxpZYK5SQu3jtWCyaho8L7AZzCjJ4mEUw==", - "dependencies": { - "magicli": "0.0.5", - "split-skip": "0.0.2", - "stringify-parameters": "0.0.4", - "unpack-string": "0.0.2" - }, - "bin": { - "inspect-parameters-declaration": "bin/cli.js" - } - }, - "node_modules/inspect-parameters-declaration/node_modules/inspect-function": { - "version": "0.2.2", - "resolved": "https://registry.npmmirror.com/inspect-function/-/inspect-function-0.2.2.tgz", - "integrity": "sha512-becs5gzcHwPrlHawscYkyQ/ShiOiosrXPhA5RVZ3qyWH4aWdD52RnMfXq/dwQXciHwiieD8aIPwdIWYv6eL+sQ==", - "dependencies": { - "split-skip": "0.0.1", - "unpack-string": "0.0.2" - } - }, - "node_modules/inspect-parameters-declaration/node_modules/inspect-function/node_modules/split-skip": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/split-skip/-/split-skip-0.0.1.tgz", - "integrity": "sha512-7dkvq+gofI4M8zx4iZnEZ3O1s7FP4Y/iaIDHJh5RyWrs8idcPauFi2OZe3TBi36fLvR2j5z3kSzVtz6IhPdncQ==" - }, - "node_modules/inspect-parameters-declaration/node_modules/magicli": { - "version": "0.0.5", - "resolved": "https://registry.npmmirror.com/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha512-wZbMtnl2v1b+Jp3xlqA9FU/O4I6YhGXR8xSY/eU2+gDAvut/F+W3gl4qs61iL4LELC7jqSAE6aAD5668EbmQHA==", - "dependencies": { - "commander": "^2.9.0", - "get-stdin": "^5.0.1", - "inspect-function": "^0.2.1", - "pipe-functions": "^1.2.0" - } - }, - "node_modules/inspect-property": { - "version": "0.0.6", - "resolved": "https://registry.npmmirror.com/inspect-property/-/inspect-property-0.0.6.tgz", - "integrity": "sha512-LgjHkRl9W6bj2n+kWrAOgvCYPTYt+LanE4rtd/vKNq6yEb+SvVV7UTLzoSPpDX6/U1cAz7VfqPr+lPAIz7wHaQ==", - "dependencies": { - "for-each-property": "0.0.4", - "for-each-property-deep": "0.0.3", - "inspect-function": "^0.3.1" + "url": "https://dotenvx.com" } }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, - "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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmmirror.com/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" }, "node_modules/jiti": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", - "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmmirror.com/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmmirror.com/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==", - "optionalDependencies": { - "graceful-fs": "^4.1.9" - } - }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lodash.padend": { - "version": "4.6.1", - "resolved": "https://registry.npmmirror.com/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/magicli": { - "version": "0.0.8", - "resolved": "https://registry.npmmirror.com/magicli/-/magicli-0.0.8.tgz", - "integrity": "sha512-x/eBenweAHF+DsYy172sK4doRxZl0yrJnfxhLJiN7H6hPM3Ya0PfI6uBZshZ3ScFFSQD7HXgBqMdbnXKEZsO1g==", - "dependencies": { - "cliss": "0.0.2", - "find-up": "^2.1.0", - "for-each-property": "0.0.4", - "inspect-property": "0.0.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" - }, - "node_modules/node-abi": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.33.0.tgz", - "integrity": "sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog==", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" - }, "node_modules/node-fetch-native": { - "version": "0.1.8", - "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-0.1.8.tgz", - "integrity": "sha512-ZNaury9r0NxaT2oL65GvdGDy+5PlSaHTovT6JV5tOW07k1TQmgC0olZETa4C9KZg0+6zBr99ctTYa3Utqj9P/Q==" + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz", + "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", + "license": "MIT" }, - "node_modules/node-html-parser": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.5.tgz", - "integrity": "sha512-fAaM511feX++/Chnhe475a0NHD8M7AxDInsqQpz6x63GRF7xYNdS8Vo5dKsIVPgsOvG7eioRRTZQnWBrhDHBSg==", + "node_modules/ofetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", + "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", + "license": "MIT", "dependencies": { - "css-select": "^5.1.0", - "he": "1.2.0" + "destr": "^2.0.3", + "node-fetch-native": "^1.6.4", + "ufo": "^1.5.4" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmmirror.com/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, - "node_modules/object-to-arguments": { - "version": "0.0.8", - "resolved": "https://registry.npmmirror.com/object-to-arguments/-/object-to-arguments-0.0.8.tgz", - "integrity": "sha512-BfWfuAwuhdH1bhMG5EG90WE/eckkBhBvnke8eSEkCDXoLE9Jk5JwYGTbCx1ehGwV48HvBkn62VukPBdlMUOY9w==", - "dependencies": { - "inspect-parameters-declaration": "0.0.10", - "magicli": "0.0.5", - "split-skip": "0.0.2", - "stringify-parameters": "0.0.4", - "unpack-string": "0.0.2" - }, - "bin": { - "object-to-arguments": "bin/cli.js" - } - }, - "node_modules/object-to-arguments/node_modules/inspect-function": { - "version": "0.2.2", - "resolved": "https://registry.npmmirror.com/inspect-function/-/inspect-function-0.2.2.tgz", - "integrity": "sha512-becs5gzcHwPrlHawscYkyQ/ShiOiosrXPhA5RVZ3qyWH4aWdD52RnMfXq/dwQXciHwiieD8aIPwdIWYv6eL+sQ==", - "dependencies": { - "split-skip": "0.0.1", - "unpack-string": "0.0.2" - } - }, - "node_modules/object-to-arguments/node_modules/inspect-function/node_modules/split-skip": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/split-skip/-/split-skip-0.0.1.tgz", - "integrity": "sha512-7dkvq+gofI4M8zx4iZnEZ3O1s7FP4Y/iaIDHJh5RyWrs8idcPauFi2OZe3TBi36fLvR2j5z3kSzVtz6IhPdncQ==" - }, - "node_modules/object-to-arguments/node_modules/inspect-parameters-declaration": { - "version": "0.0.10", - "resolved": "https://registry.npmmirror.com/inspect-parameters-declaration/-/inspect-parameters-declaration-0.0.10.tgz", - "integrity": "sha512-L8/Bvt9iDXQTZ63xY5/MAyvzz+FagR/qGh1kIXvUpsno3AAE0Z95d6QO51zrcMGaEGpwh/57idfMxTxbvRmytg==", - "dependencies": { - "magicli": "0.0.5", - "split-skip": "0.0.2", - "stringify-parameters": "0.0.4", - "unpack-string": "0.0.2" - }, - "bin": { - "inspect-parameters-declaration": "bin/cli.js" - } - }, - "node_modules/object-to-arguments/node_modules/magicli": { - "version": "0.0.5", - "resolved": "https://registry.npmmirror.com/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha512-wZbMtnl2v1b+Jp3xlqA9FU/O4I6YhGXR8xSY/eU2+gDAvut/F+W3gl4qs61iL4LELC7jqSAE6aAD5668EbmQHA==", - "dependencies": { - "commander": "^2.9.0", - "get-stdin": "^5.0.1", - "inspect-function": "^0.2.1", - "pipe-functions": "^1.2.0" - } - }, - "node_modules/ohmyfetch": { - "version": "0.4.21", - "resolved": "https://registry.npmmirror.com/ohmyfetch/-/ohmyfetch-0.4.21.tgz", - "integrity": "sha512-VG7f/JRvqvBOYvL0tHyEIEG7XHWm7OqIfAs6/HqwWwDfjiJ1g0huIpe5sFEmyb+7hpFa1EGNH2aERWR72tlClw==", - "dependencies": { - "destr": "^1.2.0", - "node-fetch-native": "^0.1.8", - "ufo": "^0.8.6", - "undici": "^5.12.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/pipe-functions": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/pipe-functions/-/pipe-functions-1.3.0.tgz", - "integrity": "sha512-6Rtbp7criZRwedlvWbUYxqlqJoAlMvYHo2UcRWq79xZ54vZcaNHpVBOcWkX3ErT2aUA69tv+uiv4zKJbhD/Wgg==" - }, - "node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmmirror.com/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmmirror.com/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", - "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/reduce-flatten": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz", - "integrity": "sha512-j5WfFJfc9CoXv/WbwVLHq74i/hdTUpy+iNC534LxczMRP67vJeK3V9JOdnL0N1cIRbn9mYhE2yVjvvKXDxvNXQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmmirror.com/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "node_modules/quansync": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", + "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1460,482 +620,104 @@ } }, "node_modules/sharp": { - "version": "0.31.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.3.tgz", - "integrity": "sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "color": "^4.2.3", - "detect-libc": "^2.0.1", - "node-addon-api": "^5.0.0", - "prebuild-install": "^7.1.1", - "semver": "^7.3.8", - "simple-get": "^4.0.1", - "tar-fs": "^2.1.1", - "tunnel-agent": "^0.6.0" + "detect-libc": "^2.0.4", + "semver": "^7.7.2" }, "engines": { - "node": ">=14.15.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" } }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" } }, - "node_modules/split-skip": { - "version": "0.0.2", - "resolved": "https://registry.npmmirror.com/split-skip/-/split-skip-0.0.2.tgz", - "integrity": "sha512-weHOi8BolsDnGIwhhWHbA+wKSuSpvWwjRrdj8SdbIIis2vSwOE37CQP8x3EleuzxanUr3AK8BdUy4MkiOULPZg==" - }, "node_modules/sponsorkit": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/sponsorkit/-/sponsorkit-0.8.2.tgz", - "integrity": "sha512-Gxh7hkTUuUVj823+BnwC77Rl5ztFEY00qA2QVULOU0N5qgj1YEP3/0BB/EayfPrHeV7HbAOwSeAnqC8/yoRABA==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/sponsorkit/-/sponsorkit-16.5.0.tgz", + "integrity": "sha512-GvlLg88eAEbKzROwAspT+PQTMfHN9KQ+zgPqBBvV1W2jQmKxOtnv9vjgByXvXA2dvTjnksdvbTuwqhJZllyLQA==", + "license": "MIT", "dependencies": { - "consola": "^2.15.3", - "dotenv": "^16.0.3", - "fs-extra": "^11.1.0", - "image-data-uri": "^2.0.1", - "node-html-parser": "^6.1.5", - "ohmyfetch": "^0.4.21", - "picocolors": "^1.0.0", - "sharp": "^0.31.3", - "unconfig": "^0.3.7", - "yargs": "^17.7.1" + "ansis": "^4.1.0", + "cac": "^6.7.14", + "consola": "^3.4.2", + "dotenv": "^16.5.0", + "ofetch": "^1.4.1", + "sharp": "^0.34.2", + "unconfig": "^7.3.2" }, "bin": { - "sponsorkit": "bin/sponsorkit.js" + "sponsorkit": "bin/sponsorkit.mjs" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, - "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmmirror.com/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "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==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/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==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/stringify-parameters": { - "version": "0.0.4", - "resolved": "https://registry.npmmirror.com/stringify-parameters/-/stringify-parameters-0.0.4.tgz", - "integrity": "sha512-H3L90ERn5UPtkpO8eugnKcLgpIVlvTyUTrcLGm607AV5JDH6z0GymtNLr3gjGlP6I6NB/mxNX9QpY6jEQGLPdQ==", - "dependencies": { - "magicli": "0.0.5", - "unpack-string": "0.0.2" - }, - "bin": { - "stringify-parameters": "bin/cli.js" - } - }, - "node_modules/stringify-parameters/node_modules/inspect-function": { - "version": "0.2.2", - "resolved": "https://registry.npmmirror.com/inspect-function/-/inspect-function-0.2.2.tgz", - "integrity": "sha512-becs5gzcHwPrlHawscYkyQ/ShiOiosrXPhA5RVZ3qyWH4aWdD52RnMfXq/dwQXciHwiieD8aIPwdIWYv6eL+sQ==", - "dependencies": { - "split-skip": "0.0.1", - "unpack-string": "0.0.2" - } - }, - "node_modules/stringify-parameters/node_modules/magicli": { - "version": "0.0.5", - "resolved": "https://registry.npmmirror.com/magicli/-/magicli-0.0.5.tgz", - "integrity": "sha512-wZbMtnl2v1b+Jp3xlqA9FU/O4I6YhGXR8xSY/eU2+gDAvut/F+W3gl4qs61iL4LELC7jqSAE6aAD5668EbmQHA==", - "dependencies": { - "commander": "^2.9.0", - "get-stdin": "^5.0.1", - "inspect-function": "^0.2.1", - "pipe-functions": "^1.2.0" - } - }, - "node_modules/stringify-parameters/node_modules/split-skip": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/split-skip/-/split-skip-0.0.1.tgz", - "integrity": "sha512-7dkvq+gofI4M8zx4iZnEZ3O1s7FP4Y/iaIDHJh5RyWrs8idcPauFi2OZe3TBi36fLvR2j5z3kSzVtz6IhPdncQ==" - }, - "node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/table-layout": { - "version": "0.4.5", - "resolved": "https://registry.npmmirror.com/table-layout/-/table-layout-0.4.5.tgz", - "integrity": "sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw==", - "dependencies": { - "array-back": "^2.0.0", - "deep-extend": "~0.6.0", - "lodash.padend": "^4.6.1", - "typical": "^2.6.1", - "wordwrapjs": "^3.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, - "node_modules/typical": { - "version": "2.6.1", - "resolved": "https://registry.npmmirror.com/typical/-/typical-2.6.1.tgz", - "integrity": "sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg==" + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true }, "node_modules/ufo": { - "version": "0.8.6", - "resolved": "https://registry.npmmirror.com/ufo/-/ufo-0.8.6.tgz", - "integrity": "sha512-fk6CmUgwKCfX79EzcDQQpSCMxrHstvbLswFChHS0Vump+kFkw7nJBfTZoC1j0bOGoY9I7R3n2DGek5ajbcYnOw==" + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "license": "MIT" }, "node_modules/unconfig": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-0.3.9.tgz", - "integrity": "sha512-8yhetFd48M641mxrkWA+C/lZU4N0rCOdlo3dFsyFPnBHBjMJfjT/3eAZBRT2RxCRqeBMAKBVgikejdS6yeBjMw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-7.3.2.tgz", + "integrity": "sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==", + "license": "MIT", "dependencies": { - "@antfu/utils": "^0.7.2", - "defu": "^6.1.2", - "jiti": "^1.18.2" + "@quansync/fs": "^0.1.1", + "defu": "^6.1.4", + "jiti": "^2.4.2", + "quansync": "^0.2.8" }, "funding": { "url": "https://github.com/sponsors/antfu" } - }, - "node_modules/undici": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.21.0.tgz", - "integrity": "sha512-HOjK8l6a57b2ZGXOcUsI5NLfoTrfmbOl90ixJDl0AEFG4wgHNDQxtZy15/ZQp7HhjkpaGlp/eneMgtsu1dIlUA==", - "dependencies": { - "busboy": "^1.6.0" - }, - "engines": { - "node": ">=12.18" - } - }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpack-string": { - "version": "0.0.2", - "resolved": "https://registry.npmmirror.com/unpack-string/-/unpack-string-0.0.2.tgz", - "integrity": "sha512-2ZFjp5aY7QwHE6HAp47RnKYfvgAQ5+NwbKq/ZVtty85RDb3/UaTeCfizo5L/fXzM7UkMP/zDtbV+kGW/iJiK6w==" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/wordwrapjs": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/wordwrapjs/-/wordwrapjs-3.0.0.tgz", - "integrity": "sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==", - "dependencies": { - "reduce-flatten": "^1.0.1", - "typical": "^2.6.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "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==", - "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/wrap-ansi/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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/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==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "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": "7.0.0", - "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha512-WhzC+xgstid9MbVUktco/bf+KJG+Uu6vMX0LN1sLJvwmbCQVxb4D8LzogobonKycNasCZLdOzTAk1SK7+K7swg==", - "dependencies": { - "camelcase": "^4.1.0" - } - }, - "node_modules/yargs/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==", - "engines": { - "node": ">=12" - } } } } diff --git a/scripts/sponsors/package.json b/scripts/sponsors/package.json index 70d6dc5ce..c9f000b90 100644 --- a/scripts/sponsors/package.json +++ b/scripts/sponsors/package.json @@ -10,6 +10,9 @@ "author": "", "license": "ISC", "dependencies": { - "sponsorkit": "^0.8.2" + "sponsorkit": "^16.5.0" + }, + "engines": { + "node": ">=22.0.0" } } diff --git a/v2/.golangci.yml b/v2/.golangci.yml new file mode 100644 index 000000000..66b77ba7f --- /dev/null +++ b/v2/.golangci.yml @@ -0,0 +1,162 @@ +# Options for analysis runner. +run: + # Custom concurrency value + concurrency: 4 + + # Execution timeout + timeout: 10m + + # Exit code when an issue is found. + issues-exit-code: 1 + + # Inclusion of test files + tests: false + + modules-download-mode: readonly + + allow-parallel-runners: false + + go: '1.21' + + +output: + # Runner output format + format: tab + + # Print line of issue code + print-issued-lines: false + + # Append linter to the output + print-linter-name: true + + # Separate issues by line + uniq-by-line: true + + # Output path prefixing + path-prefix: "" + + # Sort results + sort-results: true + + +# Specific linter configs +linters-settings: + errcheck: + check-type-assertions: false + check-blank: false + ignore: fmt:.* + disable-default-exclusions: false + + gofmt: + simplify: true + + gofumpt: + extra-rules: false + +linters: + fast: false + # Enable all available linters. + enable-all: true + # Disable specific linters + disable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - cyclop + - deadcode + - decorder + - depguard + - dogsled + - dupl + - dupword + - durationcheck + - errchkjson + - errorlint + - execinquery + - exhaustive + - exhaustivestruct + - exhaustruct + - exportloopref + - forbidigo + - forcetypeassert + - funlen + - gci + - ginkgolinter + - gocheckcompilerdirectives + - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - goerr113 + - goheader + - goimports + - golint + - gomnd + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosmopolitan + - govet + - grouper + - ifshort + - importas + - ineffassign + - interfacebloat + - interfacer + - ireturn + - lll + - loggercheck + - maintidx + - makezero + - maligned + - mirror + - musttag + - nakedret + - nestif + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nonamedreturns + - nosnakecase + - nosprintfhostport + - paralleltest + - prealloc + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - scopelint + - sqlclosecheck + - staticcheck + - structcheck + - stylecheck + - tagalign + - tagliatelle + - tenv + - testableexamples + - testpackage + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - varcheck + - varnamelen + - wastedassign + - whitespace + - wrapcheck + - wsl + - zerologlint \ No newline at end of file diff --git a/v2/Taskfile.yaml b/v2/Taskfile.yaml index 526dd097d..d1893732b 100644 --- a/v2/Taskfile.yaml +++ b/v2/Taskfile.yaml @@ -3,6 +3,16 @@ version: "3" tasks: + download: + summary: Run go mod tidy + cmds: + - go mod tidy + + lint: + summary: Run golangci-lint + cmds: + - golangci-lint run ./... --timeout=3m -v + release: summary: Release a new version of Task. Call with `task v2:release -- ` dir: tools/release diff --git a/v2/cmd/wails/build.go b/v2/cmd/wails/build.go index 1c6b791ec..39ad00d2f 100644 --- a/v2/cmd/wails/build.go +++ b/v2/cmd/wails/build.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/wailsapp/wails/v2/pkg/commands/buildtags" "os" "runtime" "strings" @@ -18,7 +19,6 @@ import ( ) func buildApplication(f *flags.Build) error { - if f.NoColour { pterm.DisableColor() colour.ColourEnabled = false @@ -50,6 +50,23 @@ func buildApplication(f *flags.Build) error { return err } + // Set obfuscation from project file + if projectOptions.Obfuscated { + f.Obfuscated = projectOptions.Obfuscated + } + + // Set garble args from project file + if projectOptions.GarbleArgs != "" { + f.GarbleArgs = projectOptions.GarbleArgs + } + + projectTags, err := buildtags.Parse(projectOptions.BuildTags) + if err != nil { + return err + } + userTags := f.GetTags() + compiledTags := append(projectTags, userTags...) + // Create BuildOptions buildOptions := &build.Options{ Logger: logger, @@ -67,7 +84,7 @@ func buildApplication(f *flags.Build) error { IgnoreFrontend: f.SkipFrontend, Compress: f.Upx, CompressFlags: f.UpxFlags, - UserTags: f.GetTags(), + UserTags: compiledTags, WebView2Strategy: f.GetWebView2Strategy(), TrimPath: f.TrimPath, RaceDetector: f.RaceDetector, @@ -76,6 +93,7 @@ func buildApplication(f *flags.Build) error { GarbleArgs: f.GarbleArgs, SkipBindings: f.SkipBindings, ProjectData: projectOptions, + SkipEmbedCreate: f.SkipEmbedCreate, } tableData := pterm.TableData{ @@ -96,7 +114,7 @@ func buildApplication(f *flags.Build) error { {"Package", bool2Str(!f.NoPackage)}, {"Clean Bin Dir", bool2Str(f.Clean)}, {"LDFlags", f.LdFlags}, - {"Tags", "[" + strings.Join(f.GetTags(), ",") + "]"}, + {"Tags", "[" + strings.Join(compiledTags, ",") + "]"}, {"Race Detector", bool2Str(f.RaceDetector)}, }...) if len(buildOptions.OutputFile) > 0 && f.GetTargets().Length() == 1 { @@ -255,5 +273,4 @@ func buildApplication(f *flags.Build) error { } return nil - } diff --git a/v2/cmd/wails/dev.go b/v2/cmd/wails/dev.go index dbb2cf5d8..30213a68e 100644 --- a/v2/cmd/wails/dev.go +++ b/v2/cmd/wails/dev.go @@ -1,16 +1,16 @@ package main import ( + "os" + "github.com/pterm/pterm" "github.com/wailsapp/wails/v2/cmd/wails/flags" "github.com/wailsapp/wails/v2/cmd/wails/internal/dev" "github.com/wailsapp/wails/v2/internal/colour" "github.com/wailsapp/wails/v2/pkg/clilogger" - "os" ) func devApplication(f *flags.Dev) error { - if f.NoColour { pterm.DisableColor() colour.ColourEnabled = false @@ -34,5 +34,4 @@ func devApplication(f *flags.Dev) error { } return dev.Application(f, logger) - } diff --git a/v2/cmd/wails/doctor.go b/v2/cmd/wails/doctor.go index 75d62b246..7f453133d 100644 --- a/v2/cmd/wails/doctor.go +++ b/v2/cmd/wails/doctor.go @@ -1,12 +1,17 @@ package main import ( + "fmt" "runtime" "runtime/debug" + "strconv" "strings" + "github.com/wailsapp/wails/v2/internal/shell" + "github.com/pterm/pterm" + "github.com/jaypipes/ghw" "github.com/wailsapp/wails/v2/cmd/wails/flags" "github.com/wailsapp/wails/v2/internal/colour" "github.com/wailsapp/wails/v2/internal/system" @@ -74,11 +79,92 @@ func diagnoseEnvironment(f *flags.Doctor) error { {pterm.Bold.Sprint("OS"), info.OS.Name}, {pterm.Bold.Sprint("Version"), info.OS.Version}, {pterm.Bold.Sprint("ID"), info.OS.ID}, + {pterm.Bold.Sprint("Branding"), info.OS.Branding}, {pterm.Bold.Sprint("Go Version"), runtime.Version()}, {pterm.Bold.Sprint("Platform"), runtime.GOOS}, {pterm.Bold.Sprint("Architecture"), runtime.GOARCH}, } + // Probe CPU + cpus, _ := ghw.CPU() + if cpus != nil { + prefix := "CPU" + for idx, cpu := range cpus.Processors { + if len(cpus.Processors) > 1 { + prefix = "CPU " + strconv.Itoa(idx+1) + } + systemTabledata = append(systemTabledata, []string{prefix, cpu.Model}) + } + } else { + cpuInfo := "Unknown" + if runtime.GOOS == "darwin" { + // Try to get CPU info from sysctl + if stdout, _, err := shell.RunCommand("", "sysctl", "-n", "machdep.cpu.brand_string"); err == nil { + cpuInfo = strings.TrimSpace(stdout) + } + } + systemTabledata = append(systemTabledata, []string{"CPU", cpuInfo}) + } + + // Probe GPU + gpu, _ := ghw.GPU(ghw.WithDisableWarnings()) + if gpu != nil { + prefix := "GPU" + for idx, card := range gpu.GraphicsCards { + if len(gpu.GraphicsCards) > 1 { + prefix = "GPU " + strconv.Itoa(idx+1) + " " + } + if card.DeviceInfo == nil { + systemTabledata = append(systemTabledata, []string{prefix, "Unknown"}) + continue + } + details := fmt.Sprintf("%s (%s) - Driver: %s", card.DeviceInfo.Product.Name, card.DeviceInfo.Vendor.Name, card.DeviceInfo.Driver) + systemTabledata = append(systemTabledata, []string{prefix, details}) + } + } else { + gpuInfo := "Unknown" + if runtime.GOOS == "darwin" { + // Try to get GPU info from system_profiler + if stdout, _, err := shell.RunCommand("", "system_profiler", "SPDisplaysDataType"); err == nil { + var ( + startCapturing bool + gpuInfoDetails []string + ) + for _, line := range strings.Split(stdout, "\n") { + if strings.Contains(line, "Chipset Model") { + startCapturing = true + } + if startCapturing { + gpuInfoDetails = append(gpuInfoDetails, strings.TrimSpace(line)) + } + if strings.Contains(line, "Metal Support") { + break + } + } + if len(gpuInfoDetails) > 0 { + gpuInfo = strings.Join(gpuInfoDetails, " ") + } + } + } + systemTabledata = append(systemTabledata, []string{"GPU", gpuInfo}) + } + + memory, _ := ghw.Memory() + if memory != nil { + systemTabledata = append(systemTabledata, []string{"Memory", strconv.Itoa(int(memory.TotalPhysicalBytes/1024/1024/1024)) + "GB"}) + } else { + memInfo := "Unknown" + if runtime.GOOS == "darwin" { + // Try to get Memory info from sysctl + if stdout, _, err := shell.RunCommand("", "sysctl", "-n", "hw.memsize"); err == nil { + if memSize, err := strconv.Atoi(strings.TrimSpace(stdout)); err == nil { + memInfo = strconv.Itoa(memSize/1024/1024/1024) + "GB" + } + } + } + systemTabledata = append(systemTabledata, []string{"Memory", memInfo}) + } + err = pterm.DefaultTable.WithBoxed().WithData(systemTabledata).Render() if err != nil { return err @@ -89,8 +175,8 @@ func diagnoseEnvironment(f *flags.Doctor) error { // Output Dependencies Status var dependenciesMissing []string var externalPackages []*packagemanager.Dependency - var dependenciesAvailableRequired = 0 - var dependenciesAvailableOptional = 0 + dependenciesAvailableRequired := 0 + dependenciesAvailableOptional := 0 dependenciesTableData := pterm.TableData{ {"Dependency", "Package Name", "Status", "Version"}, @@ -169,7 +255,6 @@ func diagnoseEnvironment(f *flags.Doctor) error { if len(dependenciesMissing) != 0 { pterm.Println("Fatal:") pterm.Println("Required dependencies missing: " + strings.Join(dependenciesMissing, " ")) - pterm.Println("Please read this article on how to resolve this: https://wails.io/guides/resolving-missing-packages") } pterm.Println() // Spacer for sponsor message diff --git a/v2/cmd/wails/flags/build.go b/v2/cmd/wails/flags/build.go index 974d9c3ad..db05c9035 100644 --- a/v2/cmd/wails/flags/build.go +++ b/v2/cmd/wails/flags/build.go @@ -24,8 +24,7 @@ type Build struct { Common BuildCommon - NoPackage bool `name:"noPackage" description:"Skips platform specific packaging"` - SkipModTidy bool `name:"m" description:"Skip mod tidy before compile"` + NoPackage bool `description:"Skips platform specific packaging"` Upx bool `description:"Compress final binary with UPX (if installed)"` UpxFlags string `description:"Flags to pass to upx"` Platform string `description:"Platform to target. Comma separate multiple platforms"` diff --git a/v2/cmd/wails/flags/buildcommon.go b/v2/cmd/wails/flags/buildcommon.go index dcad33abf..a22f7a502 100644 --- a/v2/cmd/wails/flags/buildcommon.go +++ b/v2/cmd/wails/flags/buildcommon.go @@ -1,14 +1,16 @@ package flags type BuildCommon struct { - LdFlags string `description:"Additional ldflags to pass to the compiler"` - Compiler string `description:"Use a different go compiler to build, eg go1.15beta1"` - SkipBindings bool `description:"Skips generation of bindings"` - RaceDetector bool `name:"race" description:"Build with Go's race detector"` - SkipFrontend bool `name:"s" description:"Skips building the frontend"` - Verbosity int `name:"v" description:"Verbosity level (0 = quiet, 1 = normal, 2 = verbose)"` - Tags string `description:"Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated"` - NoSyncGoMod bool `description:"Don't sync go.mod"` + LdFlags string `description:"Additional ldflags to pass to the compiler"` + Compiler string `description:"Use a different go compiler to build, eg go1.15beta1"` + SkipBindings bool `description:"Skips generation of bindings"` + RaceDetector bool `name:"race" description:"Build with Go's race detector"` + SkipFrontend bool `name:"s" description:"Skips building the frontend"` + Verbosity int `name:"v" description:"Verbosity level (0 = quiet, 1 = normal, 2 = verbose)"` + Tags string `description:"Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated"` + NoSyncGoMod bool `description:"Don't sync go.mod"` + SkipModTidy bool `name:"m" description:"Skip mod tidy before compile"` + SkipEmbedCreate bool `description:"Skips creation of embed files"` } func (c BuildCommon) Default() BuildCommon { diff --git a/v2/cmd/wails/flags/dev.go b/v2/cmd/wails/flags/dev.go index 7e5e6239c..d31d8bc87 100644 --- a/v2/cmd/wails/flags/dev.go +++ b/v2/cmd/wails/flags/dev.go @@ -31,6 +31,7 @@ type Dev struct { AppArgs string `flag:"appargs" description:"arguments to pass to the underlying app (quoted and space separated)"` Save bool `flag:"save" description:"Save the given flags as defaults"` FrontendDevServerURL string `flag:"frontenddevserverurl" description:"The url of the external frontend dev server to use"` + ViteServerTimeout int `flag:"viteservertimeout" description:"The timeout in seconds for Vite server detection (default: 10)"` // Internal state devServerURL *url.URL @@ -41,13 +42,13 @@ func (*Dev) Default() *Dev { result := &Dev{ Extensions: "go", Debounce: 100, + LogLevel: "Info", } result.BuildCommon = result.BuildCommon.Default() return result } func (d *Dev) Process() error { - var err error err = d.loadAndMergeProjectConfig() if err != nil { @@ -105,6 +106,13 @@ func (d *Dev) loadAndMergeProjectConfig() error { d.AppArgs, _ = lo.Coalesce(d.AppArgs, d.projectConfig.AppArgs) + if d.ViteServerTimeout == 0 && d.projectConfig.ViteServerTimeout != 0 { + d.ViteServerTimeout = d.projectConfig.ViteServerTimeout + } else if d.ViteServerTimeout == 0 { + d.ViteServerTimeout = 10 // Default timeout + } + d.projectConfig.ViteServerTimeout = d.ViteServerTimeout + if d.Save { err = d.projectConfig.Save() if err != nil { @@ -113,27 +121,28 @@ func (d *Dev) loadAndMergeProjectConfig() error { } return nil - } // GenerateBuildOptions creates a build.Options using the flags func (d *Dev) GenerateBuildOptions() *build.Options { result := &build.Options{ - OutputType: "dev", - Mode: build.Dev, - Devtools: true, - Arch: runtime.GOARCH, - Pack: true, - Platform: runtime.GOOS, - LDFlags: d.LdFlags, - Compiler: d.Compiler, - ForceBuild: d.ForceBuild, - IgnoreFrontend: d.SkipFrontend, - SkipBindings: d.SkipBindings, - Verbosity: d.Verbosity, - WailsJSDir: d.WailsJSDir, - RaceDetector: d.RaceDetector, - ProjectData: d.projectConfig, + OutputType: "dev", + Mode: build.Dev, + Devtools: true, + Arch: runtime.GOARCH, + Pack: true, + Platform: runtime.GOOS, + LDFlags: d.LdFlags, + Compiler: d.Compiler, + ForceBuild: d.ForceBuild, + IgnoreFrontend: d.SkipFrontend, + SkipBindings: d.SkipBindings, + SkipModTidy: d.SkipModTidy, + Verbosity: d.Verbosity, + WailsJSDir: d.WailsJSDir, + RaceDetector: d.RaceDetector, + ProjectData: d.projectConfig, + SkipEmbedCreate: d.SkipEmbedCreate, } return result diff --git a/v2/cmd/wails/flags/generate.go b/v2/cmd/wails/flags/generate.go index 9adf78aa4..b14d67017 100644 --- a/v2/cmd/wails/flags/generate.go +++ b/v2/cmd/wails/flags/generate.go @@ -2,6 +2,7 @@ package flags type GenerateModule struct { Common + Compiler string `description:"Use a different go compiler to build, eg go1.15beta1"` Tags string `description:"Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated"` Verbosity int `name:"v" description:"Verbosity level (0 = quiet, 1 = normal, 2 = verbose)"` } @@ -12,3 +13,9 @@ type GenerateTemplate struct { Frontend string `description:"Frontend to use for the template"` Quiet bool `description:"Suppress output"` } + +func (c *GenerateModule) Default() *GenerateModule { + return &GenerateModule{ + Compiler: "go", + } +} diff --git a/v2/cmd/wails/flags/init.go b/v2/cmd/wails/flags/init.go index 6e642ec9a..16d56a207 100644 --- a/v2/cmd/wails/flags/init.go +++ b/v2/cmd/wails/flags/init.go @@ -14,7 +14,6 @@ type Init struct { } func (i *Init) Default() *Init { - result := &Init{ TemplateName: "vanilla", } diff --git a/v2/cmd/wails/generate.go b/v2/cmd/wails/generate.go index a7b059ecf..15a6b33d8 100644 --- a/v2/cmd/wails/generate.go +++ b/v2/cmd/wails/generate.go @@ -2,6 +2,9 @@ package main import ( "fmt" + "os" + "path/filepath" + "github.com/leaanthony/debme" "github.com/leaanthony/gosod" "github.com/pterm/pterm" @@ -14,12 +17,9 @@ import ( "github.com/wailsapp/wails/v2/pkg/clilogger" "github.com/wailsapp/wails/v2/pkg/commands/bindings" "github.com/wailsapp/wails/v2/pkg/commands/buildtags" - "os" - "path/filepath" ) func generateModule(f *flags.GenerateModule) error { - if f.NoColour { pterm.DisableColor() colour.ColourEnabled = false @@ -43,10 +43,16 @@ func generateModule(f *flags.GenerateModule) error { return err } + if projectConfig.Bindings.TsGeneration.OutputType == "" { + projectConfig.Bindings.TsGeneration.OutputType = "classes" + } + _, err = bindings.GenerateBindings(bindings.Options{ - Tags: buildTags, - TsPrefix: projectConfig.Bindings.TsGeneration.Prefix, - TsSuffix: projectConfig.Bindings.TsGeneration.Suffix, + Compiler: f.Compiler, + Tags: buildTags, + TsPrefix: projectConfig.Bindings.TsGeneration.Prefix, + TsSuffix: projectConfig.Bindings.TsGeneration.Suffix, + TsOutputType: projectConfig.Bindings.TsGeneration.OutputType, }) if err != nil { return err @@ -55,7 +61,6 @@ func generateModule(f *flags.GenerateModule) error { } func generateTemplate(f *flags.GenerateTemplate) error { - if f.NoColour { pterm.DisableColor() colour.ColourEnabled = false @@ -77,7 +82,7 @@ func generateTemplate(f *flags.GenerateTemplate) error { } templateDir := filepath.Join(cwd, f.Name) if !fs.DirExists(templateDir) { - err := os.MkdirAll(templateDir, 0755) + err := os.MkdirAll(templateDir, 0o755) if err != nil { return err } @@ -200,7 +205,7 @@ func processPackageJSON(frontendDir string) error { json, _ = sjson.SetBytes(json, "name", "{{.ProjectName}}") json, _ = sjson.SetBytes(json, "author", "{{.AuthorName}}") - err = os.WriteFile(packageJSON, json, 0644) + err = os.WriteFile(packageJSON, json, 0o644) if err != nil { return err } @@ -231,7 +236,7 @@ func processPackageLockJSON(frontendDir string) error { printBulletPoint("Updating package-lock.json data...") json, _ = sjson.Set(json, "name", "{{.ProjectName}}") - err = os.WriteFile(filename, []byte(json), 0644) + err = os.WriteFile(filename, []byte(json), 0o644) if err != nil { return err } diff --git a/v2/cmd/wails/init.go b/v2/cmd/wails/init.go index f9a9c6b3f..f79e37ffc 100644 --- a/v2/cmd/wails/init.go +++ b/v2/cmd/wails/init.go @@ -3,6 +3,12 @@ package main import ( "bufio" "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + "github.com/flytam/filenamify" "github.com/leaanthony/slicer" "github.com/pkg/errors" @@ -13,15 +19,9 @@ import ( "github.com/wailsapp/wails/v2/pkg/clilogger" "github.com/wailsapp/wails/v2/pkg/git" "github.com/wailsapp/wails/v2/pkg/templates" - "os" - "os/exec" - "path/filepath" - "strings" - "time" ) func initProject(f *flags.Init) error { - if f.NoColour { pterm.DisableColor() colour.ColourEnabled = false @@ -125,6 +125,12 @@ func initProject(f *flags.Init) error { return err } + // Change the module name to project name + err = updateModuleNameToProjectName(options, quiet) + if err != nil { + return err + } + if !f.CIMode { // Run `go mod tidy` to ensure `go.sum` is up to date cmd := exec.Command("go", "mod", "tidy") @@ -215,7 +221,7 @@ func initGit(options *templates.Options) error { "frontend/dist", "frontend/node_modules", } - err = os.WriteFile(filepath.Join(options.TargetDir, ".gitignore"), []byte(strings.Join(ignore, "\n")), 0644) + err = os.WriteFile(filepath.Join(options.TargetDir, ".gitignore"), []byte(strings.Join(ignore, "\n")), 0o644) if err != nil { return errors.Wrap(err, "Unable to create gitignore") } @@ -271,8 +277,19 @@ func updateReplaceLine(targetPath string) { } } - err = os.WriteFile("go.mod", []byte(strings.Join(lines, "\n")), 0644) + err = os.WriteFile("go.mod", []byte(strings.Join(lines, "\n")), 0o644) if err != nil { fatal(err.Error()) } } + +func updateModuleNameToProjectName(options *templates.Options, quiet bool) error { + cmd := exec.Command("go", "mod", "edit", "-module", options.ProjectName) + cmd.Dir = options.TargetDir + cmd.Stderr = os.Stderr + if !quiet { + cmd.Stdout = os.Stdout + } + + return cmd.Run() +} diff --git a/v2/cmd/wails/internal/dev/dev.go b/v2/cmd/wails/internal/dev/dev.go index 80d7a6d87..9495b5bf2 100644 --- a/v2/cmd/wails/internal/dev/dev.go +++ b/v2/cmd/wails/internal/dev/dev.go @@ -52,7 +52,6 @@ func sliceToMap(input []string) map[string]struct{} { // Application runs the application in dev mode func Application(f *flags.Dev, logger *clilogger.CLILogger) error { - cwd := lo.Must(os.Getwd()) // Update go.mod to use current wails version @@ -61,10 +60,12 @@ func Application(f *flags.Dev, logger *clilogger.CLILogger) error { return err } - // Run go mod tidy to ensure we're up-to-date - err = runCommand(cwd, false, "go", "mod", "tidy") - if err != nil { - return err + if !f.SkipModTidy { + // Run go mod tidy to ensure we're up-to-date + err = runCommand(cwd, false, f.Compiler, "mod", "tidy") + if err != nil { + return err + } } buildOptions := f.GenerateBuildOptions() @@ -75,10 +76,15 @@ func Application(f *flags.Dev, logger *clilogger.CLILogger) error { return err } - buildOptions.UserTags = userTags - projectConfig := f.ProjectConfig() + projectTags, err := buildtags.Parse(projectConfig.BuildTags) + if err != nil { + return err + } + compiledTags := append(projectTags, userTags...) + buildOptions.UserTags = compiledTags + // Setup signal handler quitChannel := make(chan os.Signal, 1) signal.Notify(quitChannel, os.Interrupt, syscall.SIGTERM) @@ -98,7 +104,7 @@ func Application(f *flags.Dev, logger *clilogger.CLILogger) error { // frontend:dev:watcher command. frontendDevAutoDiscovery := projectConfig.IsFrontendDevServerURLAutoDiscovery() if command := projectConfig.DevWatcherCommand; command != "" { - closer, devServerURL, devServerViteVersion, err := runFrontendDevWatcherCommand(projectConfig.GetFrontendDir(), command, frontendDevAutoDiscovery) + closer, devServerURL, devServerViteVersion, err := runFrontendDevWatcherCommand(projectConfig.GetFrontendDir(), command, frontendDevAutoDiscovery, projectConfig.ViteServerTimeout) if err != nil { return err } @@ -152,7 +158,7 @@ func Application(f *flags.Dev, logger *clilogger.CLILogger) error { }() // Watch for changes and trigger restartApp() - debugBinaryProcess, err = doWatcherLoop(cwd, buildOptions, debugBinaryProcess, f, exitCodeChannel, quitChannel, f.DevServerURL(), legacyUseDevServerInsteadofCustomScheme) + debugBinaryProcess, err = doWatcherLoop(cwd, projectConfig.ReloadDirectories, buildOptions, debugBinaryProcess, f, exitCodeChannel, quitChannel, f.DevServerURL(), legacyUseDevServerInsteadofCustomScheme) if err != nil { return err } @@ -204,7 +210,7 @@ func runCommand(dir string, exitOnError bool, command string, args ...string) er } // runFrontendDevWatcherCommand will run the `frontend:dev:watcher` command if it was given, ex- `npm run dev` -func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, discoverViteServerURL bool) (func(), string, string, error) { +func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, discoverViteServerURL bool, viteServerTimeout int) (func(), string, string, error) { ctx, cancel := context.WithCancel(context.Background()) scanner := NewStdoutScanner() cmdSlice := strings.Split(devCommand, " ") @@ -224,9 +230,9 @@ func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, d select { case serverURL := <-scanner.ViteServerURLChan: viteServerURL = serverURL - case <-time.After(time.Second * 10): + case <-time.After(time.Second * time.Duration(viteServerTimeout)): cancel() - return nil, "", "", errors.New("failed to find Vite server URL") + return nil, "", "", fmt.Errorf("failed to find Vite server URL: Timed out waiting for Vite to output a URL after %d seconds", viteServerTimeout) } } @@ -271,7 +277,6 @@ func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, d // restartApp does the actual rebuilding of the application when files change func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, exitCodeChannel chan int, legacyUseDevServerInsteadofCustomScheme bool) (*process.Process, string, error) { - appBinary, err := build.Build(buildOptions) println() if err != nil { @@ -298,7 +303,6 @@ func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process // parse appargs if any args, err := shlex.Split(f.AppArgs) - if err != nil { buildOptions.Logger.Fatal("Unable to parse appargs: %s", err.Error()) } @@ -327,9 +331,9 @@ func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process } // doWatcherLoop is the main watch loop that runs while dev is active -func doWatcherLoop(cwd string, buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, exitCodeChannel chan int, quitChannel chan os.Signal, devServerURL *url.URL, legacyUseDevServerInsteadofCustomScheme bool) (*process.Process, error) { +func doWatcherLoop(cwd string, reloadDirs string, buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, exitCodeChannel chan int, quitChannel chan os.Signal, devServerURL *url.URL, legacyUseDevServerInsteadofCustomScheme bool) (*process.Process, error) { // create the project files watcher - watcher, err := initialiseWatcher(cwd) + watcher, err := initialiseWatcher(cwd, reloadDirs) if err != nil { logutils.LogRed("Unable to create filesystem watcher. Reloads will not occur.") return nil, err @@ -345,7 +349,7 @@ func doWatcherLoop(cwd string, buildOptions *build.Options, debugBinaryProcess * logutils.LogGreen("Watching (sub)/directory: %s", cwd) // Main Loop - var extensionsThatTriggerARebuild = sliceToMap(strings.Split(f.Extensions, ",")) + extensionsThatTriggerARebuild := sliceToMap(strings.Split(f.Extensions, ",")) var dirsThatTriggerAReload []string for _, dir := range strings.Split(f.ReloadDirs, ",") { if dir == "" { diff --git a/v2/cmd/wails/internal/dev/watcher.go b/v2/cmd/wails/internal/dev/watcher.go index 499b76dfd..e1161f87c 100644 --- a/v2/cmd/wails/internal/dev/watcher.go +++ b/v2/cmd/wails/internal/dev/watcher.go @@ -4,6 +4,7 @@ import ( "bufio" "os" "path/filepath" + "strings" "github.com/wailsapp/wails/v2/internal/fs" @@ -17,8 +18,7 @@ type Watcher interface { } // initialiseWatcher creates the project directory watcher that will trigger recompile -func initialiseWatcher(cwd string) (*fsnotify.Watcher, error) { - +func initialiseWatcher(cwd, reloadDirs string) (*fsnotify.Watcher, error) { // Ignore dot files, node_modules and build directories by default ignoreDirs := getIgnoreDirs(cwd) @@ -28,12 +28,22 @@ func initialiseWatcher(cwd string) (*fsnotify.Watcher, error) { return nil, err } + customDirs := dirs.AsSlice() + seperatedDirs := strings.Split(reloadDirs, ",") + for _, dir := range seperatedDirs { + customSub, err := fs.GetSubdirectories(filepath.Join(cwd, dir)) + if err != nil { + return nil, err + } + customDirs = append(customDirs, customSub.AsSlice()...) + } + watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err } - for _, dir := range processDirectories(dirs.AsSlice(), ignoreDirs) { + for _, dir := range processDirectories(customDirs, ignoreDirs) { err := watcher.Add(dir) if err != nil { return nil, err diff --git a/v2/cmd/wails/internal/gomod/gomod.go b/v2/cmd/wails/internal/gomod/gomod.go index 52e56344b..5da14a5ff 100644 --- a/v2/cmd/wails/internal/gomod/gomod.go +++ b/v2/cmd/wails/internal/gomod/gomod.go @@ -56,7 +56,7 @@ func SyncGoMod(logger *clilogger.CLILogger, updateWailsVersion bool) error { } if updated { - return os.WriteFile(gomodFilename, gomodData, 0755) + return os.WriteFile(gomodFilename, gomodData, 0o755) } return nil diff --git a/v2/cmd/wails/internal/version.txt b/v2/cmd/wails/internal/version.txt index 7433fb30c..805579f30 100644 --- a/v2/cmd/wails/internal/version.txt +++ b/v2/cmd/wails/internal/version.txt @@ -1 +1 @@ -v2.6.0 \ No newline at end of file +v2.11.0 \ No newline at end of file diff --git a/v2/cmd/wails/main.go b/v2/cmd/wails/main.go index a0a7d4a31..ccf1576e9 100644 --- a/v2/cmd/wails/main.go +++ b/v2/cmd/wails/main.go @@ -66,7 +66,6 @@ func bool2Str(b bool) string { var app *clir.Cli func main() { - var err error app = clir.NewCli("Wails", "Go/HTML Appkit", internal.Version) diff --git a/v2/cmd/wails/update.go b/v2/cmd/wails/update.go index ac0e7375a..9f8b6e604 100644 --- a/v2/cmd/wails/update.go +++ b/v2/cmd/wails/update.go @@ -15,7 +15,6 @@ import ( // AddSubcommand adds the `init` command for the Wails application func update(f *flags.Update) error { - if f.NoColour { colour.ColourEnabled = false pterm.DisableColor() @@ -73,8 +72,7 @@ func update(f *flags.Update) error { } func updateToVersion(targetVersion *github.SemanticVersion, force bool, currentVersion string) error { - - var targetVersionString = "v" + targetVersion.String() + targetVersionString := "v" + targetVersion.String() if targetVersionString == currentVersion { pterm.Println("\nLooks like you're up to date!") diff --git a/v2/examples/customlayout/go.mod b/v2/examples/customlayout/go.mod index fcb4ce4fd..e1a17304e 100644 --- a/v2/examples/customlayout/go.mod +++ b/v2/examples/customlayout/go.mod @@ -1,33 +1,39 @@ module changeme -go 1.18 +go 1.22.0 + +toolchain go1.24.1 require github.com/wailsapp/wails/v2 v2.1.0 require ( github.com/bep/debounce v1.2.1 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect - github.com/labstack/echo/v4 v4.10.2 // indirect - github.com/labstack/gommon v0.4.0 // indirect - github.com/leaanthony/go-ansi-parser v1.6.0 // indirect - github.com/leaanthony/gosod v1.0.3 // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/samber/lo v1.38.1 // indirect - github.com/tkrajina/go-reflector v0.5.6 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - golang.org/x/crypto v0.9.0 // indirect + golang.org/x/crypto v0.33.0 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.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 ) replace github.com/wailsapp/wails/v2 v2.1.0 => ../.. diff --git a/v2/examples/customlayout/go.sum b/v2/examples/customlayout/go.sum index b00ead2f3..f1995affb 100644 --- a/v2/examples/customlayout/go.sum +++ b/v2/examples/customlayout/go.sum @@ -5,71 +5,85 @@ 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/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/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY= -github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= +github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= -github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= -github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= -github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4= -github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM= +github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg= github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= -github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI= +github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +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.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc= -github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/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/samber/lo v1.27.1 h1:sTXwkRiIFIQG+G0HeAvOEnGjqWeWtI9cg5/n51KrxPg= -github.com/samber/lo v1.27.1/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg= +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/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= -github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ= -github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +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/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w= +github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= +github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/go-webview2 v1.0.22/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/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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= @@ -79,17 +93,19 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/examples/dragdrop-test/.gitignore b/v2/examples/dragdrop-test/.gitignore new file mode 100644 index 000000000..a11bbf414 --- /dev/null +++ b/v2/examples/dragdrop-test/.gitignore @@ -0,0 +1,4 @@ +build/bin +node_modules +frontend/dist +frontend/wailsjs diff --git a/v2/examples/dragdrop-test/README.md b/v2/examples/dragdrop-test/README.md new file mode 100644 index 000000000..397b08b92 --- /dev/null +++ b/v2/examples/dragdrop-test/README.md @@ -0,0 +1,19 @@ +# README + +## About + +This is the official Wails Vanilla template. + +You can configure the project by editing `wails.json`. More information about the project settings can be found +here: https://wails.io/docs/reference/project-config + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/v2/examples/dragdrop-test/app.go b/v2/examples/dragdrop-test/app.go new file mode 100644 index 000000000..af53038a1 --- /dev/null +++ b/v2/examples/dragdrop-test/app.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/examples/dragdrop-test/build/README.md b/v2/examples/dragdrop-test/build/README.md new file mode 100644 index 000000000..1ae2f677f --- /dev/null +++ b/v2/examples/dragdrop-test/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/v2/examples/dragdrop-test/build/appicon.png b/v2/examples/dragdrop-test/build/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/v2/examples/dragdrop-test/build/appicon.png differ diff --git a/v2/examples/dragdrop-test/build/darwin/Info.dev.plist b/v2/examples/dragdrop-test/build/darwin/Info.dev.plist new file mode 100644 index 000000000..14121ef7c --- /dev/null +++ b/v2/examples/dragdrop-test/build/darwin/Info.dev.plist @@ -0,0 +1,68 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + diff --git a/v2/examples/dragdrop-test/build/darwin/Info.plist b/v2/examples/dragdrop-test/build/darwin/Info.plist new file mode 100644 index 000000000..d17a7475c --- /dev/null +++ b/v2/examples/dragdrop-test/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/v2/examples/dragdrop-test/build/windows/icon.ico b/v2/examples/dragdrop-test/build/windows/icon.ico new file mode 100644 index 000000000..f33479841 Binary files /dev/null and b/v2/examples/dragdrop-test/build/windows/icon.ico differ diff --git a/v2/examples/dragdrop-test/build/windows/info.json b/v2/examples/dragdrop-test/build/windows/info.json new file mode 100644 index 000000000..9727946b7 --- /dev/null +++ b/v2/examples/dragdrop-test/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/v2/examples/dragdrop-test/build/windows/installer/project.nsi b/v2/examples/dragdrop-test/build/windows/installer/project.nsi new file mode 100644 index 000000000..654ae2e49 --- /dev/null +++ b/v2/examples/dragdrop-test/build/windows/installer/project.nsi @@ -0,0 +1,114 @@ +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 with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows 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 ProjectInfo file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" +## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" +## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" +## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" +### +## !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 # Uinstalling 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.associateCustomProtocols + + !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.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/v2/examples/dragdrop-test/build/windows/installer/wails_tools.nsh b/v2/examples/dragdrop-test/build/windows/installer/wails_tools.nsh new file mode 100644 index 000000000..f9c0f8852 --- /dev/null +++ b/v2/examples/dragdrop-test/build/windows/installer/wails_tools.nsh @@ -0,0 +1,249 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "{{.Name}}" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "{{.Info.CompanyName}}" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "{{.Info.ProductName}}" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "{{.Info.Copyright}}" +!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 "tmp\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 + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + File "..\{{.IconName}}.ico" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconName}}.ico" + {{end}} +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!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 + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/v2/examples/dragdrop-test/build/windows/wails.exe.manifest b/v2/examples/dragdrop-test/build/windows/wails.exe.manifest new file mode 100644 index 000000000..17e1a2387 --- /dev/null +++ b/v2/examples/dragdrop-test/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/v2/examples/dragdrop-test/frontend/index.html b/v2/examples/dragdrop-test/frontend/index.html new file mode 100644 index 000000000..4010f1be6 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + dragdrop-test + + +
+ + + diff --git a/v2/examples/dragdrop-test/frontend/package-lock.json b/v2/examples/dragdrop-test/frontend/package-lock.json new file mode 100644 index 000000000..8eed5313c --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/package-lock.json @@ -0,0 +1,653 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "devDependencies": { + "vite": "^3.0.7" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/vite": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", + "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/v2/examples/dragdrop-test/frontend/package.json b/v2/examples/dragdrop-test/frontend/package.json new file mode 100644 index 000000000..a1b6f8e1a --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/examples/dragdrop-test/frontend/src/app.css b/v2/examples/dragdrop-test/frontend/src/app.css new file mode 100644 index 000000000..1d3b595bc --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/src/app.css @@ -0,0 +1,229 @@ +/* #app styles are in style.css to avoid conflicts */ + +.compact-container { + display: flex; + gap: 15px; + margin: 15px 0; + justify-content: center; + align-items: flex-start; +} + +.drag-source { + background: white; + border: 2px solid #5c6bc0; + padding: 12px; + min-width: 140px; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.drag-source h4 { + color: #3949ab; + margin: 0 0 8px 0; + font-size: 14px; +} + +.draggable { + background: #f5f5f5; + color: #1a1a1a; + padding: 8px; + margin: 6px 0; + border-radius: 4px; + cursor: move; + text-align: center; + transition: all 0.3s ease; + font-weight: 600; + font-size: 14px; + border: 2px solid #c5cae9; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.draggable:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + background: #e8eaf6; + border-color: #7986cb; +} + +.draggable.dragging { + opacity: 0.5; + transform: scale(0.95); + background: #c5cae9; +} + +.drop-zone { + background: #f8f9fa; + border: 2px dashed #9e9e9e; + padding: 12px; + min-width: 180px; + min-height: 120px; + border-radius: 6px; + transition: all 0.3s ease; +} + +.drop-zone h4 { + color: #5c6bc0; + margin: 0 0 8px 0; + font-size: 14px; +} + +.drop-zone.drag-over { + background: #e3f2fd; + border-color: #2196F3; + transform: scale(1.02); + box-shadow: 0 4px 12px rgba(33, 150, 243, 0.2); +} + +.file-drop-zone { + background: #fff8e1; + border: 2px dashed #ffc107; + padding: 12px; + min-width: 180px; + min-height: 120px; + border-radius: 6px; + transition: all 0.3s ease; +} + +.file-drop-zone h4 { + color: #f57c00; + margin: 0 0 8px 0; + font-size: 14px; +} + +.file-drop-zone.drag-over { + background: #fff3cd; + border-color: #ff9800; + transform: scale(1.02); + box-shadow: 0 4px 12px rgba(255, 152, 0, 0.2); +} + +.dropped-item { + background: linear-gradient(135deg, #42a5f5 0%, #66bb6a 100%); + color: white; + padding: 6px 8px; + margin: 4px 2px; + border-radius: 4px; + text-align: center; + animation: slideIn 0.3s ease; + display: inline-block; + font-weight: 500; + font-size: 13px; +} + +.dropped-file { + background: #fff; + border: 2px solid #ff9800; + color: #333; + padding: 6px 8px; + margin: 4px 0; + border-radius: 4px; + text-align: left; + animation: slideIn 0.3s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + font-size: 13px; +} + +#dropMessage, #fileDropMessage { + font-size: 12px; + color: #666; + margin: 4px 0; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.status { + margin: 15px auto; + max-width: 1000px; + padding: 12px; + background: #2c3e50; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.status h4 { + color: white; + margin: 0 0 8px 0; + font-size: 14px; +} + +#eventLog { + background: #1a1a1a; + padding: 10px; + border-radius: 4px; + max-height: 200px; + overflow-y: auto; + font-family: 'Courier New', monospace; + text-align: left; + font-size: 12px; +} + +.log-entry { + padding: 4px 8px; + font-size: 13px; + margin: 2px 0; + border-radius: 3px; +} + +.log-entry.drag-start { + color: #81c784; + background: rgba(129, 199, 132, 0.1); +} + +.log-entry.drag-over { + color: #64b5f6; + background: rgba(100, 181, 246, 0.1); +} + +.log-entry.drag-enter { + color: #ffb74d; + background: rgba(255, 183, 77, 0.1); +} + +.log-entry.drag-leave { + color: #ba68c8; + background: rgba(186, 104, 200, 0.1); +} + +.log-entry.drop { + color: #e57373; + background: rgba(229, 115, 115, 0.1); + font-weight: bold; +} + +.log-entry.drag-end { + color: #90a4ae; + background: rgba(144, 164, 174, 0.1); +} + +.log-entry.file-drop { + color: #ffc107; + background: rgba(255, 193, 7, 0.1); + font-weight: bold; +} + +.log-entry.page-loaded { + color: #4caf50; + background: rgba(76, 175, 80, 0.1); +} + +.log-entry.wails-status { + color: #00bcd4; + background: rgba(0, 188, 212, 0.1); +} + +h1 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-size: 1.8em; + margin: 10px 0 8px 0; +} \ No newline at end of file diff --git a/v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt b/v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/examples/dragdrop-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/examples/dragdrop-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/examples/dragdrop-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/examples/dragdrop-test/frontend/src/assets/images/logo-universal.png b/v2/examples/dragdrop-test/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..d63303bfa Binary files /dev/null and b/v2/examples/dragdrop-test/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/examples/dragdrop-test/frontend/src/main.js b/v2/examples/dragdrop-test/frontend/src/main.js new file mode 100644 index 000000000..60d76ac0f --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/src/main.js @@ -0,0 +1,231 @@ +import './style.css'; +import './app.css'; + +// CRITICAL: Register global handlers IMMEDIATELY to prevent file drops from opening new windows +// This must be done as early as possible, before any other code runs +(function() { + // Helper function to check if drag event contains files + function isFileDrop(e) { + return e.dataTransfer && e.dataTransfer.types && + (e.dataTransfer.types.includes('Files') || + Array.from(e.dataTransfer.types).includes('Files')); + } + + // Global dragover handler - MUST prevent default for file drops + window.addEventListener('dragover', function(e) { + if (isFileDrop(e)) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + } + }, true); // Use capture phase to handle before any other handlers + + // Global drop handler - MUST prevent default for file drops + window.addEventListener('drop', function(e) { + if (isFileDrop(e)) { + e.preventDefault(); + console.log('Global handler prevented file drop navigation'); + } + }, true); // Use capture phase to handle before any other handlers + + // Global dragleave handler + window.addEventListener('dragleave', function(e) { + if (isFileDrop(e)) { + e.preventDefault(); + } + }, true); // Use capture phase + + console.log('Global file drop prevention handlers registered'); +})(); + +document.querySelector('#app').innerHTML = ` +

Wails Drag & Drop Test

+ +
+
+

HTML5 Source

+
Item 1
+
Item 2
+
Item 3
+
+ +
+

HTML5 Drop

+

Drop here

+
+ +
+

File Drop

+

Drop files here

+
+
+ +
+

Event Log

+
+
+`; + +// Get all draggable items and drop zones +const draggables = document.querySelectorAll('.draggable'); +const dropZone = document.getElementById('dropZone'); +const fileDropZone = document.getElementById('fileDropZone'); +const eventLog = document.getElementById('eventLog'); +const dropMessage = document.getElementById('dropMessage'); +const fileDropMessage = document.getElementById('fileDropMessage'); + +let draggedItem = null; +let eventCounter = 0; + +// Function to log events +function logEvent(eventName, details = '') { + eventCounter++; + const timestamp = new Date().toLocaleTimeString(); + const logEntry = document.createElement('div'); + logEntry.className = `log-entry ${eventName.replace(' ', '-').toLowerCase()}`; + logEntry.textContent = `[${timestamp}] ${eventCounter}. ${eventName} ${details}`; + eventLog.insertBefore(logEntry, eventLog.firstChild); + + // Keep only last 20 events + while (eventLog.children.length > 20) { + eventLog.removeChild(eventLog.lastChild); + } + + console.log(`Event: ${eventName} ${details}`); +} + +// Add event listeners to draggable items +draggables.forEach(item => { + // Drag start + item.addEventListener('dragstart', (e) => { + draggedItem = e.target; + e.target.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'copy'; + e.dataTransfer.setData('text/plain', e.target.dataset.item); + logEvent('drag-start', `- Started dragging: ${e.target.dataset.item}`); + }); + + // Drag end + item.addEventListener('dragend', (e) => { + e.target.classList.remove('dragging'); + logEvent('drag-end', `- Ended dragging: ${e.target.dataset.item}`); + }); +}); + +// Add event listeners to HTML drop zone +dropZone.addEventListener('dragenter', (e) => { + e.preventDefault(); + dropZone.classList.add('drag-over'); + logEvent('drag-enter', '- Entered HTML drop zone'); +}); + +dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + // Don't log every dragover to avoid spam +}); + +dropZone.addEventListener('dragleave', (e) => { + if (e.target === dropZone) { + dropZone.classList.remove('drag-over'); + logEvent('drag-leave', '- Left HTML drop zone'); + } +}); + +dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('drag-over'); + + const data = e.dataTransfer.getData('text/plain'); + logEvent('drop', `- Dropped in HTML zone: ${data}`); + + if (draggedItem) { + // Create a copy of the dragged item + const droppedElement = document.createElement('div'); + droppedElement.className = 'dropped-item'; + droppedElement.textContent = data; + + // Remove the placeholder message if it exists + if (dropMessage) { + dropMessage.style.display = 'none'; + } + + dropZone.appendChild(droppedElement); + } + + draggedItem = null; +}); + +// Add event listeners to file drop zone +fileDropZone.addEventListener('dragenter', (e) => { + e.preventDefault(); + fileDropZone.classList.add('drag-over'); + logEvent('drag-enter', '- Entered file drop zone'); +}); + +fileDropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; +}); + +fileDropZone.addEventListener('dragleave', (e) => { + if (e.target === fileDropZone) { + fileDropZone.classList.remove('drag-over'); + logEvent('drag-leave', '- Left file drop zone'); + } +}); + +fileDropZone.addEventListener('drop', (e) => { + e.preventDefault(); + fileDropZone.classList.remove('drag-over'); + + const files = [...e.dataTransfer.files]; + if (files.length > 0) { + logEvent('file-drop', `- Dropped ${files.length} file(s)`); + + // Hide the placeholder message + if (fileDropMessage) { + fileDropMessage.style.display = 'none'; + } + + // Display dropped files + files.forEach(file => { + const fileElement = document.createElement('div'); + fileElement.className = 'dropped-file'; + + // Format file size + let size = file.size; + let unit = 'bytes'; + if (size > 1024 * 1024) { + size = (size / (1024 * 1024)).toFixed(2); + unit = 'MB'; + } else if (size > 1024) { + size = (size / 1024).toFixed(2); + unit = 'KB'; + } + + fileElement.textContent = `📄 ${file.name} (${size} ${unit})`; + fileDropZone.appendChild(fileElement); + }); + } +}); + +// Log when page loads +window.addEventListener('DOMContentLoaded', () => { + logEvent('page-loaded', '- Wails drag-and-drop test page ready'); + console.log('Wails Drag and Drop test application loaded'); + + // Check if Wails drag and drop is enabled + if (window.wails && window.wails.flags) { + logEvent('wails-status', `- Wails DnD enabled: ${window.wails.flags.enableWailsDragAndDrop}`); + } + + // IMPORTANT: Register Wails drag-and-drop handlers to prevent browser navigation + // This will ensure external files don't open in new windows when dropped anywhere + if (window.runtime && window.runtime.OnFileDrop) { + window.runtime.OnFileDrop((x, y, paths) => { + logEvent('wails-file-drop', `- Wails received ${paths.length} file(s) at (${x}, ${y})`); + console.log('Wails OnFileDrop:', paths); + }, false); // false = don't require drop target, handle all file drops + logEvent('wails-setup', '- Wails OnFileDrop handlers registered'); + } +}); \ No newline at end of file diff --git a/v2/examples/dragdrop-test/frontend/src/style.css b/v2/examples/dragdrop-test/frontend/src/style.css new file mode 100644 index 000000000..f5d071597 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/src/style.css @@ -0,0 +1,33 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; + height: 100%; + overflow: hidden; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + height: 100%; + overflow: hidden; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; + overflow: hidden; + box-sizing: border-box; + padding: 10px; +} diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 000000000..02a3bb988 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1:string):Promise; diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.js b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.js new file mode 100644 index 000000000..c71ae77cb --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/runtime/package.json b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..4445dac21 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..7cb89d750 --- /dev/null +++ b/v2/examples/dragdrop-test/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,242 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} \ No newline at end of file diff --git a/v2/examples/dragdrop-test/go.mod b/v2/examples/dragdrop-test/go.mod new file mode 100644 index 000000000..be13aac19 --- /dev/null +++ b/v2/examples/dragdrop-test/go.mod @@ -0,0 +1,37 @@ +module dragdrop-test + +go 1.23 + +require github.com/wailsapp/wails/v2 v2.10.1 + +require ( + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // 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/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // 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 +) + +replace github.com/wailsapp/wails/v2 => E:/releases/wails/v2 diff --git a/v2/examples/dragdrop-test/go.sum b/v2/examples/dragdrop-test/go.sum new file mode 100644 index 000000000..10d4a9b18 --- /dev/null +++ b/v2/examples/dragdrop-test/go.sum @@ -0,0 +1,79 @@ +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +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/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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +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/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +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.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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +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/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/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/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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.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/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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/examples/dragdrop-test/main.go b/v2/examples/dragdrop-test/main.go new file mode 100644 index 000000000..64a0c2734 --- /dev/null +++ b/v2/examples/dragdrop-test/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "Wails Drag & Drop Test", + Width: 800, + Height: 600, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/examples/dragdrop-test/wails.json b/v2/examples/dragdrop-test/wails.json new file mode 100644 index 000000000..7970ea4ca --- /dev/null +++ b/v2/examples/dragdrop-test/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "dragdrop-test", + "outputfilename": "dragdrop-test", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Lea Anthony", + "email": "lea.anthony@gmail.com" + } +} diff --git a/v2/examples/panic-recovery-test/README.md b/v2/examples/panic-recovery-test/README.md new file mode 100644 index 000000000..c0a6a7e5a --- /dev/null +++ b/v2/examples/panic-recovery-test/README.md @@ -0,0 +1,76 @@ +# Panic Recovery Test + +This example demonstrates the Linux signal handler issue (#3965) and verifies the fix using `runtime.ResetSignalHandlers()`. + +## The Problem + +On Linux, WebKit installs signal handlers without the `SA_ONSTACK` flag, which prevents Go from recovering panics caused by nil pointer dereferences (SIGSEGV). Without the fix, the application crashes with: + +``` +signal 11 received but handler not on signal stack +fatal error: non-Go code set up signal handler without SA_ONSTACK flag +``` + +## The Solution + +Call `runtime.ResetSignalHandlers()` immediately before code that might panic: + +```go +import "github.com/wailsapp/wails/v2/pkg/runtime" + +go func() { + defer func() { + if err := recover(); err != nil { + log.Printf("Recovered: %v", err) + } + }() + runtime.ResetSignalHandlers() + // Code that might panic... +}() +``` + +## How to Reproduce + +### Prerequisites + +- Linux with WebKit2GTK 4.1 installed +- Go 1.21+ +- Wails CLI + +### Steps + +1. Build the example: + ```bash + cd v2/examples/panic-recovery-test + wails build -tags webkit2_41 + ``` + +2. Run the application: + ```bash + ./build/bin/panic-recovery-test + ``` + +3. Wait ~10 seconds (the app auto-calls `Greet` after 5s, then waits another 5s before the nil pointer dereference) + +### Expected Result (with fix) + +The panic is recovered and you see: +``` +------------------------------"invalid memory address or nil pointer dereference" +``` + +The application continues running. + +### Without the fix + +Comment out the `runtime.ResetSignalHandlers()` call in `app.go` and rebuild. The application will crash with a fatal signal 11 error. + +## Files + +- `app.go` - Contains the `Greet` function that demonstrates panic recovery +- `frontend/src/main.js` - Auto-calls `Greet` after 5 seconds to trigger the test + +## Related + +- Issue: https://github.com/wailsapp/wails/issues/3965 +- Original fix PR: https://github.com/wailsapp/wails/pull/2152 diff --git a/v2/examples/panic-recovery-test/app.go b/v2/examples/panic-recovery-test/app.go new file mode 100644 index 000000000..ceb46e8d5 --- /dev/null +++ b/v2/examples/panic-recovery-test/app.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + go func() { + defer func() { + if err := recover(); err != nil { + fmt.Printf("------------------------------%#v\n", err) + } + }() + time.Sleep(5 * time.Second) + // Fix signal handlers right before potential panic using the Wails runtime + runtime.ResetSignalHandlers() + // Nil pointer dereference - causes SIGSEGV + var t *time.Time + fmt.Println(t.Unix()) + }() + + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/v2/examples/panic-recovery-test/frontend/index.html b/v2/examples/panic-recovery-test/frontend/index.html new file mode 100644 index 000000000..d7aa4e942 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + panic-test + + +
+ + + diff --git a/v2/examples/panic-recovery-test/frontend/package.json b/v2/examples/panic-recovery-test/frontend/package.json new file mode 100644 index 000000000..a1b6f8e1a --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/v2/examples/panic-recovery-test/frontend/src/app.css b/v2/examples/panic-recovery-test/frontend/src/app.css new file mode 100644 index 000000000..59d06f692 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/src/app.css @@ -0,0 +1,54 @@ +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v2/examples/panic-recovery-test/frontend/src/assets/fonts/OFL.txt b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 000000000..9cac04ce8 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v2/examples/panic-recovery-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 000000000..2f9cc5964 Binary files /dev/null and b/v2/examples/panic-recovery-test/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/v2/examples/panic-recovery-test/frontend/src/assets/images/logo-universal.png b/v2/examples/panic-recovery-test/frontend/src/assets/images/logo-universal.png new file mode 100644 index 000000000..d63303bfa Binary files /dev/null and b/v2/examples/panic-recovery-test/frontend/src/assets/images/logo-universal.png differ diff --git a/v2/examples/panic-recovery-test/frontend/src/main.js b/v2/examples/panic-recovery-test/frontend/src/main.js new file mode 100644 index 000000000..ea5e74fc6 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/src/main.js @@ -0,0 +1,55 @@ +import './style.css'; +import './app.css'; + +import logo from './assets/images/logo-universal.png'; +import {Greet} from '../wailsjs/go/main/App'; + +document.querySelector('#app').innerHTML = ` + +
Please enter your name below 👇
+
+ + +
+ +`; +document.getElementById('logo').src = logo; + +let nameElement = document.getElementById("name"); +nameElement.focus(); +let resultElement = document.getElementById("result"); + +// Setup the greet function +window.greet = function () { + // Get name + let name = nameElement.value; + + // Check if the input is empty + if (name === "") return; + + // Call App.Greet(name) + try { + Greet(name) + .then((result) => { + // Update result with data back from App.Greet() + resultElement.innerText = result; + }) + .catch((err) => { + console.error(err); + }); + } catch (err) { + console.error(err); + } +}; + +// Auto-call Greet after 5 seconds to trigger the panic test +setTimeout(() => { + console.log("Auto-calling Greet to trigger panic test..."); + Greet("PanicTest") + .then((result) => { + resultElement.innerText = result + " (auto-called - panic will occur in 5s)"; + }) + .catch((err) => { + console.error("Error:", err); + }); +}, 5000); diff --git a/v2/examples/panic-recovery-test/frontend/src/style.css b/v2/examples/panic-recovery-test/frontend/src/style.css new file mode 100644 index 000000000..3940d6c63 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/src/style.css @@ -0,0 +1,26 @@ +html { + background-color: rgba(27, 38, 54, 1); + text-align: center; + color: white; +} + +body { + margin: 0; + color: white; + font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; +} + +@font-face { + font-family: "Nunito"; + font-style: normal; + font-weight: 400; + src: local(""), + url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2"); +} + +#app { + height: 100vh; + text-align: center; +} diff --git a/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.d.ts b/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.d.ts new file mode 100755 index 000000000..02a3bb988 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1:string):Promise; diff --git a/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.js b/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.js new file mode 100755 index 000000000..c71ae77cb --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/package.json b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/package.json new file mode 100644 index 000000000..1e7c8a5d7 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.d.ts b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 000000000..4445dac21 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.js b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 000000000..7cb89d750 --- /dev/null +++ b/v2/examples/panic-recovery-test/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,242 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} \ No newline at end of file diff --git a/v2/examples/panic-recovery-test/go.mod b/v2/examples/panic-recovery-test/go.mod new file mode 100644 index 000000000..026042cbf --- /dev/null +++ b/v2/examples/panic-recovery-test/go.mod @@ -0,0 +1,5 @@ +module panic-recovery-test + +go 1.21 + +require github.com/wailsapp/wails/v2 v2.11.0 diff --git a/v2/examples/panic-recovery-test/main.go b/v2/examples/panic-recovery-test/main.go new file mode 100644 index 000000000..f6a38e86c --- /dev/null +++ b/v2/examples/panic-recovery-test/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "panic-test", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/v2/examples/panic-recovery-test/wails.json b/v2/examples/panic-recovery-test/wails.json new file mode 100644 index 000000000..56770f091 --- /dev/null +++ b/v2/examples/panic-recovery-test/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "panic-recovery-test", + "outputfilename": "panic-recovery-test", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Lea Anthony", + "email": "lea.anthony@gmail.com" + } +} diff --git a/v2/go.mod b/v2/go.mod index 60bf75abc..f1287bde7 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -1,97 +1,113 @@ module github.com/wailsapp/wails/v2 -go 1.18 +go 1.22.0 require ( github.com/Masterminds/semver v1.5.0 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/bep/debounce v1.2.1 - github.com/bitfield/script v0.19.0 - github.com/charmbracelet/glamour v0.5.0 - github.com/flytam/filenamify v1.0.0 - github.com/fsnotify/fsnotify v1.4.9 - github.com/go-git/go-git/v5 v5.3.0 - github.com/go-ole/go-ole v1.2.6 + github.com/bitfield/script v0.24.0 + github.com/charmbracelet/glamour v0.8.0 + github.com/flytam/filenamify v1.2.0 + github.com/fsnotify/fsnotify v1.9.0 + github.com/go-git/go-git/v5 v5.13.2 + github.com/go-ole/go-ole v1.3.0 + github.com/godbus/dbus/v5 v5.1.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 github.com/jackmordaunt/icns v1.0.0 - github.com/labstack/echo/v4 v4.10.2 - github.com/labstack/gommon v0.4.0 + github.com/jaypipes/ghw v0.21.3 + github.com/labstack/echo/v4 v4.13.3 + github.com/labstack/gommon v0.4.2 github.com/leaanthony/clir v1.3.0 github.com/leaanthony/debme v1.2.1 - github.com/leaanthony/go-ansi-parser v1.6.0 - github.com/leaanthony/gosod v1.0.3 + github.com/leaanthony/go-ansi-parser v1.6.1 + github.com/leaanthony/gosod v1.0.4 github.com/leaanthony/slicer v1.6.0 - github.com/leaanthony/u v1.1.0 + github.com/leaanthony/u v1.1.1 github.com/leaanthony/winicon v1.0.0 - github.com/matryer/is v1.4.0 - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 + github.com/matryer/is v1.4.1 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/errors v0.9.1 - github.com/pterm/pterm v0.12.49 + github.com/pterm/pterm v0.12.80 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 - github.com/samber/lo v1.38.1 - github.com/stretchr/testify v1.8.1 - github.com/tc-hib/winres v0.1.5 - github.com/tidwall/sjson v1.1.7 - github.com/tkrajina/go-reflector v0.5.6 - github.com/wailsapp/go-webview2 v1.0.7 + github.com/samber/lo v1.49.1 + github.com/stretchr/testify v1.10.0 + github.com/tc-hib/winres v0.3.1 + github.com/tidwall/sjson v1.2.5 + github.com/tkrajina/go-reflector v0.5.8 + github.com/wailsapp/go-webview2 v1.0.22 github.com/wailsapp/mimetype v1.4.1 github.com/wzshiming/ctc v1.2.3 - golang.org/x/mod v0.12.0 - golang.org/x/net v0.10.0 - golang.org/x/sys v0.8.0 - golang.org/x/tools v0.6.0 + golang.org/x/mod v0.23.0 + golang.org/x/net v0.35.0 + golang.org/x/sys v0.30.0 + golang.org/x/tools v0.30.0 ) require ( - atomicgo.dev/cursor v0.1.1 // indirect - atomicgo.dev/keyboard v0.2.8 // indirect - bitbucket.org/creachadair/shell v0.0.7 // indirect - github.com/Microsoft/go-winio v0.4.16 // indirect - github.com/alecthomas/chroma v0.10.0 // indirect + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect + dario.cat/mergo v1.0.0 // indirect + git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.1.5 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/charmbracelet/lipgloss v0.12.1 // indirect + github.com/charmbracelet/x/ansi v0.1.4 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/console v1.0.3 // indirect + github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.4.0 // indirect - github.com/emirpasic/gods v1.12.0 // indirect - github.com/go-git/gcfg v1.5.0 // indirect - github.com/go-git/go-billy/v5 v5.2.0 // indirect - github.com/gookit/color v1.5.2 // indirect - github.com/gorilla/css v1.0.0 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/dlclark/regexp2 v1.11.0 // 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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/itchyny/gojq v0.12.13 // indirect + github.com/itchyny/timefmt-go v0.1.5 // indirect + github.com/jaypipes/pcidb v1.1.1 // 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 v0.0.0-20201106050909-4977a11b4351 // indirect - github.com/kr/pretty v0.3.0 // indirect - github.com/lithammer/fuzzysearch v1.1.5 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/microcosm-cc/bluemonday v1.0.17 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.9.0 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/sergi/go-diff v1.2.0 // indirect - github.com/tidwall/gjson v1.9.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect + github.com/tidwall/gjson v1.14.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae // indirect - github.com/xanzy/ssh-agent v0.3.0 // indirect - github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect - github.com/yuin/goldmark v1.4.13 // indirect - github.com/yuin/goldmark-emoji v1.0.1 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect - golang.org/x/image v0.5.0 // indirect - golang.org/x/term v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.4 // indirect + github.com/yuin/goldmark-emoji v1.0.3 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/image v0.12.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect + mvdan.cc/sh/v3 v3.7.0 // indirect ) diff --git a/v2/go.sum b/v2/go.sum index 7769626fe..2cfe9f7ab 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,9 +1,15 @@ -atomicgo.dev/cursor v0.1.1 h1:0t9sxQomCTRh5ug+hAMCs59x/UmC9QL6Ci5uosINKD4= -atomicgo.dev/cursor v0.1.1/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= -atomicgo.dev/keyboard v0.2.8 h1:Di09BitwZgdTV1hPyX/b9Cqxi8HVuJQwWivnZUEqlj4= -atomicgo.dev/keyboard v0.2.8/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= -bitbucket.org/creachadair/shell v0.0.7 h1:Z96pB6DkSb7F3Y3BBnJeOZH2gazyMTWlvecSD4vDqfk= -bitbucket.org/creachadair/shell v0.0.7/go.mod h1:oqtXSSvSYr4624lnnabXHaBsYW6RD80caLi2b3hJk0U= +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -11,154 +17,179 @@ github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSr github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= -github.com/MarvinJWendt/testza v0.4.3 h1:u2XaM4IqGp9dsdUmML8/Z791fu4yjQYzOiufOtJwTII= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -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= +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/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= +github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/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/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= -github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= -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/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +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/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/bitfield/script v0.19.0 h1:W24f+FQuPab9gXcW8bhcbo5qO8AtrXyu3XOnR4zhHN0= -github.com/bitfield/script v0.19.0/go.mod h1:ana6F8YOSZ3ImT8SauIzuYSqXgFVkSUJ6kgja+WMmIY= -github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g= -github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc= +github.com/bitfield/script v0.24.0 h1:ic0Tbx+2AgRtkGGIcUyr+Un60vu4WXvqFrCSumf+T7M= +github.com/bitfield/script v0.24.0/go.mod h1:fv+6x4OzVsRs6qAlc7wiGq8fq1b5orhtQdtW0dwjUHI= +github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= +github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= +github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= +github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= +github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= +github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4 h1:6KzMkQeAF56rggw2NZu1L+TH7j9+DM1/2Kmh7KUxg1I= +github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= +github.com/cyphar/filepath-securejoin v0.3.6/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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -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/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/flytam/filenamify v1.0.0 h1:ewx6BY2dj7U6h2zGPJmt33q/BjkSf/YsY/woQvnUNIs= -github.com/flytam/filenamify v1.0.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -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/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-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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +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/flytam/filenamify v1.2.0 h1:7RiSqXYR4cJftDQ5NuvljKMfd/ubKnW/j9C6iekChgI= +github.com/flytam/filenamify v1.2.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +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.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +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/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/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= -github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= -github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -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/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= +github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= +github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= +github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ= github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= +github.com/jaypipes/ghw v0.21.3 h1:v5mUHM+RN854Vqmk49Uh213jyUA4+8uqaRajlYESsh8= +github.com/jaypipes/ghw v0.21.3/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE= +github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro= +github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/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/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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +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.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 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.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= -github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= github.com/leaanthony/clir v1.3.0 h1:L9nPDWrmc/qU9UWZZvRaFajWYuO0np9V5p+5gxyYno0= github.com/leaanthony/clir v1.3.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= -github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg= -github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= -github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= -github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= +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/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= -github.com/leaanthony/u v1.0.1 h1:tJLpf9EsuCgSB02ojRxg8KRqMgRN6mCTvFwI55kxRFE= -github.com/leaanthony/u v1.0.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= -github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI= -github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ= github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU= -github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c= -github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +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/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y= -github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= -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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= -github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -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/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= @@ -170,161 +201,153 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.49 h1:qeNm0wTWawy6WhKoY8ZKq6qTXFr0s2UtUyRW0yVztEg= -github.com/pterm/pterm v0.12.49/go.mod h1:D4OBoWNqAfXkm5QLTjIgjNiMXPHemLJHnIreGUsWzWg= +github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= +github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +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/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= -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/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.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +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.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= 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/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/tc-hib/winres v0.1.5 h1:2dA5yfjdoEA3UyRaOC92HNMt3jap66pLzoW4MjpC/0M= -github.com/tc-hib/winres v0.1.5/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A= -github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= -github.com/tidwall/gjson v1.9.3 h1:hqzS9wAHMO+KVBBkLxYdkEeeFHuqr95GfClRLKlgK0E= -github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +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/tc-hib/winres v0.3.1 h1:CwRjEGrKdbi5CvZ4ID+iyVhgyfatxFoizjPhzez9Io4= +github.com/tc-hib/winres v0.3.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= +github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= +github.com/tidwall/gjson v1.14.2/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.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 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.1.7 h1:sgVPwu/yygHJ2m1pJDLgGM/h+1F5odx5Q9ljG3imRm8= -github.com/tidwall/sjson v1.1.7/go.mod h1:w/yG+ezBeTdUxiKs5NcPicO9diP38nk96QBAbIIGeFs= -github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= -github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/wailsapp/go-webview2 v1.0.7 h1:s95+7irJsAsTy1RsjJ6N0cYX7tZ4gP7Uzawds0L2urs= -github.com/wailsapp/go-webview2 v1.0.7/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/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/wzshiming/ctc v1.2.3 h1:q+hW3IQNsjIlOFBTGZZZeIXTElFM4grF4spW/errh/c= github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28= github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae h1:tpXvBXC3hpQBDCc9OojJZCQMVRAbT3TTdUMP8WguXkY= github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20= -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/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +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/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= -github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= +github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -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/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +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-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= -golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= +golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.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/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 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 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-20210330210617-4fbd30eecc44/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.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.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +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= 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 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/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.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 h1:eeH1AIcPvSc0Z25ThsYF+Xoqbn0CI/YnXVYoTLFdGQw= +howett.net/plist v1.0.2-0.20250314012144-ee69052608d9/go.mod h1:fyFX5Hj5tP1Mpk8obqA9MZgXT416Q5711SDT7dQLTLk= +mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= +mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= diff --git a/v2/internal/app/app_bindings.go b/v2/internal/app/app_bindings.go index d079790aa..be031819c 100644 --- a/v2/internal/app/app_bindings.go +++ b/v2/internal/app/app_bindings.go @@ -31,6 +31,7 @@ func (a *App) Run() error { var tsPrefixFlag *string var tsPostfixFlag *string + var tsOutputTypeFlag *string tsPrefix := os.Getenv("tsprefix") if tsPrefix == "" { @@ -42,6 +43,11 @@ func (a *App) Run() error { tsPostfixFlag = bindingFlags.String("tssuffix", "", "Suffix for generated typescript entities") } + tsOutputType := os.Getenv("tsoutputtype") + if tsOutputType == "" { + tsOutputTypeFlag = bindingFlags.String("tsoutputtype", "", "Output type for generated typescript entities (classes|interfaces)") + } + _ = bindingFlags.Parse(os.Args[1:]) if tsPrefixFlag != nil { tsPrefix = *tsPrefixFlag @@ -49,11 +55,15 @@ func (a *App) Run() error { if tsPostfixFlag != nil { tsSuffix = *tsPostfixFlag } + if tsOutputTypeFlag != nil { + tsOutputType = *tsOutputTypeFlag + } - appBindings := binding.NewBindings(a.logger, a.options.Bind, bindingExemptions, IsObfuscated()) + appBindings := binding.NewBindings(a.logger, a.options.Bind, bindingExemptions, IsObfuscated(), a.options.EnumBind) appBindings.SetTsPrefix(tsPrefix) appBindings.SetTsSuffix(tsSuffix) + appBindings.SetOutputType(tsOutputType) err := generateBindings(appBindings) if err != nil { diff --git a/v2/internal/app/app_dev.go b/v2/internal/app/app_dev.go index 51e3e63ba..6de845f96 100644 --- a/v2/internal/app/app_dev.go +++ b/v2/internal/app/app_dev.go @@ -46,7 +46,12 @@ func CreateApp(appoptions *options.App) (*App, error) { ctx = context.WithValue(ctx, "debug", true) ctx = context.WithValue(ctx, "devtoolsEnabled", true) - // Set up logger + // Set up logger if the appoptions.LogLevel is an invalid value, set it to the default log level + appoptions.LogLevel, err = pkglogger.StringToLogLevel(appoptions.LogLevel.String()) + if err != nil { + return nil, err + } + myLogger := logger.New(appoptions.Logger) myLogger.SetLogLevel(appoptions.LogLevel) @@ -74,9 +79,11 @@ func CreateApp(appoptions *options.App) (*App, error) { } loglevel := os.Getenv("loglevel") - if loglevel == "" { - loglevelFlag = devFlags.String("loglevel", "debug", "Loglevel to use - Trace, Debug, Info, Warning, Error") + appLogLevel := appoptions.LogLevel.String() + if loglevel != "" { + appLogLevel = loglevel } + loglevelFlag = devFlags.String("loglevel", appLogLevel, "Loglevel to use - Trace, Debug, Info, Warning, Error") // If we weren't given the assetdir in the environment variables if assetdir == "" { @@ -174,7 +181,10 @@ func CreateApp(appoptions *options.App) (*App, error) { if err != nil { return nil, err } - myLogger.SetLogLevel(level) + // Only set the log level if it's different from the appoptions.LogLevel + if level != appoptions.LogLevel { + myLogger.SetLogLevel(level) + } } // Attach logger to context @@ -209,11 +219,11 @@ func CreateApp(appoptions *options.App) (*App, error) { appoptions.OnDomReady, appoptions.OnBeforeClose, } - appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false) + appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false, appoptions.EnumBind) eventHandler := runtime.NewEvents(myLogger) ctx = context.WithValue(ctx, "events", eventHandler) - messageDispatcher := dispatcher.NewDispatcher(ctx, myLogger, appBindings, eventHandler, appoptions.ErrorFormatter) + messageDispatcher := dispatcher.NewDispatcher(ctx, myLogger, appBindings, eventHandler, appoptions.ErrorFormatter, appoptions.DisablePanicRecovery) // Create the frontends and register to event handler desktopFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher) diff --git a/v2/internal/app/app_devtools_not.go b/v2/internal/app/app_devtools_not.go index d37ee3124..912672048 100644 --- a/v2/internal/app/app_devtools_not.go +++ b/v2/internal/app/app_devtools_not.go @@ -1,8 +1,9 @@ -//go:build !devtools - -package app - -// Note: devtools flag is also added in debug builds -func IsDevtoolsEnabled() bool { - return false -} +//go:build !devtools + +package app + +// IsDevtoolsEnabled returns true if devtools should be enabled +// Note: devtools flag is also added in debug builds +func IsDevtoolsEnabled() bool { + return false +} diff --git a/v2/internal/app/app_production.go b/v2/internal/app/app_production.go index 96ed84e30..9eb0e5a66 100644 --- a/v2/internal/app/app_production.go +++ b/v2/internal/app/app_production.go @@ -72,7 +72,7 @@ func CreateApp(appoptions *options.App) (*App, error) { appoptions.OnDomReady, appoptions.OnBeforeClose, } - appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated()) + appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated(), appoptions.EnumBind) eventHandler := runtime.NewEvents(myLogger) ctx = context.WithValue(ctx, "events", eventHandler) // Attach logger to context @@ -82,7 +82,7 @@ func CreateApp(appoptions *options.App) (*App, error) { ctx = context.WithValue(ctx, "buildtype", "production") } - messageDispatcher := dispatcher.NewDispatcher(ctx, myLogger, appBindings, eventHandler, appoptions.ErrorFormatter) + messageDispatcher := dispatcher.NewDispatcher(ctx, myLogger, appBindings, eventHandler, appoptions.ErrorFormatter, appoptions.DisablePanicRecovery) appFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher) eventHandler.AddFrontend(appFrontend) diff --git a/v2/internal/binding/binding.go b/v2/internal/binding/binding.go old mode 100755 new mode 100644 index 75b821f29..b7bf07ae0 --- a/v2/internal/binding/binding.go +++ b/v2/internal/binding/binding.go @@ -14,6 +14,7 @@ import ( "github.com/wailsapp/wails/v2/internal/typescriptify" "github.com/leaanthony/slicer" + "github.com/wailsapp/wails/v2/internal/logger" ) @@ -23,17 +24,20 @@ type Bindings struct { exemptions slicer.StringSlicer structsToGenerateTS map[string]map[string]interface{} + enumsToGenerateTS map[string]map[string]interface{} tsPrefix string tsSuffix string + tsInterface bool obfuscate bool } // NewBindings returns a new Bindings object -func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool) *Bindings { +func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool, enumsToBind []interface{}) *Bindings { result := &Bindings{ db: newDB(), logger: logger.CustomLogger("Bindings"), structsToGenerateTS: make(map[string]map[string]interface{}), + enumsToGenerateTS: make(map[string]map[string]interface{}), obfuscate: obfuscate, } @@ -47,6 +51,10 @@ func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exem result.exemptions.Add(name) } + for _, enum := range enumsToBind { + result.AddEnumToGenerateTS(enum) + } + // Add the structs to bind for _, ptr := range structPointersToBind { err := result.Add(ptr) @@ -60,20 +68,13 @@ func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exem // Add the given struct methods to the Bindings func (b *Bindings) Add(structPtr interface{}) error { - methods, err := b.getMethods(structPtr) if err != nil { return fmt.Errorf("cannot bind value to app: %s", err.Error()) } for _, method := range methods { - splitName := strings.Split(method.Name, ".") - packageName := splitName[0] - structName := splitName[1] - methodName := splitName[2] - - // Add it as a regular method - b.db.AddMethod(packageName, structName, methodName, method) + b.db.AddMethod(method.Path.Package, method.Path.Struct, method.Path.Name, method) } return nil } @@ -89,16 +90,21 @@ func (b *Bindings) ToJSON() (string, error) { func (b *Bindings) GenerateModels() ([]byte, error) { models := map[string]string{} var seen slicer.StringSlicer + var seenEnumsPackages slicer.StringSlicer allStructNames := b.getAllStructNames() allStructNames.Sort() + allEnumNames := b.getAllEnumNames() + allEnumNames.Sort() for packageName, structsToGenerate := range b.structsToGenerateTS { thisPackageCode := "" w := typescriptify.New() w.WithPrefix(b.tsPrefix) w.WithSuffix(b.tsSuffix) + w.WithInterface(b.tsInterface) w.Namespace = packageName w.WithBackupDir("") w.KnownStructs = allStructNames + w.KnownEnums = allEnumNames // sort the structs var structNames []string for structName := range structsToGenerate { @@ -113,6 +119,28 @@ func (b *Bindings) GenerateModels() ([]byte, error) { structInterface := structsToGenerate[structName] w.Add(structInterface) } + + // if we have enums for this package, add them as well + var enums, enumsExist = b.enumsToGenerateTS[packageName] + if enumsExist { + // Sort the enum names first to make the output deterministic + sortedEnumNames := make([]string, 0, len(enums)) + for enumName := range enums { + sortedEnumNames = append(sortedEnumNames, enumName) + } + sort.Strings(sortedEnumNames) + + for _, enumName := range sortedEnumNames { + enum := enums[enumName] + fqemumname := packageName + "." + enumName + if seen.Contains(fqemumname) { + continue + } + w.AddEnum(enum) + } + seenEnumsPackages.Add(packageName) + } + str, err := w.Convert(nil) if err != nil { return nil, err @@ -122,8 +150,37 @@ func (b *Bindings) GenerateModels() ([]byte, error) { models[packageName] = thisPackageCode } + // Add outstanding enums to the models that were not in packages with structs + for packageName, enumsToGenerate := range b.enumsToGenerateTS { + if seenEnumsPackages.Contains(packageName) { + continue + } + + thisPackageCode := "" + w := typescriptify.New() + w.WithPrefix(b.tsPrefix) + w.WithSuffix(b.tsSuffix) + w.WithInterface(b.tsInterface) + w.Namespace = packageName + w.WithBackupDir("") + + for enumName, enum := range enumsToGenerate { + fqemumname := packageName + "." + enumName + if seen.Contains(fqemumname) { + continue + } + w.AddEnum(enum) + } + str, err := w.Convert(nil) + if err != nil { + return nil, err + } + thisPackageCode += str + models[packageName] = thisPackageCode + } + // Sort the package names first to make the output deterministic - sortedPackageNames := make([]string, 0) + sortedPackageNames := make([]string, 0, len(models)) for packageName := range models { sortedPackageNames = append(sortedPackageNames, packageName) } @@ -146,7 +203,6 @@ func (b *Bindings) GenerateModels() ([]byte, error) { } func (b *Bindings) WriteModels(modelsDir string) error { - modelsData, err := b.GenerateModels() if err != nil { return err @@ -157,7 +213,7 @@ func (b *Bindings) WriteModels(modelsDir string) error { } filename := filepath.Join(modelsDir, "models.ts") - err = os.WriteFile(filename, modelsData, 0755) + err = os.WriteFile(filename, modelsData, 0o755) if err != nil { return err } @@ -165,6 +221,39 @@ func (b *Bindings) WriteModels(modelsDir string) error { return nil } +func (b *Bindings) AddEnumToGenerateTS(e interface{}) { + enumType := reflect.TypeOf(e) + + var packageName string + var enumName string + // enums should be represented as array of all possible values + if hasElements(enumType) { + enum := enumType.Elem() + // simple enum represented by struct with Value/TSName fields + if enum.Kind() == reflect.Struct { + _, tsNamePresented := enum.FieldByName("TSName") + enumT, valuePresented := enum.FieldByName("Value") + if tsNamePresented && valuePresented { + packageName = getPackageName(enumT.Type.String()) + enumName = enumT.Type.Name() + } else { + return + } + // otherwise expecting implementation with TSName() https://github.com/tkrajina/typescriptify-golang-structs#enums-with-tsname + } else { + packageName = getPackageName(enumType.Elem().String()) + enumName = enumType.Elem().Name() + } + if b.enumsToGenerateTS[packageName] == nil { + b.enumsToGenerateTS[packageName] = make(map[string]interface{}) + } + if b.enumsToGenerateTS[packageName][enumName] != nil { + return + } + b.enumsToGenerateTS[packageName][enumName] = e + } +} + func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) { if b.structsToGenerateTS[packageName] == nil { b.structsToGenerateTS[packageName] = make(map[string]interface{}) @@ -176,22 +265,19 @@ func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, // Iterate this struct and add any struct field references structType := reflect.TypeOf(s) - if hasElements(structType) { + for hasElements(structType) { structType = structType.Elem() } for i := 0; i < structType.NumField(); i++ { field := structType.Field(i) - if field.Anonymous { + if field.Anonymous || !field.IsExported() { continue } kind := field.Type.Kind() if kind == reflect.Struct { - if !field.IsExported() { - continue - } fqname := field.Type.String() - sNameSplit := strings.Split(fqname, ".") + sNameSplit := strings.SplitN(fqname, ".", 2) if len(sNameSplit) < 2 { continue } @@ -202,22 +288,24 @@ func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s := reflect.Indirect(a).Interface() b.AddStructToGenerateTS(pName, sName, s) } - } else if hasElements(field.Type) && field.Type.Elem().Kind() == reflect.Struct { - if !field.IsExported() { - continue + } else { + fType := field.Type + for hasElements(fType) { + fType = fType.Elem() } - fqname := field.Type.Elem().String() - sNameSplit := strings.Split(fqname, ".") - if len(sNameSplit) < 2 { - continue - } - sName := sNameSplit[1] - pName := getPackageName(fqname) - typ := field.Type.Elem() - a := reflect.New(typ) - if b.hasExportedJSONFields(typ) { - s := reflect.Indirect(a).Interface() - b.AddStructToGenerateTS(pName, sName, s) + if fType.Kind() == reflect.Struct { + fqname := fType.String() + sNameSplit := strings.SplitN(fqname, ".", 2) + if len(sNameSplit) < 2 { + continue + } + sName := sNameSplit[1] + pName := getPackageName(fqname) + a := reflect.New(fType) + if b.hasExportedJSONFields(fType) { + s := reflect.Indirect(a).Interface() + b.AddStructToGenerateTS(pName, sName, s) + } } } } @@ -233,6 +321,13 @@ func (b *Bindings) SetTsSuffix(postfix string) *Bindings { return b } +func (b *Bindings) SetOutputType(outputType string) *Bindings { + if outputType == "interfaces" { + b.tsInterface = true + } + return b +} + func (b *Bindings) getAllStructNames() *slicer.StringSlicer { var result slicer.StringSlicer for packageName, structsToGenerate := range b.structsToGenerateTS { @@ -243,11 +338,32 @@ func (b *Bindings) getAllStructNames() *slicer.StringSlicer { return &result } +func (b *Bindings) getAllEnumNames() *slicer.StringSlicer { + var result slicer.StringSlicer + for packageName, enumsToGenerate := range b.enumsToGenerateTS { + for enumName := range enumsToGenerate { + result.Add(packageName + "." + enumName) + } + } + return &result +} + func (b *Bindings) hasExportedJSONFields(typeOf reflect.Type) bool { for i := 0; i < typeOf.NumField(); i++ { jsonFieldName := "" f := typeOf.Field(i) - jsonTag := f.Tag.Get("json") + // function, complex, and channel types cannot be json-encoded + if f.Type.Kind() == reflect.Chan || + f.Type.Kind() == reflect.Func || + f.Type.Kind() == reflect.UnsafePointer || + f.Type.Kind() == reflect.Complex128 || + f.Type.Kind() == reflect.Complex64 { + continue + } + jsonTag, hasTag := f.Tag.Lookup("json") + if !hasTag && f.IsExported() { + return true + } if len(jsonTag) == 0 { continue } diff --git a/v2/internal/binding/binding_test/binding_anonymous_sub_struct_multi_level_test.go b/v2/internal/binding/binding_test/binding_anonymous_sub_struct_multi_level_test.go index 3c888ab27..29777481b 100644 --- a/v2/internal/binding/binding_test/binding_anonymous_sub_struct_multi_level_test.go +++ b/v2/internal/binding/binding_test/binding_anonymous_sub_struct_multi_level_test.go @@ -45,7 +45,7 @@ export namespace binding_test { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { diff --git a/v2/internal/binding/binding_test/binding_anonymous_sub_struct_test.go b/v2/internal/binding/binding_test/binding_anonymous_sub_struct_test.go index 53617efac..11afe4f0d 100644 --- a/v2/internal/binding/binding_test/binding_anonymous_sub_struct_test.go +++ b/v2/internal/binding/binding_test/binding_anonymous_sub_struct_test.go @@ -39,7 +39,7 @@ export namespace binding_test { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { diff --git a/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go b/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go index 2309d6daf..b37334ec3 100644 --- a/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go +++ b/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go @@ -42,7 +42,7 @@ func TestConflictingPackageName(t *testing.T) { // setup testLogger := &logger.Logger{} - b := binding.NewBindings(testLogger, []interface{}{&HandlerTest{}}, []interface{}{}, false) + b := binding.NewBindings(testLogger, []interface{}{&HandlerTest{}}, []interface{}{}, false, []interface{}{}) // then err := b.GenerateGoBindings(generationDir) diff --git a/v2/internal/binding/binding_test/binding_deepelements_test.go b/v2/internal/binding/binding_test/binding_deepelements_test.go new file mode 100644 index 000000000..034687474 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_deepelements_test.go @@ -0,0 +1,126 @@ +package binding_test + +// Issues 2303, 3442, 3709 + +type DeepMessage struct { + Msg string +} + +type DeepElements struct { + Single []int + Double [][]string + FourDouble [4][]float64 + DoubleFour [][4]int64 + Triple [][][]int + + SingleMap map[string]int + SliceMap map[string][]int + DoubleSliceMap map[string][][]int + + ArrayMap map[string][4]int + DoubleArrayMap1 map[string][4][]int + DoubleArrayMap2 map[string][][4]int + DoubleArrayMap3 map[string][4][4]int + + OneStructs []*DeepMessage + TwoStructs [3][]*DeepMessage + ThreeStructs [][][]DeepMessage + MapStructs map[string][]*DeepMessage + MapTwoStructs map[string][4][]DeepMessage + MapThreeStructs map[string][][7][]*DeepMessage +} + +func (x DeepElements) Get() DeepElements { + return x +} + +var DeepElementsTest = BindingTest{ + name: "DeepElements", + structs: []interface{}{ + &DeepElements{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + + export class DeepMessage { + Msg: string; + + static createFrom(source: any = {}) { + return new DeepMessage(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Msg = source["Msg"]; + } + } + export class DeepElements { + Single: number[]; + Double: string[][]; + FourDouble: number[][]; + DoubleFour: number[][]; + Triple: number[][][]; + SingleMap: Record; + SliceMap: Record>; + DoubleSliceMap: Record>>; + ArrayMap: Record>; + DoubleArrayMap1: Record>>; + DoubleArrayMap2: Record>>; + DoubleArrayMap3: Record>>; + OneStructs: DeepMessage[]; + TwoStructs: DeepMessage[][]; + ThreeStructs: DeepMessage[][][]; + MapStructs: Record>; + MapTwoStructs: Record>>; + MapThreeStructs: Record>>>; + + static createFrom(source: any = {}) { + return new DeepElements(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Single = source["Single"]; + this.Double = source["Double"]; + this.FourDouble = source["FourDouble"]; + this.DoubleFour = source["DoubleFour"]; + this.Triple = source["Triple"]; + this.SingleMap = source["SingleMap"]; + this.SliceMap = source["SliceMap"]; + this.DoubleSliceMap = source["DoubleSliceMap"]; + this.ArrayMap = source["ArrayMap"]; + this.DoubleArrayMap1 = source["DoubleArrayMap1"]; + this.DoubleArrayMap2 = source["DoubleArrayMap2"]; + this.DoubleArrayMap3 = source["DoubleArrayMap3"]; + this.OneStructs = this.convertValues(source["OneStructs"], DeepMessage); + this.TwoStructs = this.convertValues(source["TwoStructs"], DeepMessage); + this.ThreeStructs = this.convertValues(source["ThreeStructs"], DeepMessage); + this.MapStructs = this.convertValues(source["MapStructs"], Array, true); + this.MapTwoStructs = this.convertValues(source["MapTwoStructs"], Array>, true); + this.MapThreeStructs = this.convertValues(source["MapThreeStructs"], Array>>, true); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + } +`, +} diff --git a/v2/internal/binding/binding_test/binding_emptystruct_test.go b/v2/internal/binding/binding_test/binding_emptystruct_test.go index c36603e64..ffb85e865 100644 --- a/v2/internal/binding/binding_test/binding_emptystruct_test.go +++ b/v2/internal/binding/binding_test/binding_emptystruct_test.go @@ -34,7 +34,7 @@ export namespace binding_test { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { diff --git a/v2/internal/binding/binding_test/binding_enum_ordering_test.go b/v2/internal/binding/binding_test/binding_enum_ordering_test.go new file mode 100644 index 000000000..0939535ec --- /dev/null +++ b/v2/internal/binding/binding_test/binding_enum_ordering_test.go @@ -0,0 +1,271 @@ +package binding_test + +// Test for PR #4664: Fix generated enums ordering +// This test ensures that enum output is deterministic regardless of map iteration order + +// ZFirstEnum - named with Z prefix to test alphabetical sorting +type ZFirstEnum int + +const ( + ZFirstEnumValue1 ZFirstEnum = iota + ZFirstEnumValue2 +) + +var AllZFirstEnumValues = []struct { + Value ZFirstEnum + TSName string +}{ + {ZFirstEnumValue1, "ZValue1"}, + {ZFirstEnumValue2, "ZValue2"}, +} + +// ASecondEnum - named with A prefix to test alphabetical sorting +type ASecondEnum int + +const ( + ASecondEnumValue1 ASecondEnum = iota + ASecondEnumValue2 +) + +var AllASecondEnumValues = []struct { + Value ASecondEnum + TSName string +}{ + {ASecondEnumValue1, "AValue1"}, + {ASecondEnumValue2, "AValue2"}, +} + +// MMiddleEnum - named with M prefix to test alphabetical sorting +type MMiddleEnum int + +const ( + MMiddleEnumValue1 MMiddleEnum = iota + MMiddleEnumValue2 +) + +var AllMMiddleEnumValues = []struct { + Value MMiddleEnum + TSName string +}{ + {MMiddleEnumValue1, "MValue1"}, + {MMiddleEnumValue2, "MValue2"}, +} + +type EntityWithMultipleEnums struct { + Name string `json:"name"` + EnumZ ZFirstEnum `json:"enumZ"` + EnumA ASecondEnum `json:"enumA"` + EnumM MMiddleEnum `json:"enumM"` +} + +func (e EntityWithMultipleEnums) Get() EntityWithMultipleEnums { + return e +} + +// EnumOrderingTest tests that multiple enums in the same package are output +// in alphabetical order by enum name. Before PR #4664, the order was +// non-deterministic due to Go map iteration order. +var EnumOrderingTest = BindingTest{ + name: "EnumOrderingTest", + structs: []interface{}{ + &EntityWithMultipleEnums{}, + }, + enums: []interface{}{ + // Intentionally add enums in non-alphabetical order + AllZFirstEnumValues, + AllASecondEnumValues, + AllMMiddleEnumValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "", + TsSuffix: "", + }, + // Expected output should have enums in alphabetical order: ASecondEnum, MMiddleEnum, ZFirstEnum + want: `export namespace binding_test { + + export enum ASecondEnum { + AValue1 = 0, + AValue2 = 1, + } + export enum MMiddleEnum { + MValue1 = 0, + MValue2 = 1, + } + export enum ZFirstEnum { + ZValue1 = 0, + ZValue2 = 1, + } + export class EntityWithMultipleEnums { + name: string; + enumZ: ZFirstEnum; + enumA: ASecondEnum; + enumM: MMiddleEnum; + + static createFrom(source: any = {}) { + return new EntityWithMultipleEnums(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enumZ = source["enumZ"]; + this.enumA = source["enumA"]; + this.enumM = source["enumM"]; + } + } + +} +`, +} + +// EnumElementOrderingEnum tests sorting of enum elements by TSName +type EnumElementOrderingEnum string + +const ( + EnumElementZ EnumElementOrderingEnum = "z_value" + EnumElementA EnumElementOrderingEnum = "a_value" + EnumElementM EnumElementOrderingEnum = "m_value" +) + +// AllEnumElementOrderingValues intentionally lists values out of alphabetical order +// to test that AddEnum sorts them +var AllEnumElementOrderingValues = []struct { + Value EnumElementOrderingEnum + TSName string +}{ + {EnumElementZ, "Zebra"}, + {EnumElementA, "Apple"}, + {EnumElementM, "Mango"}, +} + +type EntityWithUnorderedEnumElements struct { + Name string `json:"name"` + Enum EnumElementOrderingEnum `json:"enum"` +} + +func (e EntityWithUnorderedEnumElements) Get() EntityWithUnorderedEnumElements { + return e +} + +// EnumElementOrderingTest tests that enum elements are sorted alphabetically +// by their TSName within an enum. Before PR #4664, elements appeared in the +// order they were added, which could be arbitrary. +var EnumElementOrderingTest = BindingTest{ + name: "EnumElementOrderingTest", + structs: []interface{}{ + &EntityWithUnorderedEnumElements{}, + }, + enums: []interface{}{ + AllEnumElementOrderingValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "", + TsSuffix: "", + }, + // Expected output should have enum elements sorted: Apple, Mango, Zebra + want: `export namespace binding_test { + + export enum EnumElementOrderingEnum { + Apple = "a_value", + Mango = "m_value", + Zebra = "z_value", + } + export class EntityWithUnorderedEnumElements { + name: string; + enum: EnumElementOrderingEnum; + + static createFrom(source: any = {}) { + return new EntityWithUnorderedEnumElements(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + +} +`, +} + +// TSNameEnumElementOrdering tests sorting with TSName() method enum +type TSNameEnumElementOrdering string + +const ( + TSNameEnumZ TSNameEnumElementOrdering = "z_value" + TSNameEnumA TSNameEnumElementOrdering = "a_value" + TSNameEnumM TSNameEnumElementOrdering = "m_value" +) + +func (v TSNameEnumElementOrdering) TSName() string { + switch v { + case TSNameEnumZ: + return "Zebra" + case TSNameEnumA: + return "Apple" + case TSNameEnumM: + return "Mango" + default: + return "Unknown" + } +} + +// AllTSNameEnumValues intentionally out of order +var AllTSNameEnumValues = []TSNameEnumElementOrdering{TSNameEnumZ, TSNameEnumA, TSNameEnumM} + +type EntityWithTSNameEnumOrdering struct { + Name string `json:"name"` + Enum TSNameEnumElementOrdering `json:"enum"` +} + +func (e EntityWithTSNameEnumOrdering) Get() EntityWithTSNameEnumOrdering { + return e +} + +// TSNameEnumElementOrderingTest tests that enums using TSName() method +// also have their elements sorted alphabetically by the TSName. +var TSNameEnumElementOrderingTest = BindingTest{ + name: "TSNameEnumElementOrderingTest", + structs: []interface{}{ + &EntityWithTSNameEnumOrdering{}, + }, + enums: []interface{}{ + AllTSNameEnumValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "", + TsSuffix: "", + }, + // Expected output should have enum elements sorted: Apple, Mango, Zebra + want: `export namespace binding_test { + + export enum TSNameEnumElementOrdering { + Apple = "a_value", + Mango = "m_value", + Zebra = "z_value", + } + export class EntityWithTSNameEnumOrdering { + name: string; + enum: TSNameEnumElementOrdering; + + static createFrom(source: any = {}) { + return new EntityWithTSNameEnumOrdering(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_generics_test.go b/v2/internal/binding/binding_test/binding_generics_test.go new file mode 100644 index 000000000..920bd2a7a --- /dev/null +++ b/v2/internal/binding/binding_test/binding_generics_test.go @@ -0,0 +1,154 @@ +package binding_test + +import "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/float_package" + +// Issues 3900, 3371, 2323 (no TS generics though) + +type ListData[T interface{}] struct { + Total int64 `json:"Total"` + TotalPage int64 `json:"TotalPage"` + PageNum int `json:"PageNum"` + List []T `json:"List,omitempty"` +} + +func (x ListData[T]) Get() ListData[T] { + return x +} + +var Generics1Test = BindingTest{ + name: "Generics1", + structs: []interface{}{ + &ListData[string]{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + + export class ListData_string_ { + Total: number; + TotalPage: number; + PageNum: number; + List?: string[]; + + static createFrom(source: any = {}) { + return new ListData_string_(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Total = source["Total"]; + this.TotalPage = source["TotalPage"]; + this.PageNum = source["PageNum"]; + this.List = source["List"]; + } + } + + } +`, +} + +var Generics2Test = BindingTest{ + name: "Generics2", + structs: []interface{}{ + &ListData[float_package.SomeStruct]{}, + &ListData[*float_package.SomeStruct]{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + + export class ListData__github_com_wailsapp_wails_v2_internal_binding_binding_test_binding_test_import_float_package_SomeStruct_ { + Total: number; + TotalPage: number; + PageNum: number; + List?: float_package.SomeStruct[]; + + static createFrom(source: any = {}) { + return new ListData__github_com_wailsapp_wails_v2_internal_binding_binding_test_binding_test_import_float_package_SomeStruct_(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Total = source["Total"]; + this.TotalPage = source["TotalPage"]; + this.PageNum = source["PageNum"]; + this.List = this.convertValues(source["List"], float_package.SomeStruct); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + export class ListData_github_com_wailsapp_wails_v2_internal_binding_binding_test_binding_test_import_float_package_SomeStruct_ { + Total: number; + TotalPage: number; + PageNum: number; + List?: float_package.SomeStruct[]; + + static createFrom(source: any = {}) { + return new ListData_github_com_wailsapp_wails_v2_internal_binding_binding_test_binding_test_import_float_package_SomeStruct_(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Total = source["Total"]; + this.TotalPage = source["TotalPage"]; + this.PageNum = source["PageNum"]; + this.List = this.convertValues(source["List"], float_package.SomeStruct); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + + } + + export namespace float_package { + + export class SomeStruct { + string: string; + + static createFrom(source: any = {}) { + return new SomeStruct(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.string = source["string"]; + } + } + + } +`, +} diff --git a/v2/internal/binding/binding_test/binding_ignored_test.go b/v2/internal/binding/binding_test/binding_ignored_test.go new file mode 100644 index 000000000..aeb6a9c3f --- /dev/null +++ b/v2/internal/binding/binding_test/binding_ignored_test.go @@ -0,0 +1,47 @@ +package binding_test + +import ( + "unsafe" +) + +// Issues 3755, 3809 + +type Ignored struct { + Valid bool + Total func() int `json:"Total"` + UnsafeP unsafe.Pointer + Complex64 complex64 `json:"Complex"` + Complex128 complex128 + StringChan chan string +} + +func (x Ignored) Get() Ignored { + return x +} + +var IgnoredTest = BindingTest{ + name: "Ignored", + structs: []interface{}{ + &Ignored{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + + export class Ignored { + Valid: boolean; + + static createFrom(source: any = {}) { + return new Ignored(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Valid = source["Valid"]; + } + } + + } +`, +} diff --git a/v2/internal/binding/binding_test/binding_importedenum_test.go b/v2/internal/binding/binding_test/binding_importedenum_test.go new file mode 100644 index 000000000..5b5b4419e --- /dev/null +++ b/v2/internal/binding/binding_test/binding_importedenum_test.go @@ -0,0 +1,50 @@ +package binding_test + +import "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import" + +type ImportedEnumStruct struct { + EnumValue binding_test_import.ImportedEnum `json:"EnumValue"` +} + +func (s ImportedEnumStruct) Get() ImportedEnumStruct { + return s +} + +var ImportedEnumTest = BindingTest{ + name: "ImportedEnum", + structs: []interface{}{ + &ImportedEnumStruct{}, + }, + enums: []interface{}{ + binding_test_import.AllImportedEnumValues, + }, + exemptions: nil, + shouldError: false, + want: `export namespace binding_test { + + export class ImportedEnumStruct { + EnumValue: binding_test_import.ImportedEnum; + + static createFrom(source: any = {}) { + return new ImportedEnumStruct(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.EnumValue = source["EnumValue"]; + } + } + + } + + export namespace binding_test_import { + + export enum ImportedEnum { + Value1 = "value1", + Value2 = "value2", + Value3 = "value3", + } + + } +`, +} diff --git a/v2/internal/binding/binding_test/binding_importedmap_test.go b/v2/internal/binding/binding_test/binding_importedmap_test.go index 54fb261a8..4a4b2996c 100644 --- a/v2/internal/binding/binding_test/binding_importedmap_test.go +++ b/v2/internal/binding/binding_test/binding_importedmap_test.go @@ -32,7 +32,7 @@ export namespace binding_test { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { @@ -50,7 +50,7 @@ export namespace binding_test { export namespace binding_test_import { export class AMapWrapper { - AMap: {[key: string]: binding_test_nestedimport.A}; + AMap: Record; static createFrom(source: any = {}) { return new AMapWrapper(source); } @@ -62,7 +62,7 @@ export namespace binding_test_import { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { diff --git a/v2/internal/binding/binding_test/binding_importedslice_test.go b/v2/internal/binding/binding_test/binding_importedslice_test.go index b4a63689c..5abf55b43 100644 --- a/v2/internal/binding/binding_test/binding_importedslice_test.go +++ b/v2/internal/binding/binding_test/binding_importedslice_test.go @@ -32,7 +32,7 @@ export namespace binding_test { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { @@ -62,7 +62,7 @@ export namespace binding_test_import { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { diff --git a/v2/internal/binding/binding_test/binding_importedstruct_test.go b/v2/internal/binding/binding_test/binding_importedstruct_test.go index 1629be9fa..1e94453c2 100644 --- a/v2/internal/binding/binding_test/binding_importedstruct_test.go +++ b/v2/internal/binding/binding_test/binding_importedstruct_test.go @@ -33,7 +33,7 @@ export namespace binding_test { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { @@ -63,7 +63,7 @@ export namespace binding_test_import { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { diff --git a/v2/internal/binding/binding_test/binding_nestedfield_test.go b/v2/internal/binding/binding_test/binding_nestedfield_test.go index c2e4fcf9f..66dd11cbf 100644 --- a/v2/internal/binding/binding_test/binding_nestedfield_test.go +++ b/v2/internal/binding/binding_test/binding_nestedfield_test.go @@ -44,7 +44,7 @@ export namespace binding_test { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { diff --git a/v2/internal/binding/binding_test/binding_nonstringmapkey_test.go b/v2/internal/binding/binding_test/binding_nonstringmapkey_test.go index 37a61dd29..9efee710f 100644 --- a/v2/internal/binding/binding_test/binding_nonstringmapkey_test.go +++ b/v2/internal/binding/binding_test/binding_nonstringmapkey_test.go @@ -18,7 +18,7 @@ var NonStringMapKeyTest = BindingTest{ want: ` export namespace binding_test { export class NonStringMapKey { - numberMap: {[key: number]: any}; + numberMap: Record; static createFrom(source: any = {}) { return new NonStringMapKey(source); } diff --git a/v2/internal/binding/binding_test/binding_notags_test.go b/v2/internal/binding/binding_test/binding_notags_test.go new file mode 100644 index 000000000..d4d9997e0 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_notags_test.go @@ -0,0 +1,60 @@ +package binding_test + +type NoFieldTags struct { + Name string + Address string + Zip *string + Spouse *NoFieldTags + NoFunc func() string +} + +func (n NoFieldTags) Get() NoFieldTags { + return n +} + +var NoFieldTagsTest = BindingTest{ + name: "NoFieldTags", + structs: []interface{}{ + &NoFieldTags{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class NoFieldTags { + Name: string; + Address: string; + Zip?: string; + Spouse?: NoFieldTags; + static createFrom(source: any = {}) { + return new NoFieldTags(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Name = source["Name"]; + this.Address = source["Address"]; + this.Zip = source["Zip"]; + this.Spouse = this.convertValues(source["Spouse"], NoFieldTags); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_returned_promises_test.go b/v2/internal/binding/binding_test/binding_returned_promises_test.go index 837d5fad3..94941d0a3 100644 --- a/v2/internal/binding/binding_test/binding_returned_promises_test.go +++ b/v2/internal/binding/binding_test/binding_returned_promises_test.go @@ -59,7 +59,7 @@ func TestPromises(t *testing.T) { // setup testLogger := &logger.Logger{} - b := binding.NewBindings(testLogger, []interface{}{&PromisesTest{}}, []interface{}{}, false) + b := binding.NewBindings(testLogger, []interface{}{&PromisesTest{}}, []interface{}{}, false, []interface{}{}) // then err := b.GenerateGoBindings(generationDir) diff --git a/v2/internal/binding/binding_test/binding_structwithoutfields_test.go b/v2/internal/binding/binding_test/binding_structwithoutfields_test.go new file mode 100644 index 000000000..4b2289b98 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_structwithoutfields_test.go @@ -0,0 +1,34 @@ +package binding_test + +type WithoutFields struct { +} + +func (s WithoutFields) Get() WithoutFields { + return s +} + +var WithoutFieldsTest = BindingTest{ + name: "StructWithoutFields", + structs: []interface{}{ + &WithoutFields{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + + export class WithoutFields { + + + static createFrom(source: any = {}) { + return new WithoutFields(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + + } + } + +}`, +} diff --git a/v2/internal/binding/binding_test/binding_test.go b/v2/internal/binding/binding_test/binding_test.go index c2e351915..41f0618ce 100644 --- a/v2/internal/binding/binding_test/binding_test.go +++ b/v2/internal/binding/binding_test/binding_test.go @@ -13,6 +13,7 @@ import ( type BindingTest struct { name string structs []interface{} + enums []interface{} exemptions []interface{} want string shouldError bool @@ -20,8 +21,9 @@ type BindingTest struct { } type TsGenerationOptionsTest struct { - TsPrefix string - TsSuffix string + TsPrefix string + TsSuffix string + TsOutputType string } func TestBindings_GenerateModels(t *testing.T) { @@ -31,28 +33,45 @@ func TestBindings_GenerateModels(t *testing.T) { ImportedStructTest, ImportedSliceTest, ImportedMapTest, + ImportedEnumTest, NestedFieldTest, NonStringMapKeyTest, SingleFieldTest, MultistructTest, EmptyStructTest, GeneratedJsEntityTest, + GeneratedJsEntityWithIntEnumTest, + GeneratedJsEntityWithStringEnumTest, + GeneratedJsEntityWithEnumTsName, + GeneratedJsEntityWithNestedStructInterfacesTest, AnonymousSubStructTest, AnonymousSubStructMultiLevelTest, GeneratedJsEntityWithNestedStructTest, - EntityWithDiffNamespaces, + EntityWithDiffNamespacesTest, + SpecialCharacterFieldTest, + WithoutFieldsTest, + NoFieldTagsTest, + Generics1Test, + Generics2Test, + IgnoredTest, + DeepElementsTest, + // PR #4664: Enum ordering tests + EnumOrderingTest, + EnumElementOrderingTest, + TSNameEnumElementOrderingTest, } testLogger := &logger.Logger{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := binding.NewBindings(testLogger, tt.structs, tt.exemptions, false) + b := binding.NewBindings(testLogger, tt.structs, tt.exemptions, false, tt.enums) for _, s := range tt.structs { err := b.Add(s) require.NoError(t, err) } b.SetTsPrefix(tt.TsPrefix) b.SetTsSuffix(tt.TsSuffix) + b.SetOutputType(tt.TsOutputType) got, err := b.GenerateModels() if (err != nil) != tt.shouldError { t.Errorf("GenerateModels() error = %v, shouldError %v", err, tt.shouldError) diff --git a/v2/internal/binding/binding_test/binding_test_import/binding_test_import.go b/v2/internal/binding/binding_test/binding_test_import/binding_test_import.go index 6b99d43be..e7080c694 100644 --- a/v2/internal/binding/binding_test/binding_test_import/binding_test_import.go +++ b/v2/internal/binding/binding_test/binding_test_import/binding_test_import.go @@ -13,3 +13,20 @@ type ASliceWrapper struct { type AMapWrapper struct { AMap map[string]binding_test_nestedimport.A `json:"AMap"` } + +type ImportedEnum string + +const ( + ImportedEnumValue1 ImportedEnum = "value1" + ImportedEnumValue2 ImportedEnum = "value2" + ImportedEnumValue3 ImportedEnum = "value3" +) + +var AllImportedEnumValues = []struct { + Value ImportedEnum + TSName string +}{ + {ImportedEnumValue1, "Value1"}, + {ImportedEnumValue2, "Value2"}, + {ImportedEnumValue3, "Value3"}, +} diff --git a/v2/internal/binding/binding_test/binding_tsgeneration_test.go b/v2/internal/binding/binding_test/binding_tsgeneration_test.go index d2c5349c5..850bc778a 100644 --- a/v2/internal/binding/binding_test/binding_tsgeneration_test.go +++ b/v2/internal/binding/binding_test/binding_tsgeneration_test.go @@ -107,7 +107,7 @@ export namespace binding_test { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { @@ -140,7 +140,7 @@ type ChildPackageEntity struct { ImportedPackage binding_test_import.AWrapper `json:"importedPackage"` } -var EntityWithDiffNamespaces = BindingTest{ +var EntityWithDiffNamespacesTest = BindingTest{ name: "EntityWithDiffNamespaces ", structs: []interface{}{ &ParentPackageEntity{}, @@ -172,7 +172,7 @@ export namespace binding_test { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { @@ -204,7 +204,7 @@ export namespace binding_test { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { @@ -239,7 +239,7 @@ export namespace binding_test { if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { @@ -275,3 +275,235 @@ export namespace binding_test { `, } + +type IntEnum int + +const ( + IntEnumValue1 IntEnum = iota + IntEnumValue2 + IntEnumValue3 +) + +var AllIntEnumValues = []struct { + Value IntEnum + TSName string +}{ + {IntEnumValue1, "Value1"}, + {IntEnumValue2, "Value2"}, + {IntEnumValue3, "Value3"}, +} + +type EntityWithIntEnum struct { + Name string `json:"name"` + Enum IntEnum `json:"enum"` +} + +func (e EntityWithIntEnum) Get() EntityWithIntEnum { + return e +} + +var GeneratedJsEntityWithIntEnumTest = BindingTest{ + name: "GeneratedJsEntityWithIntEnumTest", + structs: []interface{}{ + &EntityWithIntEnum{}, + }, + enums: []interface{}{ + AllIntEnumValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + }, + want: `export namespace binding_test { + + export enum MY_PREFIX_IntEnum_MY_SUFFIX { + Value1 = 0, + Value2 = 1, + Value3 = 2, + } + export class MY_PREFIX_EntityWithIntEnum_MY_SUFFIX { + name: string; + enum: MY_PREFIX_IntEnum_MY_SUFFIX; + + static createFrom(source: any = {}) { + return new MY_PREFIX_EntityWithIntEnum_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + + } +`, +} + +type StringEnum string + +const ( + StringEnumValue1 StringEnum = "value1" + StringEnumValue2 StringEnum = "value2" + StringEnumValue3 StringEnum = "value3" +) + +var AllStringEnumValues = []struct { + Value StringEnum + TSName string +}{ + {StringEnumValue1, "Value1"}, + {StringEnumValue2, "Value2"}, + {StringEnumValue3, "Value3"}, +} + +type EntityWithStringEnum struct { + Name string `json:"name"` + Enum StringEnum `json:"enum"` +} + +func (e EntityWithStringEnum) Get() EntityWithStringEnum { + return e +} + +var GeneratedJsEntityWithStringEnumTest = BindingTest{ + name: "GeneratedJsEntityWithStringEnumTest", + structs: []interface{}{ + &EntityWithStringEnum{}, + }, + enums: []interface{}{ + AllStringEnumValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + }, + want: `export namespace binding_test { + + export enum MY_PREFIX_StringEnum_MY_SUFFIX { + Value1 = "value1", + Value2 = "value2", + Value3 = "value3", + } + export class MY_PREFIX_EntityWithStringEnum_MY_SUFFIX { + name: string; + enum: MY_PREFIX_StringEnum_MY_SUFFIX; + + static createFrom(source: any = {}) { + return new MY_PREFIX_EntityWithStringEnum_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + + } +`, +} + +type EnumWithTsName string + +const ( + EnumWithTsName1 EnumWithTsName = "value1" + EnumWithTsName2 EnumWithTsName = "value2" + EnumWithTsName3 EnumWithTsName = "value3" +) + +var AllEnumWithTsNameValues = []EnumWithTsName{EnumWithTsName1, EnumWithTsName2, EnumWithTsName3} + +func (v EnumWithTsName) TSName() string { + switch v { + case EnumWithTsName1: + return "TsName1" + case EnumWithTsName2: + return "TsName2" + case EnumWithTsName3: + return "TsName3" + default: + return "???" + } +} + +type EntityWithEnumTsName struct { + Name string `json:"name"` + Enum EnumWithTsName `json:"enum"` +} + +func (e EntityWithEnumTsName) Get() EntityWithEnumTsName { + return e +} + +var GeneratedJsEntityWithEnumTsName = BindingTest{ + name: "GeneratedJsEntityWithEnumTsName", + structs: []interface{}{ + &EntityWithEnumTsName{}, + }, + enums: []interface{}{ + AllEnumWithTsNameValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + }, + want: `export namespace binding_test { + + export enum MY_PREFIX_EnumWithTsName_MY_SUFFIX { + TsName1 = "value1", + TsName2 = "value2", + TsName3 = "value3", + } + export class MY_PREFIX_EntityWithEnumTsName_MY_SUFFIX { + name: string; + enum: MY_PREFIX_EnumWithTsName_MY_SUFFIX; + + static createFrom(source: any = {}) { + return new MY_PREFIX_EntityWithEnumTsName_MY_SUFFIX(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + + } +`, +} + +var GeneratedJsEntityWithNestedStructInterfacesTest = BindingTest{ + name: "GeneratedJsEntityWithNestedStructInterfacesTest", + structs: []interface{}{ + &ParentEntity{}, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "MY_PREFIX_", + TsSuffix: "_MY_SUFFIX", + TsOutputType: "interfaces", + }, + want: `export namespace binding_test { + + export interface MY_PREFIX_ChildEntity_MY_SUFFIX { + name: string; + childProp: number; + } + export interface MY_PREFIX_ParentEntity_MY_SUFFIX { + name: string; + ref: MY_PREFIX_ChildEntity_MY_SUFFIX; + parentProp: string; + } + + } +`, +} diff --git a/v2/internal/binding/binding_test/binding_type_alias_test.go b/v2/internal/binding/binding_test/binding_type_alias_test.go index 8e7c7ca6d..90b009c5f 100644 --- a/v2/internal/binding/binding_test/binding_type_alias_test.go +++ b/v2/internal/binding/binding_test/binding_type_alias_test.go @@ -15,11 +15,11 @@ const expectedTypeAliasBindings = `// Cynhyrchwyd y ffeil hon yn awtomatig. PEID import {binding_test} from '../models'; import {int_package} from '../models'; -export function Map():Promise<{[key: string]: string}>; +export function Map():Promise>; export function MapAlias():Promise; -export function MapWithImportedStructValue():Promise<{[key: string]: int_package.SomeStruct}>; +export function MapWithImportedStructValue():Promise>; export function Slice():Promise>; @@ -41,7 +41,7 @@ func TestAliases(t *testing.T) { // setup testLogger := &logger.Logger{} - b := binding.NewBindings(testLogger, []interface{}{&AliasTest{}}, []interface{}{}, false) + b := binding.NewBindings(testLogger, []interface{}{&AliasTest{}}, []interface{}{}, false, []interface{}{}) // then err := b.GenerateGoBindings(generationDir) diff --git a/v2/internal/binding/binding_test/binding_variablespecialcharacter_test.go b/v2/internal/binding/binding_test/binding_variablespecialcharacter_test.go new file mode 100644 index 000000000..7dbe72350 --- /dev/null +++ b/v2/internal/binding/binding_test/binding_variablespecialcharacter_test.go @@ -0,0 +1,32 @@ +package binding_test + +type SpecialCharacterField struct { + ID string `json:"@ID,omitempty"` +} + +func (s SpecialCharacterField) Get() SpecialCharacterField { + return s +} + +var SpecialCharacterFieldTest = BindingTest{ + name: "SpecialCharacterField", + structs: []interface{}{ + &SpecialCharacterField{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class SpecialCharacterField { + "@ID"?: string; + static createFrom(source: any = {}) { + return new SpecialCharacterField(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this["@ID"] = source["@ID"]; + } + } +} +`, +} diff --git a/v2/internal/binding/boundMethod.go b/v2/internal/binding/boundMethod.go index f6ffdb600..e697041b0 100644 --- a/v2/internal/binding/boundMethod.go +++ b/v2/internal/binding/boundMethod.go @@ -6,14 +6,24 @@ import ( "reflect" ) +type BoundedMethodPath struct { + Package string + Struct string + Name string +} + +func (p *BoundedMethodPath) FullName() string { + return fmt.Sprintf("%s.%s.%s", p.Package, p.Struct, p.Name) +} + // BoundMethod defines all the data related to a Go method that is // bound to the Wails application type BoundMethod struct { - Name string `json:"name"` - Inputs []*Parameter `json:"inputs,omitempty"` - Outputs []*Parameter `json:"outputs,omitempty"` - Comments string `json:"comments,omitempty"` - Method reflect.Value `json:"-"` + Path *BoundedMethodPath `json:"path"` + Inputs []*Parameter `json:"inputs,omitempty"` + Outputs []*Parameter `json:"outputs,omitempty"` + Comments string `json:"comments,omitempty"` + Method reflect.Value `json:"-"` } // InputCount returns the number of inputs this bound method has @@ -28,10 +38,9 @@ func (b *BoundMethod) OutputCount() int { // ParseArgs method converts the input json into the types expected by the method func (b *BoundMethod) ParseArgs(args []json.RawMessage) ([]interface{}, error) { - result := make([]interface{}, b.InputCount()) if len(args) != b.InputCount() { - return nil, fmt.Errorf("received %d arguments to method '%s', expected %d", len(args), b.Name, b.InputCount()) + return nil, fmt.Errorf("received %d arguments to method '%s', expected %d", len(args), b.Path.FullName(), b.InputCount()) } for index, arg := range args { typ := b.Inputs[index].reflectType @@ -55,7 +64,7 @@ func (b *BoundMethod) Call(args []interface{}) (interface{}, error) { expectedInputLength := len(b.Inputs) actualInputLength := len(args) if expectedInputLength != actualInputLength { - return nil, fmt.Errorf("%s takes %d inputs. Received %d", b.Name, expectedInputLength, actualInputLength) + return nil, fmt.Errorf("%s takes %d inputs. Received %d", b.Path.FullName(), expectedInputLength, actualInputLength) } /** Convert inputs to reflect values **/ diff --git a/v2/internal/binding/db.go b/v2/internal/binding/db.go index f22798680..f7b793839 100644 --- a/v2/internal/binding/db.go +++ b/v2/internal/binding/db.go @@ -2,7 +2,6 @@ package binding import ( "encoding/json" - "sort" "sync" "unsafe" ) @@ -17,24 +16,28 @@ type DB struct { methodMap map[string]*BoundMethod // This uses ids to reference bound methods at runtime - obfuscatedMethodMap map[int]*BoundMethod + obfuscatedMethodArray []*ObfuscatedMethod // Lock to ensure sync access to the data lock sync.RWMutex } +type ObfuscatedMethod struct { + method *BoundMethod + methodName string +} + func newDB() *DB { return &DB{ - store: make(map[string]map[string]map[string]*BoundMethod), - methodMap: make(map[string]*BoundMethod), - obfuscatedMethodMap: make(map[int]*BoundMethod), + store: make(map[string]map[string]map[string]*BoundMethod), + methodMap: make(map[string]*BoundMethod), + obfuscatedMethodArray: []*ObfuscatedMethod{}, } } // GetMethodFromStore returns the method for the given package/struct/method names // nil is returned if any one of those does not exist func (d *DB) GetMethodFromStore(packageName string, structName string, methodName string) *BoundMethod { - // Lock the db whilst processing and unlock on return d.lock.RLock() defer d.lock.RUnlock() @@ -53,7 +56,6 @@ func (d *DB) GetMethodFromStore(packageName string, structName string, methodNam // GetMethod returns the method for the given qualified method name // qualifiedMethodName is "packagename.structname.methodname" func (d *DB) GetMethod(qualifiedMethodName string) *BoundMethod { - // Lock the db whilst processing and unlock on return d.lock.RLock() defer d.lock.RUnlock() @@ -67,12 +69,15 @@ func (d *DB) GetObfuscatedMethod(id int) *BoundMethod { d.lock.RLock() defer d.lock.RUnlock() - return d.obfuscatedMethodMap[id] + if len(d.obfuscatedMethodArray) <= id { + return nil + } + + return d.obfuscatedMethodArray[id].method } // AddMethod adds the given method definition to the db using the given qualified path: packageName.structName.methodName func (d *DB) AddMethod(packageName string, structName string, methodName string, methodDefinition *BoundMethod) { - // Lock the db whilst processing and unlock on return d.lock.Lock() defer d.lock.Unlock() @@ -99,12 +104,11 @@ func (d *DB) AddMethod(packageName string, structName string, methodName string, // Store in the methodMap key := packageName + "." + structName + "." + methodName d.methodMap[key] = methodDefinition - + d.obfuscatedMethodArray = append(d.obfuscatedMethodArray, &ObfuscatedMethod{method: methodDefinition, methodName: key}) } // ToJSON converts the method map to JSON func (d *DB) ToJSON() (string, error) { - // Lock the db whilst processing and unlock on return d.lock.RLock() defer d.lock.RUnlock() @@ -120,20 +124,11 @@ func (d *DB) ToJSON() (string, error) { // UpdateObfuscatedCallMap sets up the secure call mappings func (d *DB) UpdateObfuscatedCallMap() map[string]int { + mappings := make(map[string]int) - var mappings = make(map[string]int) - - // Iterate map keys and sort them - keys := make([]string, 0, len(d.methodMap)) - for k := range d.methodMap { - keys = append(keys, k) + for id, k := range d.obfuscatedMethodArray { + mappings[k.methodName] = id } - sort.Strings(keys) - // Iterate sorted keys and add to obfuscated method map - for id, k := range keys { - mappings[k] = id - d.obfuscatedMethodMap[id] = d.methodMap[k] - } return mappings } diff --git a/v2/internal/binding/generate.go b/v2/internal/binding/generate.go index 8416aade1..77edc983d 100644 --- a/v2/internal/binding/generate.go +++ b/v2/internal/binding/generate.go @@ -15,12 +15,14 @@ import ( "github.com/leaanthony/slicer" ) -var mapRegex *regexp.Regexp -var keyPackageIndex int -var keyTypeIndex int -var valueArrayIndex int -var valuePackageIndex int -var valueTypeIndex int +var ( + mapRegex *regexp.Regexp + keyPackageIndex int + keyTypeIndex int + valueArrayIndex int + valuePackageIndex int + valueTypeIndex int +) func init() { mapRegex = regexp.MustCompile(`(?:map\[(?:(?P\w+)\.)?(?P\w+)])?(?P\[])?(?:\*?(?P\w+)\.)?(?P.+)`) @@ -81,9 +83,7 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error { } else { jsoutput.WriteString(fmt.Sprintf(" return window['go']['%s']['%s']['%s'](%s);", packageName, structName, methodName, argsString)) } - jsoutput.WriteString("\n") - jsoutput.WriteString(fmt.Sprintf("}")) - jsoutput.WriteString("\n") + jsoutput.WriteString("\n}\n") // Generate TS tsBody.WriteString(fmt.Sprintf("\nexport function %s(", methodName)) @@ -127,12 +127,12 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error { tsContent.WriteString(tsBody.String()) jsfilename := filepath.Join(packageDir, structName+".js") - err = os.WriteFile(jsfilename, jsoutput.Bytes(), 0755) + err = os.WriteFile(jsfilename, jsoutput.Bytes(), 0o755) if err != nil { return err } tsfilename := filepath.Join(packageDir, structName+".d.ts") - err = os.WriteFile(tsfilename, tsContent.Bytes(), 0755) + err = os.WriteFile(tsfilename, tsContent.Bytes(), 0o755) if err != nil { return err } @@ -171,7 +171,18 @@ func fullyQualifiedName(packageName string, typeName string) string { } } +var ( + jsVariableUnsafeChars = regexp.MustCompile(`[^A-Za-z0-9_]`) +) + func arrayifyValue(valueArray string, valueType string) string { + valueType = strings.ReplaceAll(valueType, "*", "") + gidx := strings.IndexRune(valueType, '[') + if gidx > 0 { // its a generic type + rem := strings.SplitN(valueType, "[", 2) + valueType = rem[0] + "_" + jsVariableUnsafeChars.ReplaceAllLiteralString(rem[1], "_") + } + if len(valueArray) == 0 { return valueType } @@ -186,7 +197,7 @@ func goTypeToJSDocType(input string, importNamespaces *slicer.StringSlicer) stri valueArray := matches[valueArrayIndex] valuePackage := matches[valuePackageIndex] valueType := matches[valueTypeIndex] - //fmt.Printf("input=%s, keyPackage=%s, keyType=%s, valueArray=%s, valuePackage=%s, valueType=%s\n", + // fmt.Printf("input=%s, keyPackage=%s, keyType=%s, valueArray=%s, valuePackage=%s, valueType=%s\n", // input, // keyPackage, // keyType, @@ -217,7 +228,7 @@ func goTypeToJSDocType(input string, importNamespaces *slicer.StringSlicer) stri } if len(key) > 0 { - return fmt.Sprintf("{[key: %s]: %s}", key, arrayifyValue(valueArray, value)) + return fmt.Sprintf("Record<%s, %s>", key, arrayifyValue(valueArray, value)) } return arrayifyValue(valueArray, value) diff --git a/v2/internal/binding/generate_test.go b/v2/internal/binding/generate_test.go index 565fba31c..26d7c70df 100644 --- a/v2/internal/binding/generate_test.go +++ b/v2/internal/binding/generate_test.go @@ -25,7 +25,7 @@ type B struct { func TestNestedStruct(t *testing.T) { bind := &BindForTest{} - testBindings := NewBindings(logger.New(nil), []interface{}{bind}, []interface{}{}, false) + testBindings := NewBindings(logger.New(nil), []interface{}{bind}, []interface{}{}, false, []interface{}{}) namesStrSlicer := testBindings.getAllStructNames() names := []string{} @@ -116,18 +116,28 @@ func Test_goTypeToJSDocType(t *testing.T) { { name: "map", input: "map[string]float64", - want: "{[key: string]: number}", + want: "Record", }, { name: "map", input: "map[string]map[string]float64", - want: "{[key: string]: {[key: string]: number}}", + want: "Record>", }, { name: "types", input: "main.SomeType", want: "main.SomeType", }, + { + name: "primitive_generic", + input: "main.ListData[string]", + want: "main.ListData_string_", + }, + { + name: "stdlib_generic", + input: "main.ListData[*net/http.Request]", + want: "main.ListData_net_http_Request_", + }, } var importNamespaces slicer.StringSlicer for _, tt := range tests { diff --git a/v2/internal/binding/reflect.go b/v2/internal/binding/reflect.go old mode 100755 new mode 100644 index 66a9cf7bd..c254d0f0a --- a/v2/internal/binding/reflect.go +++ b/v2/internal/binding/reflect.go @@ -19,13 +19,32 @@ func isFunction(value interface{}) bool { return reflect.ValueOf(value).Kind() == reflect.Func } -// isStructPtr returns true if the value given is a struct +// isStruct returns true if the value given is a struct func isStruct(value interface{}) bool { return reflect.ValueOf(value).Kind() == reflect.Struct } -func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { +func normalizeStructName(name string) string { + return strings.ReplaceAll( + strings.ReplaceAll( + strings.ReplaceAll( + strings.ReplaceAll( + name, + ",", + "-", + ), + "*", + "", + ), + "]", + "__", + ), + "[", + "__", + ) +} +func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { // Create result placeholder var result []*BoundMethod @@ -48,14 +67,14 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { // Process Struct structType := reflect.TypeOf(value) structValue := reflect.ValueOf(value) - structTypeString := structType.String() - baseName := structTypeString[1:] + structName := structType.Elem().Name() + structNameNormalized := normalizeStructName(structName) + pkgPath := strings.TrimSuffix(structType.Elem().String(), fmt.Sprintf(".%s", structName)) // Process Methods for i := 0; i < structType.NumMethod(); i++ { methodDef := structType.Method(i) methodName := methodDef.Name - fullMethodName := baseName + "." + methodName method := structValue.MethodByName(methodName) methodReflectName := runtime.FuncForPC(methodDef.Func.Pointer()).Name() @@ -65,7 +84,11 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { // Create new method boundMethod := &BoundMethod{ - Name: fullMethodName, + Path: &BoundedMethodPath{ + Package: pkgPath, + Struct: structNameNormalized, + Name: methodName, + }, Inputs: nil, Outputs: nil, Comments: "", @@ -167,9 +190,8 @@ func getPackageName(in string) string { } func getSplitReturn(in string) (string, string) { - result := strings.Split(in, ".") + result := strings.SplitN(in, ".", 2) return result[0], result[1] - } func hasElements(typ reflect.Type) bool { diff --git a/v2/internal/frontend/calls.go b/v2/internal/frontend/calls.go index 3983c24bf..5401106bc 100644 --- a/v2/internal/frontend/calls.go +++ b/v2/internal/frontend/calls.go @@ -1,5 +1,5 @@ package frontend type Calls interface { - Callback(string) + Callback(message string) } diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.h b/v2/internal/frontend/desktop/darwin/AppDelegate.h index e2dd841c9..a8d10f647 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.h +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.h @@ -11,13 +11,23 @@ #import #import "WailsContext.h" -@interface AppDelegate : NSResponder +@interface AppDelegate : NSResponder @property bool alwaysOnTop; @property bool startHidden; +@property (retain) NSString* singleInstanceUniqueId; +@property bool singleInstanceLockEnabled; @property bool startFullscreen; @property (retain) WailsWindow* mainWindow; @end +extern void HandleOpenFile(char *); + +extern void HandleSecondInstanceData(char * message); + +void SendDataToFirstInstance(char * singleInstanceUniqueId, char * text); + +char* GetMacOsNativeTempDir(); + #endif /* AppDelegate_h */ diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.m b/v2/internal/frontend/desktop/darwin/AppDelegate.m index b66979e76..a73ec3ec3 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.m +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.m @@ -9,11 +9,37 @@ #import #import "AppDelegate.h" +#import "CustomProtocol.h" +#import "message.h" @implementation AppDelegate -- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { - return NO; +-(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename +{ + const char* utf8FileName = filename.UTF8String; + HandleOpenFile((char*)utf8FileName); + return YES; } + +- (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray> * _Nullable))restorationHandler { + if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + NSURL *url = userActivity.webpageURL; + if (url) { + HandleOpenURL((char*)[[url absoluteString] UTF8String]); + return YES; + } + } + return NO; +} + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { + return NO; +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { + processMessage("Q"); + return NSTerminateCancel; +} + - (void)applicationWillFinishLaunching:(NSNotification *)aNotification { [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; if (self.alwaysOnTop) { @@ -32,6 +58,37 @@ [self.mainWindow setCollectionBehavior:behaviour]; [self.mainWindow toggleFullScreen:nil]; } + + if ( self.singleInstanceLockEnabled ) { + [[NSDistributedNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleSecondInstanceNotification:) name:self.singleInstanceUniqueId object:nil]; + } +} + +void SendDataToFirstInstance(char * singleInstanceUniqueId, char * message) { + // we pass message in object because otherwise sandboxing will prevent us from sending it https://developer.apple.com/forums/thread/129437 + NSString * myString = [NSString stringWithUTF8String:message]; + [[NSDistributedNotificationCenter defaultCenter] + postNotificationName:[NSString stringWithUTF8String:singleInstanceUniqueId] + object:(__bridge const void *)(myString) + userInfo:nil + deliverImmediately:YES]; +} + +char* GetMacOsNativeTempDir() { + NSString *tempDir = NSTemporaryDirectory(); + char *copy = strdup([tempDir UTF8String]); + + return copy; +} + +- (void)handleSecondInstanceNotification:(NSNotification *)note; +{ + if (note.object != nil) { + NSString * message = (__bridge NSString *)note.object; + const char* utf8Message = message.UTF8String; + HandleSecondInstanceData((char*)utf8Message); + } } - (void)dealloc { diff --git a/v2/internal/frontend/desktop/darwin/Application.h b/v2/internal/frontend/desktop/darwin/Application.h index 458016cc8..c3cd8075a 100644 --- a/v2/internal/frontend/desktop/darwin/Application.h +++ b/v2/internal/frontend/desktop/darwin/Application.h @@ -17,7 +17,7 @@ #define WindowStartsMinimised 2 #define WindowStartsFullscreen 3 -WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences); +WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int zoomable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int contentProtection, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceLockEnabled, const char* singleInstanceUniqueId, bool enableDragAndDrop, bool disableWebViewDragAndDrop); void Run(void*, const char* url); void SetTitle(void* ctx, const char *title); @@ -69,6 +69,21 @@ void UpdateMenuItem(void* nsmenuitem, int checked); void RunMainLoop(void); void ReleaseContext(void *inctx); +/* Notifications */ +bool IsNotificationAvailable(void *inctx); +bool CheckBundleIdentifier(void *inctx); +bool EnsureDelegateInitialized(void *inctx); +void RequestNotificationAuthorization(void *inctx, int channelID); +void CheckNotificationAuthorization(void *inctx, int channelID); +void SendNotification(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json); +void SendNotificationWithActions(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json); +void RegisterNotificationCategory(void *inctx, int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle); +void RemoveNotificationCategory(void *inctx, int channelID, const char *categoryId); +void RemoveAllPendingNotifications(void *inctx); +void RemovePendingNotification(void *inctx, const char *identifier); +void RemoveAllDeliveredNotifications(void *inctx); +void RemoveDeliveredNotification(void *inctx, const char *identifier); + NSString* safeInit(const char* input); #endif /* Application_h */ diff --git a/v2/internal/frontend/desktop/darwin/Application.m b/v2/internal/frontend/desktop/darwin/Application.m index e0c05212d..38b2f35ef 100644 --- a/v2/internal/frontend/desktop/darwin/Application.m +++ b/v2/internal/frontend/desktop/darwin/Application.m @@ -14,23 +14,28 @@ #import "WailsMenu.h" #import "WailsMenuItem.h" -WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences) { - +WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int zoomable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int contentProtection, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceLockEnabled, const char* singleInstanceUniqueId, bool enableDragAndDrop, bool disableWebViewDragAndDrop) { + [NSApplication sharedApplication]; WailsContext *result = [WailsContext new]; result.devtoolsEnabled = devtoolsEnabled; result.defaultContextMenuEnabled = defaultContextMenuEnabled; - + if ( windowStartState == WindowStartsFullscreen ) { fullscreen = 1; } - [result CreateWindow:width :height :frameless :resizable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight :fraudulentWebsiteWarningEnabled :preferences]; + [result CreateWindow:width :height :frameless :resizable :zoomable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight :fraudulentWebsiteWarningEnabled :preferences :enableDragAndDrop :disableWebViewDragAndDrop]; [result SetTitle:safeInit(title)]; [result Center]; - + + if (contentProtection == 1 && + [result.mainWindow respondsToSelector:@selector(setSharingType:)]) { + [result.mainWindow setSharingType:NSWindowSharingNone]; + } + switch( windowStartState ) { case WindowStartsMaximised: [result.mainWindow zoom:nil]; @@ -43,14 +48,19 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in if ( startsHidden == 1 ) { result.startHidden = true; } - + if ( fullscreen == 1 ) { result.startFullscreen = true; } - + + if ( singleInstanceLockEnabled == 1 ) { + result.singleInstanceLockEnabled = true; + result.singleInstanceUniqueId = safeInit(singleInstanceUniqueId); + } + result.alwaysOnTop = alwaysOnTop; result.hideOnClose = hideWindowOnClose; - + return result; } @@ -181,7 +191,7 @@ const char* GetPosition(void *inctx) { NSString *result = [NSString stringWithFormat:@"%d,%d",x,y]; return [result UTF8String]; } - + const bool IsFullScreen(void *inctx) { WailsContext *ctx = (__bridge WailsContext*) inctx; return [ctx IsFullScreen]; @@ -249,7 +259,7 @@ NSString* safeInit(const char* input) { void MessageDialog(void *inctx, const char* dialogType, const char* title, const char* message, const char* button1, const char* button2, const char* button3, const char* button4, const char* defaultButton, const char* cancelButton, void* iconData, int iconDataLength) { WailsContext *ctx = (__bridge WailsContext*) inctx; - + NSString *_dialogType = safeInit(dialogType); NSString *_title = safeInit(title); NSString *_message = safeInit(message); @@ -259,33 +269,33 @@ void MessageDialog(void *inctx, const char* dialogType, const char* title, const NSString *_button4 = safeInit(button4); NSString *_defaultButton = safeInit(defaultButton); NSString *_cancelButton = safeInit(cancelButton); - + ON_MAIN_THREAD( [ctx MessageDialog:_dialogType :_title :_message :_button1 :_button2 :_button3 :_button4 :_defaultButton :_cancelButton :iconData :iconDataLength]; ) } void OpenFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int allowDirectories, int allowFiles, int canCreateDirectories, int treatPackagesAsDirectories, int resolveAliases, int showHiddenFiles, int allowMultipleSelection, const char* filters) { - + WailsContext *ctx = (__bridge WailsContext*) inctx; NSString *_title = safeInit(title); NSString *_defaultFilename = safeInit(defaultFilename); NSString *_defaultDirectory = safeInit(defaultDirectory); NSString *_filters = safeInit(filters); - + ON_MAIN_THREAD( [ctx OpenFileDialog:_title :_defaultFilename :_defaultDirectory :allowDirectories :allowFiles :canCreateDirectories :treatPackagesAsDirectories :resolveAliases :showHiddenFiles :allowMultipleSelection :_filters]; ) } void SaveFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int canCreateDirectories, int treatPackagesAsDirectories, int showHiddenFiles, const char* filters) { - + WailsContext *ctx = (__bridge WailsContext*) inctx; NSString *_title = safeInit(title); NSString *_defaultFilename = safeInit(defaultFilename); NSString *_defaultDirectory = safeInit(defaultDirectory); NSString *_filters = safeInit(filters); - + ON_MAIN_THREAD( [ctx SaveFileDialog:_title :_defaultFilename :_defaultDirectory :canCreateDirectories :treatPackagesAsDirectories :showHiddenFiles :_filters]; ) @@ -357,6 +367,74 @@ void AppendSeparator(void* inMenu) { } +bool IsNotificationAvailable(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + return [ctx IsNotificationAvailable]; +} + +bool CheckBundleIdentifier(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + return [ctx CheckBundleIdentifier]; +} + +bool EnsureDelegateInitialized(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + return [ctx EnsureDelegateInitialized]; +} + +void RequestNotificationAuthorization(void *inctx, int channelID) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + [ctx RequestNotificationAuthorization:channelID]; +} + +void CheckNotificationAuthorization(void *inctx, int channelID) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + [ctx CheckNotificationAuthorization:channelID]; +} + +void SendNotification(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + [ctx SendNotification:channelID :identifier :title :subtitle :body :data_json]; +} + +void SendNotificationWithActions(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + + [ctx SendNotificationWithActions:channelID :identifier :title :subtitle :body :categoryId :actions_json]; +} + +void RegisterNotificationCategory(void *inctx, int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + + [ctx RegisterNotificationCategory:channelID :categoryId :actions_json :hasReplyField :replyPlaceholder :replyButtonTitle]; +} + +void RemoveNotificationCategory(void *inctx, int channelID, const char *categoryId) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + + [ctx RemoveNotificationCategory:channelID :categoryId]; +} + +void RemoveAllPendingNotifications(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + [ctx RemoveAllPendingNotifications]; +} + +void RemovePendingNotification(void *inctx, const char *identifier) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + [ctx RemovePendingNotification:identifier]; +} + +void RemoveAllDeliveredNotifications(void *inctx) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + [ctx RemoveAllDeliveredNotifications]; +} + +void RemoveDeliveredNotification(void *inctx, const char *identifier) { + WailsContext *ctx = (__bridge WailsContext*)inctx; + [ctx RemoveDeliveredNotification:identifier]; +} + void Run(void *inctx, const char* url) { WailsContext *ctx = (__bridge WailsContext*) inctx; @@ -367,6 +445,8 @@ void Run(void *inctx, const char* url) { delegate.mainWindow = ctx.mainWindow; delegate.alwaysOnTop = ctx.alwaysOnTop; delegate.startHidden = ctx.startHidden; + delegate.singleInstanceLockEnabled = ctx.singleInstanceLockEnabled; + delegate.singleInstanceUniqueId = ctx.singleInstanceUniqueId; delegate.startFullscreen = ctx.startFullscreen; NSString *_url = safeInit(url); @@ -389,13 +469,13 @@ void ReleaseContext(void *inctx) { // Credit: https://stackoverflow.com/q/33319295 void WindowPrint(void *inctx) { - // Check if macOS 11.0 or newer +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 if (@available(macOS 11.0, *)) { ON_MAIN_THREAD( WailsContext *ctx = (__bridge WailsContext*) inctx; WKWebView* webView = ctx.webview; - // I think this should be exposed as a config + // I think this should be exposed as a config // It directly affects the printed output/PDF NSPrintInfo *pInfo = [NSPrintInfo sharedPrintInfo]; pInfo.horizontalPagination = NSPrintingPaginationModeAutomatic; @@ -417,4 +497,5 @@ void WindowPrint(void *inctx) { [po runOperationModalForWindow:ctx.mainWindow delegate:ctx.mainWindow.delegate didRunSelector:nil contextInfo:nil]; ) } -} \ No newline at end of file +#endif +} diff --git a/v2/internal/frontend/desktop/darwin/CustomProtocol.h b/v2/internal/frontend/desktop/darwin/CustomProtocol.h new file mode 100644 index 000000000..0698a4d45 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/CustomProtocol.h @@ -0,0 +1,14 @@ +#ifndef CustomProtocol_h +#define CustomProtocol_h + +#import + +extern void HandleOpenURL(char*); + +@interface CustomProtocolSchemeHandler : NSObject ++ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent; +@end + +void StartCustomProtocolHandler(void); + +#endif /* CustomProtocol_h */ diff --git a/v2/internal/frontend/desktop/darwin/CustomProtocol.m b/v2/internal/frontend/desktop/darwin/CustomProtocol.m new file mode 100644 index 000000000..ebc61aa00 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/CustomProtocol.m @@ -0,0 +1,20 @@ +#include "CustomProtocol.h" + +@implementation CustomProtocolSchemeHandler ++ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + [event paramDescriptorForKeyword:keyDirectObject]; + + NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + + HandleOpenURL((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]); +} +@end + +void StartCustomProtocolHandler(void) { + NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; + + [appleEventManager setEventHandler:[CustomProtocolSchemeHandler class] + andSelector:@selector(handleGetURLEvent:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID: kAEGetURL]; +} diff --git a/v2/internal/frontend/desktop/darwin/WailsContext.h b/v2/internal/frontend/desktop/darwin/WailsContext.h index fe66f3549..aafc3a1d4 100644 --- a/v2/internal/frontend/desktop/darwin/WailsContext.h +++ b/v2/internal/frontend/desktop/darwin/WailsContext.h @@ -10,6 +10,7 @@ #import #import +#import "WailsWebView.h" #if __has_include() #define USE_NEW_FILTERS @@ -32,7 +33,7 @@ @interface WailsContext : NSObject @property (retain) WailsWindow* mainWindow; -@property (retain) WKWebView* webview; +@property (retain) WailsWebView* webview; @property (nonatomic, assign) id appdelegate; @property bool hideOnClose; @@ -40,6 +41,9 @@ @property bool startHidden; @property bool startFullscreen; +@property bool singleInstanceLockEnabled; +@property (retain) NSString* singleInstanceUniqueId; + @property (retain) NSEvent* mouseEvent; @property bool alwaysOnTop; @@ -58,9 +62,10 @@ struct Preferences { bool *tabFocusesLinks; bool *textInteractionEnabled; + bool *fullscreenEnabled; }; -- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString *)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences; +- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)zoomable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString *)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences :(bool)enableDragAndDrop :(bool)disableWebViewDragAndDrop; - (void) SetSize:(int)width :(int)height; - (void) SetPosition:(int)x :(int) y; - (void) SetMinSize:(int)minWidth :(int)minHeight; @@ -87,10 +92,24 @@ struct Preferences { - (void) ShowApplication; - (void) Quit; --(void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength; +- (void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength; - (void) OpenFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)allowDirectories :(bool)allowFiles :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)resolveAliases :(bool)showHiddenFiles :(bool)allowMultipleSelection :(NSString*)filters; - (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters; +- (bool) IsNotificationAvailable; +- (bool) CheckBundleIdentifier; +- (bool) EnsureDelegateInitialized; +- (void) RequestNotificationAuthorization:(int)channelID; +- (void) CheckNotificationAuthorization:(int)channelID; +- (void) SendNotification:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)dataJSON; +- (void) SendNotificationWithActions:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)categoryId :(const char *)actionsJSON; +- (void) RegisterNotificationCategory:(int)channelID :(const char *)categoryId :(const char *)actionsJSON :(bool)hasReplyField :(const char *)replyPlaceholder :(const char *)replyButtonTitle; +- (void) RemoveNotificationCategory:(int)channelID :(const char *)categoryId; +- (void) RemoveAllPendingNotifications; +- (void) RemovePendingNotification:(const char *)identifier; +- (void) RemoveAllDeliveredNotifications; +- (void) RemoveDeliveredNotification:(const char *)identifier; + - (void) loadRequest:(NSString*)url; - (void) ExecJS:(NSString*)script; - (NSScreen*) getCurrentScreen; diff --git a/v2/internal/frontend/desktop/darwin/WailsContext.m b/v2/internal/frontend/desktop/darwin/WailsContext.m index 1c84a5761..51993eda2 100644 --- a/v2/internal/frontend/desktop/darwin/WailsContext.m +++ b/v2/internal/frontend/desktop/darwin/WailsContext.m @@ -1,4 +1,3 @@ -//go:build darwin // // WailsContext.m // test @@ -6,11 +5,13 @@ // Created by Lea Anthony on 10/10/21. // +#include "Application.h" #import #import #import "WailsContext.h" #import "WailsAlert.h" #import "WailsMenu.h" +#import "WailsWebView.h" #import "WindowDelegate.h" #import "message.h" #import "Role.h" @@ -36,12 +37,20 @@ typedef void (^schemeTaskCaller)(id); @end +// Notifications +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 +#import +#endif + +extern void captureResult(int channelID, bool success, const char* error); +extern void didReceiveNotificationResponse(const char *jsonPayload, const char* error); + @implementation WailsContext - (void) SetSize:(int)width :(int)height { - + if (self.shuttingDown) return; - + NSRect frame = [self.mainWindow frame]; frame.origin.y += frame.size.height - height; frame.size.width = width; @@ -50,22 +59,22 @@ typedef void (^schemeTaskCaller)(id); } - (void) SetPosition:(int)x :(int)y { - + if (self.shuttingDown) return; - + NSScreen* screen = [self getCurrentScreen]; NSRect windowFrame = [self.mainWindow frame]; - NSRect screenFrame = [screen frame]; + NSRect screenFrame = [screen visibleFrame]; windowFrame.origin.x = screenFrame.origin.x + (float)x; windowFrame.origin.y = (screenFrame.origin.y + screenFrame.size.height) - windowFrame.size.height - (float)y; - + [self.mainWindow setFrame:windowFrame display:TRUE animate:FALSE]; } - (void) SetMinSize:(int)minWidth :(int)minHeight { - + if (self.shuttingDown) return; - + NSSize size = { minWidth, minHeight }; self.mainWindow.userMinSize = size; [self.mainWindow setMinSize:size]; @@ -74,14 +83,14 @@ typedef void (^schemeTaskCaller)(id); - (void) SetMaxSize:(int)maxWidth :(int)maxHeight { - + if (self.shuttingDown) return; - + NSSize size = { FLT_MAX, FLT_MAX }; - + size.width = maxWidth > 0 ? maxWidth : FLT_MAX; size.height = maxHeight > 0 ? maxHeight : FLT_MAX; - + self.mainWindow.userMaxSize = size; [self.mainWindow setMaxSize:size]; [self adjustWindowSize]; @@ -89,18 +98,18 @@ typedef void (^schemeTaskCaller)(id); - (void) adjustWindowSize { - + if (self.shuttingDown) return; - + NSRect currentFrame = [self.mainWindow frame]; - + if ( currentFrame.size.width > self.mainWindow.userMaxSize.width ) currentFrame.size.width = self.mainWindow.userMaxSize.width; if ( currentFrame.size.width < self.mainWindow.userMinSize.width ) currentFrame.size.width = self.mainWindow.userMinSize.width; if ( currentFrame.size.height > self.mainWindow.userMaxSize.height ) currentFrame.size.height = self.mainWindow.userMaxSize.height; if ( currentFrame.size.height < self.mainWindow.userMinSize.height ) currentFrame.size.height = self.mainWindow.userMinSize.height; [self.mainWindow setFrame:currentFrame display:YES animate:FALSE]; - + } - (void) dealloc { @@ -136,16 +145,16 @@ typedef void (^schemeTaskCaller)(id); return NO; } -- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString*)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences { +- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)zoomable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString*)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences :(bool)enableDragAndDrop :(bool)disableWebViewDragAndDrop { NSWindowStyleMask styleMask = 0; - + if( !frameless ) { if (!hideTitleBar) { styleMask |= NSWindowStyleMaskTitled; } styleMask |= NSWindowStyleMaskClosable; } - + styleMask |= NSWindowStyleMaskMiniaturizable; if( fullSizeContent || frameless || titlebarAppearsTransparent ) { @@ -155,23 +164,22 @@ typedef void (^schemeTaskCaller)(id); if (resizable) { styleMask |= NSWindowStyleMaskResizable; } - + self.mainWindow = [[WailsWindow alloc] initWithContentRect:NSMakeRect(0, 0, width, height) styleMask:styleMask backing:NSBackingStoreBuffered defer:NO]; - if (!frameless && useToolbar) { id toolbar = [[NSToolbar alloc] initWithIdentifier:@"wails.toolbar"]; [toolbar autorelease]; [toolbar setShowsBaselineSeparator:!hideToolbarSeparator]; [self.mainWindow setToolbar:toolbar]; - + } - + [self.mainWindow setTitleVisibility:hideTitle]; [self.mainWindow setTitlebarAppearsTransparent:titlebarAppearsTransparent]; - + // [self.mainWindow canBecomeKeyWindow]; - + id contentView = [self.mainWindow contentView]; if (windowIsTranslucent) { NSVisualEffectView *effectView = [NSVisualEffectView alloc]; @@ -182,12 +190,17 @@ typedef void (^schemeTaskCaller)(id); [effectView setState:NSVisualEffectStateActive]; [contentView addSubview:effectView positioned:NSWindowBelow relativeTo:nil]; } - + if (appearance != nil) { NSAppearance *nsAppearance = [NSAppearance appearanceNamed:appearance]; [self.mainWindow setAppearance:nsAppearance]; } - + + if (!zoomable && resizable) { + NSButton *button = [self.mainWindow standardWindowButton:NSWindowZoomButton]; + [button setEnabled: NO]; + } + NSSize minSize = { minWidth, minHeight }; NSSize maxSize = { maxWidth, maxHeight }; @@ -199,19 +212,25 @@ typedef void (^schemeTaskCaller)(id); } self.mainWindow.userMaxSize = maxSize; self.mainWindow.userMinSize = minSize; - + if( !fullscreen ) { [self.mainWindow applyWindowConstraints]; } - + WindowDelegate *windowDelegate = [WindowDelegate new]; windowDelegate.hideOnClose = hideWindowOnClose; windowDelegate.ctx = self; [self.mainWindow setDelegate:windowDelegate]; - + // Webview stuff here! WKWebViewConfiguration *config = [WKWebViewConfiguration new]; - config.suppressesIncrementalRendering = true; + // Disable suppressesIncrementalRendering on macOS 26+ to prevent WebView crashes + // during rapid UI updates. See: https://github.com/wailsapp/wails/issues/4592 + if (@available(macOS 26.0, *)) { + config.suppressesIncrementalRendering = false; + } else { + config.suppressesIncrementalRendering = true; + } config.applicationNameForUserAgent = @"wails.io"; [config setURLSchemeHandler:self forURLScheme:@"wails"]; @@ -219,17 +238,27 @@ typedef void (^schemeTaskCaller)(id); config.preferences.tabFocusesLinks = *preferences.tabFocusesLinks; } +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110300 if (@available(macOS 11.3, *)) { if (preferences.textInteractionEnabled != NULL) { config.preferences.textInteractionEnabled = *preferences.textInteractionEnabled; } } - -// [config.preferences setValue:[NSNumber numberWithBool:true] forKey:@"developerExtrasEnabled"]; +#endif +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 120300 + if (@available(macOS 12.3, *)) { + if (preferences.fullscreenEnabled != NULL) { + config.preferences.elementFullscreenEnabled = *preferences.fullscreenEnabled; + } + } +#endif + +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101500 if (@available(macOS 10.15, *)) { config.preferences.fraudulentWebsiteWarningEnabled = fraudulentWebsiteWarningEnabled; } +#endif WKUserContentController* userContentController = [WKUserContentController new]; [userContentController addScriptMessageHandler:self name:@"external"]; @@ -248,25 +277,28 @@ typedef void (^schemeTaskCaller)(id); forMainFrameOnly:false]; [userContentController addUserScript:initScript]; } - - self.webview = [WKWebView alloc]; + + self.webview = [WailsWebView alloc]; + self.webview.enableDragAndDrop = enableDragAndDrop; + self.webview.disableWebViewDragAndDrop = disableWebViewDragAndDrop; + CGRect init = { 0,0,0,0 }; [self.webview initWithFrame:init configuration:config]; [contentView addSubview:self.webview]; [self.webview setAutoresizingMask: NSViewWidthSizable|NSViewHeightSizable]; CGRect contentViewBounds = [contentView bounds]; [self.webview setFrame:contentViewBounds]; - + if (webviewIsTransparent) { [self.webview setValue:[NSNumber numberWithBool:!webviewIsTransparent] forKey:@"drawsBackground"]; } - + [self.webview setNavigationDelegate:self]; self.webview.UIDelegate = self; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setBool:FALSE forKey:@"NSAutomaticQuoteSubstitutionEnabled"]; - + // Mouse monitors [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) { id window = [event window]; @@ -275,7 +307,7 @@ typedef void (^schemeTaskCaller)(id); } return event; }]; - + [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseUp handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) { id window = [event window]; if (window == self.mainWindow) { @@ -284,9 +316,9 @@ typedef void (^schemeTaskCaller)(id); } return event; }]; - + self.applicationMenu = [NSMenu new]; - + } - (NSMenuItem*) newMenuItem :(NSString*)title :(SEL)selector :(NSString*)key :(NSEventModifierFlags)flags { @@ -322,9 +354,9 @@ typedef void (^schemeTaskCaller)(id); float green = g/255.0; float blue = b/255.0; float alpha = a/255.0; - + id colour = [NSColor colorWithCalibratedRed:red green:green blue:blue alpha:alpha ]; - + [self.mainWindow setBackgroundColor:colour]; } @@ -420,16 +452,17 @@ typedef void (^schemeTaskCaller)(id); [self.webview evaluateJavaScript:script completionHandler:nil]; } -- (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters +- (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray * URLs))completionHandler { - + NSOpenPanel *openPanel = [NSOpenPanel openPanel]; openPanel.allowsMultipleSelection = parameters.allowsMultipleSelection; +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 if (@available(macOS 10.14, *)) { openPanel.canChooseDirectories = parameters.allowsDirectories; } - - [openPanel +#endif + [openPanel beginSheetModalForWindow:webView.window completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) @@ -459,8 +492,17 @@ typedef void (^schemeTaskCaller)(id); } - (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + // Get the origin from the message's frame + NSString *origin = nil; + if (message.frameInfo && message.frameInfo.request && message.frameInfo.request.URL) { + NSURL *url = message.frameInfo.request.URL; + if (url.scheme && url.host) { + origin = [url absoluteString]; + } + } + NSString *m = message.body; - + // Check for drag if ( [m isEqualToString:@"drag"] ) { if( [self IsFullScreen] ) { @@ -471,18 +513,18 @@ typedef void (^schemeTaskCaller)(id); } return; } - - const char *_m = [m UTF8String]; - - processMessage(_m); -} + const char *_m = [m UTF8String]; + const char *_origin = [origin UTF8String]; + + processBindingMessage(_m, _origin, message.frameInfo.isMainFrame); +} /***** Dialogs ******/ -(void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength { WailsAlert *alert = [WailsAlert new]; - + int style = NSAlertStyleInformational; if (dialogType != nil ) { if( [dialogType isEqualToString:@"warning"] ) { @@ -499,12 +541,12 @@ typedef void (^schemeTaskCaller)(id); if( message != nil ) { [alert setInformativeText:message]; } - + [alert addButton:button1 :defaultButton :cancelButton]; [alert addButton:button2 :defaultButton :cancelButton]; [alert addButton:button3 :defaultButton :cancelButton]; [alert addButton:button4 :defaultButton :cancelButton]; - + NSImage *icon = nil; if (iconData != nil) { NSData *imageData = [NSData dataWithBytes:iconData length:iconDataLength]; @@ -533,8 +575,8 @@ typedef void (^schemeTaskCaller)(id); } -(void) OpenFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)allowDirectories :(bool)allowFiles :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)resolveAliases :(bool)showHiddenFiles :(bool)allowMultipleSelection :(NSString*)filters { - - + + // Create the dialog NSOpenPanel *dialog = [NSOpenPanel openPanel]; @@ -552,14 +594,18 @@ typedef void (^schemeTaskCaller)(id); #ifdef USE_NEW_FILTERS NSMutableArray *contentTypes = [[NSMutableArray new] autorelease]; for (NSString *filter in filterList) { +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 if (@available(macOS 11.0, *)) { UTType *t = [UTType typeWithFilenameExtension:filter]; [contentTypes addObject:t]; } +#endif } +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 if (@available(macOS 11.0, *)) { [dialog setAllowedContentTypes:contentTypes]; } +#endif #else [dialog setAllowedFileTypes:filterList]; #endif @@ -570,11 +616,10 @@ typedef void (^schemeTaskCaller)(id); if( defaultFilename != nil ) { [dialog setNameFieldStringValue:defaultFilename]; } - - [dialog setAllowsMultipleSelection: allowMultipleSelection]; - [dialog setShowsHiddenFiles: showHiddenFiles]; + [dialog setAllowsMultipleSelection: allowMultipleSelection]; } + [dialog setShowsHiddenFiles: showHiddenFiles]; // Default Directory if( defaultDirectory != nil ) { @@ -606,19 +651,19 @@ typedef void (^schemeTaskCaller)(id); [nsjson release]; [arr release]; }]; - + } -(void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters; { - - + + // Create the dialog NSSavePanel *dialog = [NSSavePanel savePanel]; // Do not hide extension [dialog setExtensionHidden:false]; - + // Valid but appears to do nothing.... :/ if( title != nil ) { [dialog setTitle:title]; @@ -632,17 +677,21 @@ typedef void (^schemeTaskCaller)(id); #ifdef USE_NEW_FILTERS NSMutableArray *contentTypes = [[NSMutableArray new] autorelease]; for (NSString *filter in filterList) { +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 if (@available(macOS 11.0, *)) { UTType *t = [UTType typeWithFilenameExtension:filter]; [contentTypes addObject:t]; } +#endif } if( contentTypes.count == 0) { [dialog setAllowsOtherFileTypes:true]; } else { +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 if (@available(macOS 11.0, *)) { [dialog setAllowedContentTypes:contentTypes]; } +#endif } #else @@ -655,7 +704,7 @@ typedef void (^schemeTaskCaller)(id); if( defaultFilename != nil ) { [dialog setNameFieldStringValue:defaultFilename]; } - + // Default Directory if( defaultDirectory != nil ) { NSURL *url = [NSURL fileURLWithPath:defaultDirectory]; @@ -680,19 +729,370 @@ typedef void (^schemeTaskCaller)(id); } processSaveFileDialogResponse(""); }]; - + } +/***** Notifications ******/ +- (bool) IsNotificationAvailable { + if (@available(macOS 10.14, *)) { + return YES; + } else { + return NO; + } +} + +- (bool) CheckBundleIdentifier { + NSBundle *main = [NSBundle mainBundle]; + if (main.bundleIdentifier == nil) { + return NO; + } + return YES; +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + willPresentNotification:(UNNotification *)notification + withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler API_AVAILABLE(macos(10.14)) { + UNNotificationPresentationOptions options = UNNotificationPresentationOptionSound; + + if (@available(macOS 11.0, *)) { + // These options are only available in macOS 11.0+ + options = UNNotificationPresentationOptionList | + UNNotificationPresentationOptionBanner | + UNNotificationPresentationOptionSound; + } + + completionHandler(options); +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center +didReceiveNotificationResponse:(UNNotificationResponse *)response + withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(macos(10.14)) { + + NSMutableDictionary *payload = [NSMutableDictionary dictionary]; + + [payload setObject:response.notification.request.identifier forKey:@"id"]; + [payload setObject:response.actionIdentifier forKey:@"actionIdentifier"]; + [payload setObject:response.notification.request.content.title ?: @"" forKey:@"title"]; + [payload setObject:response.notification.request.content.body ?: @"" forKey:@"body"]; + + if (response.notification.request.content.categoryIdentifier) { + [payload setObject:response.notification.request.content.categoryIdentifier forKey:@"categoryId"]; + } + + if (response.notification.request.content.subtitle) { + [payload setObject:response.notification.request.content.subtitle forKey:@"subtitle"]; + } + + if (response.notification.request.content.userInfo) { + [payload setObject:response.notification.request.content.userInfo forKey:@"userInfo"]; + } + + if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) { + UNTextInputNotificationResponse *textResponse = (UNTextInputNotificationResponse *)response; + [payload setObject:textResponse.userText forKey:@"userText"]; + } + + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error]; + if (error) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; + didReceiveNotificationResponse(NULL, [errorMsg UTF8String]); + } else { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + didReceiveNotificationResponse([jsonString UTF8String], NULL); + } + + completionHandler(); +} + +- (bool) EnsureDelegateInitialized { + if (@available(macOS 10.14, *)) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = (id)self; + return YES; + } + return NO; +} + +- (void) RequestNotificationAuthorization :(int)channelID { + if (@available(macOS 10.14, *)) { + if (![self EnsureDelegateInitialized]) { + NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service."; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; + + [center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) { + if (error) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + } else { + captureResult(channelID, granted, NULL); + } + }]; + } else { + captureResult(channelID, false, "Notifications not available on macOS versions prior to 10.14"); + } +} + +- (void) CheckNotificationAuthorization :(int) channelID { + if (@available(macOS 10.14, *)) { + if (![self EnsureDelegateInitialized]) { + NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service."; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) { + BOOL isAuthorized = (settings.authorizationStatus == UNAuthorizationStatusAuthorized); + captureResult(channelID, isAuthorized, NULL); + }]; + } else { + captureResult(channelID, false, "Notifications not available on macOS versions prior to 10.14"); + } +} + +- (UNMutableNotificationContent *)createNotificationContent:(const char *)title subtitle:(const char *)subtitle body:(const char *)body dataJSON:(const char *)dataJSON error:(NSError **)contentError API_AVAILABLE(macos(10.14)) { + if (title == NULL) title = ""; + if (body == NULL) body = ""; + + NSString *nsTitle = [NSString stringWithUTF8String:title]; + NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @""; + NSString *nsBody = [NSString stringWithUTF8String:body]; + + UNMutableNotificationContent *content = [[[UNMutableNotificationContent alloc] init] autorelease]; + content.title = nsTitle; + if (![nsSubtitle isEqualToString:@""]) { + content.subtitle = nsSubtitle; + } + content.body = nsBody; + content.sound = [UNNotificationSound defaultSound]; + + // Parse JSON data if provided + if (dataJSON) { + NSString *dataJsonStr = [NSString stringWithUTF8String:dataJSON]; + NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error = nil; + NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + if (!error && parsedData) { + content.userInfo = parsedData; + } else if (error) { + if (contentError) *contentError = error; + } + } + + return content; +} + +- (void) sendNotificationWithRequest:(UNNotificationRequest *)request channelID:(int)channelID API_AVAILABLE(macos(10.14)) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { + if (error) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + } else { + captureResult(channelID, true, NULL); + } + }]; +} + +- (void) SendNotification:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)dataJSON API_AVAILABLE(macos(10.14)) { + if (![self EnsureDelegateInitialized]) { + NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service."; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; + + NSError *contentError = nil; + UNMutableNotificationContent *content = [self createNotificationContent:title subtitle:subtitle body:body dataJSON:dataJSON error:&contentError]; + if (contentError) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [contentError localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + UNTimeIntervalNotificationTrigger *trigger = nil; + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger]; + + [self sendNotificationWithRequest:request channelID:channelID]; +} + +- (void) SendNotificationWithActions:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)categoryId :(const char *)dataJSON API_AVAILABLE(macos(10.14)) { + if (![self EnsureDelegateInitialized]) { + NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service."; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; + NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId]; + + NSError *contentError = nil; + UNMutableNotificationContent *content = [self createNotificationContent:title subtitle:subtitle body:body dataJSON:dataJSON error:&contentError]; + if (contentError) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [contentError localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + content.categoryIdentifier = nsCategoryId; + + UNTimeIntervalNotificationTrigger *trigger = nil; + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger]; + + [self sendNotificationWithRequest:request channelID:channelID]; +} + +- (void) RegisterNotificationCategory:(int)channelID :(const char *)categoryId :(const char *)actionsJSON :(bool)hasReplyField :(const char *)replyPlaceholder :(const char *)replyButtonTitle API_AVAILABLE(macos(10.14)) { + if (![self EnsureDelegateInitialized]) { + NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service."; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId]; + NSString *actionsJsonStr = actionsJSON ? [NSString stringWithUTF8String:actionsJSON] : @"[]"; + + NSData *jsonData = [actionsJsonStr dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error = nil; + NSArray *actionsArray = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + + if (error) { + NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + + NSMutableArray *actions = [NSMutableArray array]; + for (NSDictionary *actionDict in actionsArray) { + NSString *actionId = actionDict[@"id"]; + NSString *actionTitle = actionDict[@"title"]; + BOOL destructive = [actionDict[@"destructive"] boolValue]; + + if (actionId && actionTitle) { + UNNotificationActionOptions options = UNNotificationActionOptionNone; + if (destructive) options |= UNNotificationActionOptionDestructive; + + UNNotificationAction *action = [UNNotificationAction actionWithIdentifier:actionId + title:actionTitle + options:options]; + [actions addObject:action]; + } + } + + if (hasReplyField) { + // Defensive NULL checks: if hasReplyField is true, both strings must be non-NULL + if (!replyPlaceholder || !replyButtonTitle) { + NSString *errorMsg = @"hasReplyField is true but replyPlaceholder or replyButtonTitle is NULL"; + captureResult(channelID, false, [errorMsg UTF8String]); + return; + } + NSString *placeholder = [NSString stringWithUTF8String:replyPlaceholder]; + NSString *buttonTitle = [NSString stringWithUTF8String:replyButtonTitle]; + UNTextInputNotificationAction *textAction = + [UNTextInputNotificationAction actionWithIdentifier:@"TEXT_REPLY" + title:buttonTitle + options:UNNotificationActionOptionNone + textInputButtonTitle:buttonTitle + textInputPlaceholder:placeholder]; + [actions addObject:textAction]; + } + + UNNotificationCategory *newCategory = [UNNotificationCategory categoryWithIdentifier:nsCategoryId + actions:actions + intentIdentifiers:@[] + options:UNNotificationCategoryOptionNone]; + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center getNotificationCategoriesWithCompletionHandler:^(NSSet *categories) { + NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories]; + + // Remove existing category with same identifier if found + UNNotificationCategory *existingCategory = nil; + for (UNNotificationCategory *category in updatedCategories) { + if ([category.identifier isEqualToString:nsCategoryId]) { + existingCategory = category; + break; + } + } + if (existingCategory) { + [updatedCategories removeObject:existingCategory]; + } + + // Add the new category + [updatedCategories addObject:newCategory]; + [center setNotificationCategories:updatedCategories]; + + captureResult(channelID, true, NULL); + }]; +} + +- (void) RemoveNotificationCategory:(int)channelID :(const char *)categoryId API_AVAILABLE(macos(10.14)) { + NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + + [center getNotificationCategoriesWithCompletionHandler:^(NSSet *categories) { + NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories]; + + // Find and remove the matching category + UNNotificationCategory *categoryToRemove = nil; + for (UNNotificationCategory *category in updatedCategories) { + if ([category.identifier isEqualToString:nsCategoryId]) { + categoryToRemove = category; + break; + } + } + + if (categoryToRemove) { + [updatedCategories removeObject:categoryToRemove]; + [center setNotificationCategories:updatedCategories]; + captureResult(channelID, true, NULL); + } else { + NSString *errorMsg = [NSString stringWithFormat:@"Category '%@' not found", nsCategoryId]; + captureResult(channelID, false, [errorMsg UTF8String]); + } + }]; +} + +- (void) RemoveAllPendingNotifications API_AVAILABLE(macos(10.14)) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removeAllPendingNotificationRequests]; +} + +- (void) RemovePendingNotification:(const char *)identifier API_AVAILABLE(macos(10.14)) { + NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removePendingNotificationRequestsWithIdentifiers:@[nsIdentifier]]; +} + +- (void) RemoveAllDeliveredNotifications API_AVAILABLE(macos(10.14)) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removeAllDeliveredNotifications]; +} + +- (void) RemoveDeliveredNotification:(const char *)identifier API_AVAILABLE(macos(10.14)) { + NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removeDeliveredNotificationsWithIdentifiers:@[nsIdentifier]]; +} + + - (void) SetAbout :(NSString*)title :(NSString*)description :(void*)imagedata :(int)datalen { self.aboutTitle = title; self.aboutDescription = description; - + NSData *imageData = [NSData dataWithBytes:imagedata length:datalen]; self.aboutImage = [[NSImage alloc] initWithData:imageData]; } --(void) About { - +- (void) About { + WailsAlert *alert = [WailsAlert new]; [alert setAlertStyle:NSAlertStyleInformational]; if( self.aboutTitle != nil ) { @@ -701,8 +1101,8 @@ typedef void (^schemeTaskCaller)(id); if( self.aboutDescription != nil ) { [alert setInformativeText:self.aboutDescription]; } - - + + [alert.window setLevel:NSFloatingWindowLevel]; if ( self.aboutImage != nil) { [alert setIcon:self.aboutImage]; diff --git a/v2/internal/frontend/desktop/darwin/WailsWebView.h b/v2/internal/frontend/desktop/darwin/WailsWebView.h new file mode 100644 index 000000000..b6f746cf2 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsWebView.h @@ -0,0 +1,14 @@ +#ifndef WailsWebView_h +#define WailsWebView_h + +#import +#import + +// We will override WKWebView, so we can detect file drop in obj-c +// and grab their file path, to then inject into JS +@interface WailsWebView : WKWebView +@property bool disableWebViewDragAndDrop; +@property bool enableDragAndDrop; +@end + +#endif /* WailsWebView_h */ diff --git a/v2/internal/frontend/desktop/darwin/WailsWebView.m b/v2/internal/frontend/desktop/darwin/WailsWebView.m new file mode 100644 index 000000000..de23ac794 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/WailsWebView.m @@ -0,0 +1,122 @@ +#import "WailsWebView.h" +#import "message.h" + + +@implementation WailsWebView +@synthesize disableWebViewDragAndDrop; +@synthesize enableDragAndDrop; + +- (BOOL)prepareForDragOperation:(id)sender +{ + if ( !enableDragAndDrop ) { + return [super prepareForDragOperation: sender]; + } + + if ( disableWebViewDragAndDrop ) { + return YES; + } + + return [super prepareForDragOperation: sender]; +} + +- (BOOL)performDragOperation:(id )sender +{ + if ( !enableDragAndDrop ) { + return [super performDragOperation: sender]; + } + + NSPasteboard *pboard = [sender draggingPasteboard]; + + // if no types, then we'll just let the WKWebView handle the drag-n-drop as normal + NSArray * types = [pboard types]; + if( !types ) + return [super performDragOperation: sender]; + + // getting all NSURL types + NSArray *url_class = @[[NSURL class]]; + NSDictionary *options = @{}; + NSArray *files = [pboard readObjectsForClasses:url_class options:options]; + + // collecting all file paths + NSMutableArray *files_strs = [[NSMutableArray alloc] init]; + for (NSURL *url in files) + { + const char *fs_path = [url fileSystemRepresentation]; //Will be UTF-8 encoded + NSString *fs_path_str = [[NSString alloc] initWithCString:fs_path encoding:NSUTF8StringEncoding]; + [files_strs addObject:fs_path_str]; +// NSLog( @"performDragOperation: file path: %s", fs_path ); + } + + NSString *joined=[files_strs componentsJoinedByString:@"\n"]; + + // Release the array of file paths + [files_strs release]; + + int dragXLocation = [sender draggingLocation].x - [self frame].origin.x; + int dragYLocation = [self frame].size.height - [sender draggingLocation].y; // Y coordinate is inverted, so we need to subtract from the height + +// NSLog( @"draggingUpdated: X coord: %d", dragXLocation ); +// NSLog( @"draggingUpdated: Y coord: %d", dragYLocation ); + + NSString *message = [NSString stringWithFormat:@"DD:%d:%d:%@", dragXLocation, dragYLocation, joined]; + + const char* res = message.UTF8String; + + processMessage(res); + + if ( disableWebViewDragAndDrop ) { + return YES; + } + + return [super performDragOperation: sender]; +} + +- (NSDragOperation)draggingUpdated:(id )sender { + if ( !enableDragAndDrop ) { + return [super draggingUpdated: sender]; + } + + NSPasteboard *pboard = [sender draggingPasteboard]; + + // if no types, then we'll just let the WKWebView handle the drag-n-drop as normal + NSArray * types = [pboard types]; + if( !types ) { + return [super draggingUpdated: sender]; + } + + if ( disableWebViewDragAndDrop ) { + // we should call supper as otherwise events will not pass + [super draggingUpdated: sender]; + + // pass NSDragOperationGeneric = 4 to show regular hover for drag and drop. As we want to ignore webkit behaviours that depends on webpage + return 4; + } + + return [super draggingUpdated: sender]; +} + +- (NSDragOperation)draggingEntered:(id )sender { + if ( !enableDragAndDrop ) { + return [super draggingEntered: sender]; + } + + NSPasteboard *pboard = [sender draggingPasteboard]; + + // if no types, then we'll just let the WKWebView handle the drag-n-drop as normal + NSArray * types = [pboard types]; + if( !types ) { + return [super draggingEntered: sender]; + } + + if ( disableWebViewDragAndDrop ) { + // we should call supper as otherwise events will not pass + [super draggingEntered: sender]; + + // pass NSDragOperationGeneric = 4 to show regular hover for drag and drop. As we want to ignore webkit behaviours that depends on webpage + return 4; + } + + return [super draggingEntered: sender]; +} + +@end diff --git a/v2/internal/frontend/desktop/darwin/browser.go b/v2/internal/frontend/desktop/darwin/browser.go index 417501c8e..c865ab6d9 100644 --- a/v2/internal/frontend/desktop/darwin/browser.go +++ b/v2/internal/frontend/desktop/darwin/browser.go @@ -4,11 +4,21 @@ package darwin import ( + "fmt" "github.com/pkg/browser" + "github.com/wailsapp/wails/v2/internal/frontend/utils" ) // BrowserOpenURL Use the default browser to open the url -func (f *Frontend) BrowserOpenURL(url string) { +func (f *Frontend) BrowserOpenURL(rawURL string) { + url, err := utils.ValidateAndSanitizeURL(rawURL) + if err != nil { + f.logger.Error(fmt.Sprintf("Invalid URL %s", err.Error())) + return + } + // Specific method implementation - _ = browser.OpenURL(url) + if err := browser.OpenURL(url); err != nil { + f.logger.Error("Unable to open default system browser") + } } diff --git a/v2/internal/frontend/desktop/darwin/callbacks.go b/v2/internal/frontend/desktop/darwin/callbacks.go index 7d930a2f9..ab0d18e47 100644 --- a/v2/internal/frontend/desktop/darwin/callbacks.go +++ b/v2/internal/frontend/desktop/darwin/callbacks.go @@ -12,6 +12,7 @@ package darwin #include */ import "C" + import ( "errors" "strconv" @@ -20,7 +21,6 @@ import ( ) func (f *Frontend) handleCallback(menuItemID uint) error { - menuItem := getMenuItemForID(menuItemID) if menuItem == nil { return errors.New("unknown menuItem ID: " + strconv.Itoa(int(menuItemID))) diff --git a/v2/internal/frontend/desktop/darwin/clipboard.go b/v2/internal/frontend/desktop/darwin/clipboard.go index eea6c79ae..c40ba8771 100644 --- a/v2/internal/frontend/desktop/darwin/clipboard.go +++ b/v2/internal/frontend/desktop/darwin/clipboard.go @@ -16,7 +16,6 @@ func (f *Frontend) ClipboardGetText() (string, error) { } func (f *Frontend) ClipboardSetText(text string) error { - copyCmd := exec.Command("pbcopy") in, err := copyCmd.StdinPipe() if err != nil { diff --git a/v2/internal/frontend/desktop/darwin/dialog.go b/v2/internal/frontend/desktop/darwin/dialog.go index c6be559cb..66bb2f13a 100644 --- a/v2/internal/frontend/desktop/darwin/dialog.go +++ b/v2/internal/frontend/desktop/darwin/dialog.go @@ -11,6 +11,7 @@ package darwin #import "WailsContext.h" */ import "C" + import ( "encoding/json" "fmt" @@ -23,10 +24,12 @@ import ( ) // Obj-C dialog methods send the response to this channel -var messageDialogResponse = make(chan int) -var openFileDialogResponse = make(chan string) -var saveFileDialogResponse = make(chan string) -var dialogLock sync.Mutex +var ( + messageDialogResponse = make(chan int) + openFileDialogResponse = make(chan string) + saveFileDialogResponse = make(chan string) + dialogLock sync.Mutex +) // OpenDirectoryDialog prompts the user to select a directory func (f *Frontend) OpenDirectoryDialog(options frontend.OpenDialogOptions) (string, error) { @@ -74,7 +77,7 @@ func (f *Frontend) openDialog(options *frontend.OpenDialogOptions, multiple bool filters := filterStrings.Join(";") C.OpenFileDialog(f.mainWindow.context, title, defaultFilename, defaultDirectory, allowDirectories, allowFiles, canCreateDirectories, treatPackagesAsDirectories, resolveAliases, showHiddenFiles, allowMultipleFileSelection, c.String(filters)) - var result = <-openFileDialogResponse + result := <-openFileDialogResponse var parsedResults []string err := json.Unmarshal([]byte(result), &parsedResults) @@ -130,7 +133,7 @@ func (f *Frontend) SaveFileDialog(options frontend.SaveDialogOptions) (string, e filters := filterStrings.Join(";") C.SaveFileDialog(f.mainWindow.context, title, defaultFilename, defaultDirectory, canCreateDirectories, treatPackagesAsDirectories, showHiddenFiles, c.String(filters)) - var result = <-saveFileDialogResponse + result := <-saveFileDialogResponse return result, nil } @@ -165,7 +168,7 @@ func (f *Frontend) MessageDialog(options frontend.MessageDialogOptions) (string, C.MessageDialog(f.mainWindow.context, dialogType, title, message, buttons[0], buttons[1], buttons[2], buttons[3], defaultButton, cancelButton, iconData, iconDataLength) - var result = <-messageDialogResponse + result := <-messageDialogResponse selectedC := buttons[result] var selected string diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index a6a0808fd..6566445d5 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -8,11 +8,13 @@ package darwin #cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit #import #import "Application.h" +#import "CustomProtocol.h" #import "WailsContext.h" #include */ import "C" + import ( "context" "encoding/json" @@ -21,6 +23,7 @@ import ( "log" "net" "net/url" + "os" "unsafe" "github.com/wailsapp/wails/v2/pkg/assetserver" @@ -28,6 +31,7 @@ import ( "github.com/wailsapp/wails/v2/internal/binding" "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/originvalidator" "github.com/wailsapp/wails/v2/internal/frontend/runtime" "github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/pkg/options" @@ -35,12 +39,23 @@ import ( const startURL = "wails://wails/" -var messageBuffer = make(chan string, 100) -var requestBuffer = make(chan webview.Request, 100) -var callbackBuffer = make(chan uint, 10) +type bindingsMessage struct { + message string + source string + isMainFrame bool +} + +var ( + messageBuffer = make(chan string, 100) + bindingsMessageBuffer = make(chan *bindingsMessage, 100) + requestBuffer = make(chan webview.Request, 100) + callbackBuffer = make(chan uint, 10) + openFilepathBuffer = make(chan string, 100) + openUrlBuffer = make(chan string, 100) + secondInstanceBuffer = make(chan options.SecondInstanceData, 1) +) type Frontend struct { - // Context ctx context.Context @@ -49,6 +64,9 @@ type Frontend struct { debug bool devtoolsEnabled bool + // Keep single instance lock file, so that it will not be GC and lock will exist while app is running + singleInstanceLockFile *os.File + // Assets assets *assetserver.AssetServer startURL *url.URL @@ -57,6 +75,8 @@ type Frontend struct { mainWindow *Window bindings *binding.Bindings dispatcher frontend.Dispatcher + + originValidator *originvalidator.OriginValidator } func (f *Frontend) RunMainLoop() { @@ -76,12 +96,18 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. ctx: ctx, } result.startURL, _ = url.Parse(startURL) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) + + // this should be initialized as early as possible to handle first instance launch + C.StartCustomProtocolHandler() if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil { result.startURL = _starturl + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) } else { if port, _ := ctx.Value("assetserverport").(string); port != "" { result.startURL.Host = net.JoinHostPort(result.startURL.Host+".localhost", port) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) } var bindings string @@ -106,21 +132,72 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. } go result.startMessageProcessor() + go result.startBindingsMessageProcessor() go result.startCallbackProcessor() + go result.startFileOpenProcessor() + go result.startUrlOpenProcessor() + go result.startSecondInstanceProcessor() return result } +func (f *Frontend) startFileOpenProcessor() { + for filePath := range openFilepathBuffer { + f.ProcessOpenFileEvent(filePath) + } +} + +func (f *Frontend) startUrlOpenProcessor() { + for url := range openUrlBuffer { + f.ProcessOpenUrlEvent(url) + } +} + +func (f *Frontend) startSecondInstanceProcessor() { + for secondInstanceData := range secondInstanceBuffer { + if f.frontendOptions.SingleInstanceLock != nil && + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + } + } +} + func (f *Frontend) startMessageProcessor() { for message := range messageBuffer { f.processMessage(message) } } + +func (f *Frontend) startBindingsMessageProcessor() { + for msg := range bindingsMessageBuffer { + // Apple webkit doesn't provide origin of main frame. So we can't verify in case of iFrame that top level origin is allowed. + if !msg.isMainFrame { + f.logger.Error("Blocked request from not main frame") + continue + } + + origin, err := f.originValidator.GetOriginFromURL(msg.source) + if err != nil { + f.logger.Error(fmt.Sprintf("failed to get origin for URL %q: %v", msg.source, err)) + continue + } + + allowed := f.originValidator.IsOriginAllowed(origin) + if !allowed { + f.logger.Error("Blocked request from unauthorized origin: %s", origin) + continue + } + + f.processMessage(msg.message) + } +} + func (f *Frontend) startRequestProcessor() { for request := range requestBuffer { f.assets.ServeWebViewRequest(request) } } + func (f *Frontend) startCallbackProcessor() { for callback := range callbackBuffer { err := f.handleCallback(callback) @@ -139,22 +216,23 @@ func (f *Frontend) WindowReloadApp() { } func (f *Frontend) WindowSetSystemDefaultTheme() { - return } func (f *Frontend) WindowSetLightTheme() { - return } func (f *Frontend) WindowSetDarkTheme() { - return } func (f *Frontend) Run(ctx context.Context) error { f.ctx = ctx - var _debug = ctx.Value("debug") - var _devtoolsEnabled = ctx.Value("devtoolsEnabled") + if f.frontendOptions.SingleInstanceLock != nil { + f.singleInstanceLockFile = SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId) + } + + _debug := ctx.Value("debug") + _devtoolsEnabled := ctx.Value("devtoolsEnabled") if _debug != nil { f.debug = _debug.(bool) @@ -179,6 +257,7 @@ func (f *Frontend) Run(ctx context.Context) error { func (f *Frontend) WindowCenter() { f.mainWindow.Center() } + func (f *Frontend) WindowSetAlwaysOnTop(onTop bool) { f.mainWindow.SetAlwaysOnTop(onTop) } @@ -186,6 +265,7 @@ func (f *Frontend) WindowSetAlwaysOnTop(onTop bool) { func (f *Frontend) WindowSetPosition(x, y int) { f.mainWindow.SetPosition(x, y) } + func (f *Frontend) WindowGetPosition() (int, int) { return f.mainWindow.GetPosition() } @@ -217,6 +297,7 @@ func (f *Frontend) WindowShow() { func (f *Frontend) WindowHide() { f.mainWindow.Hide() } + func (f *Frontend) Show() { f.mainWindow.ShowApplication() } @@ -224,18 +305,23 @@ func (f *Frontend) Show() { func (f *Frontend) Hide() { f.mainWindow.HideApplication() } + func (f *Frontend) WindowMaximise() { f.mainWindow.Maximise() } + func (f *Frontend) WindowToggleMaximise() { f.mainWindow.ToggleMaximise() } + func (f *Frontend) WindowUnmaximise() { f.mainWindow.UnMaximise() } + func (f *Frontend) WindowMinimise() { f.mainWindow.Minimise() } + func (f *Frontend) WindowUnminimise() { f.mainWindow.UnMinimise() } @@ -243,6 +329,7 @@ func (f *Frontend) WindowUnminimise() { func (f *Frontend) WindowSetMinSize(width int, height int) { f.mainWindow.SetMinSize(width, height) } + func (f *Frontend) WindowSetMaxSize(width int, height int) { f.mainWindow.SetMaxSize(width, height) } @@ -309,7 +396,6 @@ func (f *Frontend) Notify(name string, data ...interface{}) { } func (f *Frontend) processMessage(message string) { - if message == "DomReady" { if f.frontendOptions.OnDomReady != nil { f.frontendOptions.OnDomReady(f.ctx) @@ -320,6 +406,11 @@ func (f *Frontend) processMessage(message string) { if message == "runtime:ready" { cmd := fmt.Sprintf("window.wails.setCSSDragProperties('%s', '%s');", f.frontendOptions.CSSDragProperty, f.frontendOptions.CSSDragValue) f.ExecJS(cmd) + + if f.frontendOptions.DragAndDrop != nil && f.frontendOptions.DragAndDrop.EnableFileDrop { + f.ExecJS("window.wails.flags.enableWailsDragAndDrop = true;") + } + return } @@ -352,7 +443,18 @@ func (f *Frontend) processMessage(message string) { f.logger.Info("Unknown message returned from dispatcher: %+v", result) } }() +} +func (f *Frontend) ProcessOpenFileEvent(filePath string) { + if f.frontendOptions.Mac != nil && f.frontendOptions.Mac.OnFileOpen != nil { + f.frontendOptions.Mac.OnFileOpen(filePath) + } +} + +func (f *Frontend) ProcessOpenUrlEvent(url string) { + if f.frontendOptions.Mac != nil && f.frontendOptions.Mac.OnUrlOpen != nil { + f.frontendOptions.Mac.OnUrlOpen(url) + } } func (f *Frontend) Callback(message string) { @@ -389,6 +491,17 @@ func processMessage(message *C.char) { messageBuffer <- goMessage } +//export processBindingMessage +func processBindingMessage(message *C.char, source *C.char, fromMainFrame bool) { + goMessage := C.GoString(message) + goSource := C.GoString(source) + bindingsMessageBuffer <- &bindingsMessage{ + message: goMessage, + source: goSource, + isMainFrame: fromMainFrame, + } +} + //export processCallback func processCallback(callbackID uint) { callbackBuffer <- callbackID @@ -398,3 +511,15 @@ func processCallback(callbackID uint) { func processURLRequest(_ unsafe.Pointer, wkURLSchemeTask unsafe.Pointer) { requestBuffer <- webview.NewRequest(wkURLSchemeTask) } + +//export HandleOpenFile +func HandleOpenFile(filePath *C.char) { + goFilepath := C.GoString(filePath) + openFilepathBuffer <- goFilepath +} + +//export HandleOpenURL +func HandleOpenURL(url *C.char) { + goUrl := C.GoString(url) + openUrlBuffer <- goUrl +} diff --git a/v2/internal/frontend/desktop/darwin/inspector.go b/v2/internal/frontend/desktop/darwin/inspector.go index 8499745d0..dc3f08969 100644 --- a/v2/internal/frontend/desktop/darwin/inspector.go +++ b/v2/internal/frontend/desktop/darwin/inspector.go @@ -7,5 +7,4 @@ import ( ) func showInspector(_ unsafe.Pointer) { - } diff --git a/v2/internal/frontend/desktop/darwin/inspector_dev.go b/v2/internal/frontend/desktop/darwin/inspector_dev.go index f3520cb3e..e79b9c3e7 100644 --- a/v2/internal/frontend/desktop/darwin/inspector_dev.go +++ b/v2/internal/frontend/desktop/darwin/inspector_dev.go @@ -23,6 +23,7 @@ extern void processMessage(const char *message); @end void showInspector(void *inctx) { +#if MAC_OS_X_VERSION_MAX_ALLOWED >= 120000 ON_MAIN_THREAD( if (@available(macOS 12.0, *)) { WailsContext *ctx = (__bridge WailsContext*) inctx; @@ -47,7 +48,7 @@ void showInspector(void *inctx) { NSLog(@"Opening the inspector needs at least MacOS 12"); } ); - +#endif } void setupF12hotkey() { diff --git a/v2/internal/frontend/desktop/darwin/main.m b/v2/internal/frontend/desktop/darwin/main.m index 1dd8bb1f3..75a84dc76 100644 --- a/v2/internal/frontend/desktop/darwin/main.m +++ b/v2/internal/frontend/desktop/darwin/main.m @@ -203,6 +203,7 @@ int main(int argc, const char * argv[]) { // insert code here... int frameless = 0; int resizable = 1; + int zoomable = 0; int fullscreen = 1; int fullSizeContent = 1; int hideTitleBar = 0; @@ -219,7 +220,7 @@ int main(int argc, const char * argv[]) { int defaultContextMenuEnabled = 1; int windowStartState = 0; int startsHidden = 0; - WailsContext *result = Create("OI OI!",400,400, frameless, resizable, fullscreen, fullSizeContent, hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent, alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled, windowStartState, + WailsContext *result = Create("OI OI!",400,400, frameless, resizable, zoomable, fullscreen, fullSizeContent, hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent, alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled, windowStartState, startsHidden, 400, 400, 600, 600, false); SetBackgroundColour(result, 255, 0, 0, 255); void *m = NewMenu(""); diff --git a/v2/internal/frontend/desktop/darwin/menu.go b/v2/internal/frontend/desktop/darwin/menu.go index 08090f89a..24dbe3201 100644 --- a/v2/internal/frontend/desktop/darwin/menu.go +++ b/v2/internal/frontend/desktop/darwin/menu.go @@ -13,6 +13,7 @@ package darwin #include */ import "C" + import ( "unsafe" @@ -122,7 +123,6 @@ func processMenuItem(parent *NSMenu, menuItem *menu.MenuItem) *MenuItem { } return parent.AddMenuItem(menuItem) - } func (f *Frontend) MenuSetApplicationMenu(menu *menu.Menu) { diff --git a/v2/internal/frontend/desktop/darwin/menuitem.go b/v2/internal/frontend/desktop/darwin/menuitem.go index 00ad57aa3..64aab84a9 100644 --- a/v2/internal/frontend/desktop/darwin/menuitem.go +++ b/v2/internal/frontend/desktop/darwin/menuitem.go @@ -13,16 +13,19 @@ package darwin #include */ import "C" + import ( "log" "math" "sync" ) -var menuItemToID = make(map[*MenuItem]uint) -var idToMenuItem = make(map[uint]*MenuItem) -var menuItemLock sync.Mutex -var menuItemIDCounter uint = 0 +var ( + menuItemToID = make(map[*MenuItem]uint) + idToMenuItem = make(map[uint]*MenuItem) + menuItemLock sync.Mutex + menuItemIDCounter uint = 0 +) func createMenuItemID(item *MenuItem) uint { menuItemLock.Lock() diff --git a/v2/internal/frontend/desktop/darwin/message.h b/v2/internal/frontend/desktop/darwin/message.h index 66110841d..86506f868 100644 --- a/v2/internal/frontend/desktop/darwin/message.h +++ b/v2/internal/frontend/desktop/darwin/message.h @@ -15,6 +15,7 @@ extern "C" #endif void processMessage(const char *); +void processBindingMessage(const char *, const char *, bool); void processURLRequest(void *, void*); void processMessageDialogResponse(int); void processOpenFileDialogResponse(const char*); diff --git a/v2/internal/frontend/desktop/darwin/notifications.go b/v2/internal/frontend/desktop/darwin/notifications.go new file mode 100644 index 000000000..b788841e0 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/notifications.go @@ -0,0 +1,465 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS:-x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa + +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 +#cgo LDFLAGS: -framework UserNotifications +#endif + +#import "Application.h" +#import "WailsContext.h" +*/ +import "C" +import ( + "context" + "encoding/json" + "fmt" + "os" + "sync" + "time" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend" +) + +// Package-scoped variable only accessible within this file +var ( + currentFrontend *Frontend + frontendMutex sync.RWMutex + // Notification channels + channels map[int]chan notificationChannel + channelsLock sync.Mutex + nextChannelID int + + notificationResultCallback func(result frontend.NotificationResult) + callbackLock sync.RWMutex +) + +const DefaultActionIdentifier = "DEFAULT_ACTION" +const AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier" + +// setCurrentFrontend sets the current frontend instance +// This is called when RequestNotificationAuthorization or CheckNotificationAuthorization is called +func setCurrentFrontend(f *Frontend) { + frontendMutex.Lock() + defer frontendMutex.Unlock() + currentFrontend = f +} + +// getCurrentFrontend gets the current frontend instance +func getCurrentFrontend() *Frontend { + frontendMutex.RLock() + defer frontendMutex.RUnlock() + return currentFrontend +} + +type notificationChannel struct { + Success bool + Error error +} + +func (f *Frontend) InitializeNotifications() error { + if !f.IsNotificationAvailable() { + return fmt.Errorf("notifications are not available on this system") + } + if !f.checkBundleIdentifier() { + return fmt.Errorf("notifications require a valid bundle identifier") + } + if !bool(C.EnsureDelegateInitialized(f.mainWindow.context)) { + return fmt.Errorf("failed to initialize notification center delegate") + } + + channels = make(map[int]chan notificationChannel) + nextChannelID = 0 + + setCurrentFrontend(f) + + return nil +} + +// CleanupNotifications is a macOS stub that does nothing. +// (Linux-specific cleanup) +func (f *Frontend) CleanupNotifications() { + // No cleanup needed on macOS +} + +func (f *Frontend) IsNotificationAvailable() bool { + return bool(C.IsNotificationAvailable(f.mainWindow.context)) +} + +func (f *Frontend) checkBundleIdentifier() bool { + return bool(C.CheckBundleIdentifier(f.mainWindow.context)) +} + +func (f *Frontend) RequestNotificationAuthorization() (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) + defer cancel() + + id, resultCh := f.registerChannel() + + C.RequestNotificationAuthorization(f.mainWindow.context, C.int(id)) + + select { + case result := <-resultCh: + close(resultCh) + return result.Success, result.Error + case <-ctx.Done(): + f.cleanupChannel(id) + return false, fmt.Errorf("notification authorization timed out after 3 minutes: %w", ctx.Err()) + } +} + +func (f *Frontend) CheckNotificationAuthorization() (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + id, resultCh := f.registerChannel() + + C.CheckNotificationAuthorization(f.mainWindow.context, C.int(id)) + + select { + case result := <-resultCh: + close(resultCh) + return result.Success, result.Error + case <-ctx.Done(): + f.cleanupChannel(id) + return false, fmt.Errorf("notification authorization timed out after 15s: %w", ctx.Err()) + } +} + +// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body. +func (f *Frontend) SendNotification(options frontend.NotificationOptions) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cIdentifier := C.CString(options.ID) + cTitle := C.CString(options.Title) + cSubtitle := C.CString(options.Subtitle) + cBody := C.CString(options.Body) + defer C.free(unsafe.Pointer(cIdentifier)) + defer C.free(unsafe.Pointer(cTitle)) + defer C.free(unsafe.Pointer(cSubtitle)) + defer C.free(unsafe.Pointer(cBody)) + + var cDataJSON *C.char + if options.Data != nil { + jsonData, err := json.Marshal(options.Data) + if err != nil { + return fmt.Errorf("failed to marshal notification data: %w", err) + } + cDataJSON = C.CString(string(jsonData)) + defer C.free(unsafe.Pointer(cDataJSON)) + } + + id, resultCh := f.registerChannel() + C.SendNotification(f.mainWindow.context, C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cDataJSON) + + select { + case result := <-resultCh: + close(resultCh) + if !result.Success { + if result.Error != nil { + return result.Error + } + return fmt.Errorf("sending notification failed") + } + return nil + case <-ctx.Done(): + f.cleanupChannel(id) + return fmt.Errorf("sending notification timed out: %w", ctx.Err()) + } +} + +// SendNotificationWithActions sends a notification with additional actions and inputs. +// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category. +// If a NotificationCategory is not registered a basic notification will be sent. +func (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cIdentifier := C.CString(options.ID) + cTitle := C.CString(options.Title) + cSubtitle := C.CString(options.Subtitle) + cBody := C.CString(options.Body) + cCategoryID := C.CString(options.CategoryID) + defer C.free(unsafe.Pointer(cIdentifier)) + defer C.free(unsafe.Pointer(cTitle)) + defer C.free(unsafe.Pointer(cSubtitle)) + defer C.free(unsafe.Pointer(cBody)) + defer C.free(unsafe.Pointer(cCategoryID)) + + var cDataJSON *C.char + if options.Data != nil { + jsonData, err := json.Marshal(options.Data) + if err != nil { + return fmt.Errorf("failed to marshal notification data: %w", err) + } + cDataJSON = C.CString(string(jsonData)) + defer C.free(unsafe.Pointer(cDataJSON)) + } + + id, resultCh := f.registerChannel() + C.SendNotificationWithActions(f.mainWindow.context, C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cCategoryID, cDataJSON) + + select { + case result := <-resultCh: + close(resultCh) + if !result.Success { + if result.Error != nil { + return result.Error + } + return fmt.Errorf("sending notification failed") + } + return nil + case <-ctx.Done(): + f.cleanupChannel(id) + return fmt.Errorf("sending notification timed out: %w", ctx.Err()) + } +} + +// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions. +// Registering a category with the same name as a previously registered NotificationCategory will override it. +func (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cCategoryID := C.CString(category.ID) + defer C.free(unsafe.Pointer(cCategoryID)) + + actionsJSON, err := json.Marshal(category.Actions) + if err != nil { + return fmt.Errorf("failed to marshal notification category: %w", err) + } + cActionsJSON := C.CString(string(actionsJSON)) + defer C.free(unsafe.Pointer(cActionsJSON)) + + var cReplyPlaceholder, cReplyButtonTitle *C.char + if category.HasReplyField { + cReplyPlaceholder = C.CString(category.ReplyPlaceholder) + cReplyButtonTitle = C.CString(category.ReplyButtonTitle) + defer C.free(unsafe.Pointer(cReplyPlaceholder)) + defer C.free(unsafe.Pointer(cReplyButtonTitle)) + } + + id, resultCh := f.registerChannel() + C.RegisterNotificationCategory(f.mainWindow.context, C.int(id), cCategoryID, cActionsJSON, C.bool(category.HasReplyField), + cReplyPlaceholder, cReplyButtonTitle) + + select { + case result := <-resultCh: + close(resultCh) + if !result.Success { + if result.Error != nil { + return result.Error + } + return fmt.Errorf("category registration failed") + } + return nil + case <-ctx.Done(): + f.cleanupChannel(id) + return fmt.Errorf("category registration timed out: %w", ctx.Err()) + } +} + +// RemoveNotificationCategory remove a previously registered NotificationCategory. +func (f *Frontend) RemoveNotificationCategory(categoryId string) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cCategoryID := C.CString(categoryId) + defer C.free(unsafe.Pointer(cCategoryID)) + + id, resultCh := f.registerChannel() + C.RemoveNotificationCategory(f.mainWindow.context, C.int(id), cCategoryID) + + select { + case result := <-resultCh: + close(resultCh) + if !result.Success { + if result.Error != nil { + return result.Error + } + return fmt.Errorf("category removal failed") + } + return nil + case <-ctx.Done(): + f.cleanupChannel(id) + return fmt.Errorf("category removal timed out: %w", ctx.Err()) + } +} + +// RemoveAllPendingNotifications removes all pending notifications. +func (f *Frontend) RemoveAllPendingNotifications() error { + C.RemoveAllPendingNotifications(f.mainWindow.context) + return nil +} + +// RemovePendingNotification removes a pending notification matching the unique identifier. +func (f *Frontend) RemovePendingNotification(identifier string) error { + cIdentifier := C.CString(identifier) + defer C.free(unsafe.Pointer(cIdentifier)) + C.RemovePendingNotification(f.mainWindow.context, cIdentifier) + return nil +} + +// RemoveAllDeliveredNotifications removes all delivered notifications. +func (f *Frontend) RemoveAllDeliveredNotifications() error { + C.RemoveAllDeliveredNotifications(f.mainWindow.context) + return nil +} + +// RemoveDeliveredNotification removes a delivered notification matching the unique identifier. +func (f *Frontend) RemoveDeliveredNotification(identifier string) error { + cIdentifier := C.CString(identifier) + defer C.free(unsafe.Pointer(cIdentifier)) + C.RemoveDeliveredNotification(f.mainWindow.context, cIdentifier) + return nil +} + +// RemoveNotification is a macOS stub that always returns nil. +// Use one of the following instead: +// RemoveAllPendingNotifications +// RemovePendingNotification +// RemoveAllDeliveredNotifications +// RemoveDeliveredNotification +// (Linux-specific) +func (f *Frontend) RemoveNotification(identifier string) error { + return nil +} + +func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) { + callbackLock.Lock() + notificationResultCallback = callback + callbackLock.Unlock() +} + +//export captureResult +func captureResult(channelID C.int, success C.bool, errorMsg *C.char) { + f := getCurrentFrontend() + if f == nil { + return + } + + resultCh, exists := f.GetChannel(int(channelID)) + if !exists { + return + } + + var err error + if errorMsg != nil { + err = fmt.Errorf("%s", C.GoString(errorMsg)) + } + + resultCh <- notificationChannel{ + Success: bool(success), + Error: err, + } +} + +//export didReceiveNotificationResponse +func didReceiveNotificationResponse(jsonPayload *C.char, err *C.char) { + result := frontend.NotificationResult{} + + if err != nil { + errMsg := C.GoString(err) + result.Error = fmt.Errorf("notification response error: %s", errMsg) + handleNotificationResult(result) + + return + } + + if jsonPayload == nil { + result.Error = fmt.Errorf("received nil JSON payload in notification response") + handleNotificationResult(result) + return + } + + payload := C.GoString(jsonPayload) + + var response frontend.NotificationResponse + if err := json.Unmarshal([]byte(payload), &response); err != nil { + result.Error = fmt.Errorf("failed to unmarshal notification response: %w", err) + handleNotificationResult(result) + return + } + + if response.ActionIdentifier == AppleDefaultActionIdentifier { + response.ActionIdentifier = DefaultActionIdentifier + } + + result.Response = response + handleNotificationResult(result) +} + +func handleNotificationResult(result frontend.NotificationResult) { + callbackLock.Lock() + callback := notificationResultCallback + callbackLock.Unlock() + + if callback != nil { + go func() { + defer func() { + if r := recover(); r != nil { + // Log panic but don't crash the app + fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r) + } + }() + callback(result) + }() + } +} + +// Helper methods + +func (f *Frontend) registerChannel() (int, chan notificationChannel) { + channelsLock.Lock() + defer channelsLock.Unlock() + + // Initialize channels map if it's nil + if channels == nil { + channels = make(map[int]chan notificationChannel) + nextChannelID = 0 + } + + id := nextChannelID + nextChannelID++ + + resultCh := make(chan notificationChannel, 1) + + channels[id] = resultCh + return id, resultCh +} + +func (f *Frontend) GetChannel(id int) (chan notificationChannel, bool) { + channelsLock.Lock() + defer channelsLock.Unlock() + + if channels == nil { + return nil, false + } + + ch, exists := channels[id] + if exists { + delete(channels, id) + } + return ch, exists +} + +func (f *Frontend) cleanupChannel(id int) { + channelsLock.Lock() + defer channelsLock.Unlock() + + if channels == nil { + return + } + + if ch, exists := channels[id]; exists { + delete(channels, id) + close(ch) + } +} diff --git a/v2/internal/frontend/desktop/darwin/screen.go b/v2/internal/frontend/desktop/darwin/screen.go index a96a8efa7..bd64a31f9 100644 --- a/v2/internal/frontend/desktop/darwin/screen.go +++ b/v2/internal/frontend/desktop/darwin/screen.go @@ -82,6 +82,7 @@ Screen GetNthScreen(int nth, void *inctx){ */ import "C" + import ( "unsafe" diff --git a/v2/internal/frontend/desktop/darwin/single_instance.go b/v2/internal/frontend/desktop/darwin/single_instance.go new file mode 100644 index 000000000..27b34045b --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/single_instance.go @@ -0,0 +1,95 @@ +//go:build darwin +// +build darwin + +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa +#import "AppDelegate.h" + +*/ +import "C" + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/options" +) + +func SetupSingleInstance(uniqueID string) *os.File { + lockFilePath := getTempDir() + lockFileName := uniqueID + ".lock" + file, err := createLockFile(lockFilePath + "/" + lockFileName) + // if lockFile exist – send notification to second instance + if err != nil { + c := NewCalloc() + defer c.Free() + singleInstanceUniqueId := c.String(uniqueID) + + data, err := options.NewSecondInstanceData() + if err != nil { + return nil + } + + serialized, err := json.Marshal(data) + if err != nil { + return nil + } + + C.SendDataToFirstInstance(singleInstanceUniqueId, c.String(string(serialized))) + + os.Exit(0) + } + + return file +} + +//export HandleSecondInstanceData +func HandleSecondInstanceData(secondInstanceMessage *C.char) { + message := C.GoString(secondInstanceMessage) + + var secondInstanceData options.SecondInstanceData + + err := json.Unmarshal([]byte(message), &secondInstanceData) + if err == nil { + secondInstanceBuffer <- secondInstanceData + } +} + +// createLockFile tries to create a file with given name and acquire an +// exclusive lock on it. If the file already exists AND is still locked, it will +// fail. +func createLockFile(filename string) (*os.File, error) { + file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0o600) + if err != nil { + fmt.Printf("Failed to open lockfile %s: %s", filename, err) + return nil, err + } + + err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + if err != nil { + // Flock failed for some other reason than other instance already lock it. Print it in logs for possible debugging. + if !strings.Contains(err.Error(), "resource temporarily unavailable") { + fmt.Printf("Failed to lock lockfile %s: %s", filename, err) + } + file.Close() + return nil, err + } + + return file, nil +} + +// If app is sandboxed, golang os.TempDir() will return path that will not be accessible. So use native macOS temp dir function. +func getTempDir() string { + cstring := C.GetMacOsNativeTempDir() + path := C.GoString(cstring) + C.free(unsafe.Pointer(cstring)) + + return path +} diff --git a/v2/internal/frontend/desktop/darwin/window.go b/v2/internal/frontend/desktop/darwin/window.go index 803d04c1e..87d4213d9 100644 --- a/v2/internal/frontend/desktop/darwin/window.go +++ b/v2/internal/frontend/desktop/darwin/window.go @@ -13,6 +13,7 @@ package darwin #include */ import "C" + import ( "log" "runtime" @@ -31,6 +32,8 @@ func init() { type Window struct { context unsafe.Pointer + + applicationMenu *menu.Menu } func bool2Cint(value bool) C.int { @@ -46,7 +49,6 @@ func bool2CboolPtr(value bool) *C.bool { } func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window { - c := NewCalloc() defer c.Free() @@ -58,9 +60,10 @@ func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window startsHidden := bool2Cint(frontendOptions.StartHidden) devtoolsEnabled := bool2Cint(devtools) defaultContextMenuEnabled := bool2Cint(debug || frontendOptions.EnableDefaultContextMenu) + singleInstanceEnabled := bool2Cint(frontendOptions.SingleInstanceLock != nil) - var fullSizeContent, hideTitleBar, hideTitle, useToolbar, webviewIsTransparent C.int - var titlebarAppearsTransparent, hideToolbarSeparator, windowIsTranslucent C.int + var fullSizeContent, hideTitleBar, zoomable, hideTitle, useToolbar, webviewIsTransparent C.int + var titlebarAppearsTransparent, hideToolbarSeparator, windowIsTranslucent, contentProtection C.int var appearance, title *C.char var preferences C.struct_Preferences @@ -74,8 +77,17 @@ func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window title = c.String(frontendOptions.Title) + singleInstanceUniqueIdStr := "" + if frontendOptions.SingleInstanceLock != nil { + singleInstanceUniqueIdStr = frontendOptions.SingleInstanceLock.UniqueId + } + singleInstanceUniqueId := c.String(singleInstanceUniqueIdStr) + enableFraudulentWebsiteWarnings := C.bool(frontendOptions.EnableFraudulentWebsiteDetection) + enableDragAndDrop := C.bool(frontendOptions.DragAndDrop != nil && frontendOptions.DragAndDrop.EnableFileDrop) + disableWebViewDragAndDrop := C.bool(frontendOptions.DragAndDrop != nil && frontendOptions.DragAndDrop.DisableWebViewDrop) + if frontendOptions.Mac != nil { mac := frontendOptions.Mac if mac.TitleBar != nil { @@ -95,17 +107,26 @@ func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window if mac.Preferences.TextInteractionEnabled.IsSet() { preferences.textInteractionEnabled = bool2CboolPtr(mac.Preferences.TextInteractionEnabled.Get()) } + + if mac.Preferences.FullscreenEnabled.IsSet() { + preferences.fullscreenEnabled = bool2CboolPtr(mac.Preferences.FullscreenEnabled.Get()) + } } + zoomable = bool2Cint(!frontendOptions.Mac.DisableZoom) + windowIsTranslucent = bool2Cint(mac.WindowIsTranslucent) webviewIsTransparent = bool2Cint(mac.WebviewIsTransparent) + contentProtection = bool2Cint(mac.ContentProtection) appearance = c.String(string(mac.Appearance)) } - var context *C.WailsContext = C.Create(title, width, height, frameless, resizable, fullscreen, fullSizeContent, + var context *C.WailsContext = C.Create(title, width, height, frameless, resizable, zoomable, fullscreen, fullSizeContent, hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent, - alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled, - windowStartState, startsHidden, minWidth, minHeight, maxWidth, maxHeight, enableFraudulentWebsiteWarnings, preferences) + alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, contentProtection, devtoolsEnabled, defaultContextMenuEnabled, + windowStartState, startsHidden, minWidth, minHeight, maxWidth, maxHeight, enableFraudulentWebsiteWarnings, + preferences, singleInstanceEnabled, singleInstanceUniqueId, enableDragAndDrop, disableWebViewDragAndDrop, + ) // Create menu result := &Window{ @@ -183,6 +204,7 @@ func (w *Window) SetTitle(title string) { func (w *Window) Maximise() { C.Maximise(w.context) } + func (w *Window) ToggleMaximise() { C.ToggleMaximise(w.context) } @@ -238,6 +260,7 @@ func (w *Window) Show() { func (w *Window) Hide() { C.Hide(w.context) } + func (w *Window) ShowApplication() { C.ShowApplication(w.context) } @@ -272,12 +295,16 @@ func (w *Window) Size() (int, int) { } func (w *Window) SetApplicationMenu(inMenu *menu.Menu) { - mainMenu := NewNSMenu(w.context, "") - processMenu(mainMenu, inMenu) - C.SetAsApplicationMenu(w.context, mainMenu.nsmenu) + w.applicationMenu = inMenu + w.UpdateApplicationMenu() } func (w *Window) UpdateApplicationMenu() { + mainMenu := NewNSMenu(w.context, "") + if w.applicationMenu != nil { + processMenu(mainMenu, w.applicationMenu) + } + C.SetAsApplicationMenu(w.context, mainMenu.nsmenu) C.UpdateApplicationMenu(w.context) } diff --git a/v2/internal/frontend/desktop/linux/browser.go b/v2/internal/frontend/desktop/linux/browser.go index 47bf0ba5d..962e3b28a 100644 --- a/v2/internal/frontend/desktop/linux/browser.go +++ b/v2/internal/frontend/desktop/linux/browser.go @@ -3,10 +3,21 @@ package linux -import "github.com/pkg/browser" +import ( + "fmt" + "github.com/pkg/browser" + "github.com/wailsapp/wails/v2/internal/frontend/utils" +) // BrowserOpenURL Use the default browser to open the url -func (f *Frontend) BrowserOpenURL(url string) { +func (f *Frontend) BrowserOpenURL(rawURL string) { + url, err := utils.ValidateAndSanitizeURL(rawURL) + if err != nil { + f.logger.Error(fmt.Sprintf("Invalid URL %s", err.Error())) + return + } // Specific method implementation - _ = browser.OpenURL(url) + if err := browser.OpenURL(url); err != nil { + f.logger.Error("Unable to open default system browser") + } } diff --git a/v2/internal/frontend/desktop/linux/clipboard.go b/v2/internal/frontend/desktop/linux/clipboard.go index 88b8c713f..a2a46dacc 100644 --- a/v2/internal/frontend/desktop/linux/clipboard.go +++ b/v2/internal/frontend/desktop/linux/clipboard.go @@ -4,7 +4,9 @@ package linux /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 #include "gtk/gtk.h" #include "webkit2/webkit2.h" diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index 70f8468b6..2942a112e 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -4,7 +4,9 @@ package linux /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 #include "gtk/gtk.h" #include "webkit2/webkit2.h" @@ -71,6 +73,16 @@ static void install_signal_handlers() #endif } +static gboolean install_signal_handlers_idle(gpointer data) { + (void)data; + install_signal_handlers(); + return G_SOURCE_REMOVE; +} + +static void fix_signal_handlers_after_gtk_init() { + g_idle_add(install_signal_handlers_idle, NULL); +} + */ import "C" import ( @@ -93,6 +105,7 @@ import ( "github.com/wailsapp/wails/v2/internal/binding" "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/originvalidator" wailsruntime "github.com/wailsapp/wails/v2/internal/frontend/runtime" "github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/pkg/options" @@ -102,6 +115,8 @@ var initOnce = sync.Once{} const startURL = "wails://wails/" +var secondInstanceBuffer = make(chan options.SecondInstanceData, 1) + type Frontend struct { // Context @@ -120,6 +135,8 @@ type Frontend struct { mainWindow *Window bindings *binding.Bindings dispatcher frontend.Dispatcher + + originValidator *originvalidator.OriginValidator } func (f *Frontend) RunMainLoop() { @@ -152,12 +169,15 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. ctx: ctx, } result.startURL, _ = url.Parse(startURL) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil { result.startURL = _starturl + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) } else { if port, _ := ctx.Value("assetserverport").(string); port != "" { result.startURL.Host = net.JoinHostPort(result.startURL.Host+".localhost", port) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) } var bindings string @@ -180,6 +200,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. } go result.startMessageProcessor() + go result.startBindingsMessageProcessor() var _debug = ctx.Value("debug") var _devtoolsEnabled = ctx.Value("devtoolsEnabled") @@ -193,7 +214,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. result.mainWindow = NewWindow(appoptions, result.debug, result.devtoolsEnabled) - C.install_signal_handlers() + C.fix_signal_handlers_after_gtk_init() if appoptions.Linux != nil && appoptions.Linux.ProgramName != "" { prgname := C.CString(appoptions.Linux.ProgramName) @@ -201,6 +222,8 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. C.free(unsafe.Pointer(prgname)) } + go result.startSecondInstanceProcessor() + return result } @@ -210,6 +233,24 @@ func (f *Frontend) startMessageProcessor() { } } +func (f *Frontend) startBindingsMessageProcessor() { + for msg := range bindingsMessageBuffer { + origin, err := f.originValidator.GetOriginFromURL(msg.source) + if err != nil { + f.logger.Error(fmt.Sprintf("failed to get origin for URL %q: %v", msg.source, err)) + continue + } + + allowed := f.originValidator.IsOriginAllowed(origin) + if !allowed { + f.logger.Error("Blocked request from unauthorized origin: %s", origin) + continue + } + + f.processMessage(msg.message) + } +} + func (f *Frontend) WindowReload() { f.ExecJS("runtime.WindowReload();") } @@ -235,6 +276,10 @@ func (f *Frontend) Run(ctx context.Context) error { } }() + if f.frontendOptions.SingleInstanceLock != nil { + SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId) + } + f.mainWindow.Run(f.startURL.String()) return nil @@ -434,12 +479,24 @@ func (f *Frontend) processMessage(message string) { if message == "runtime:ready" { cmd := fmt.Sprintf( "window.wails.setCSSDragProperties('%s', '%s');\n"+ - "window.wails.flags.deferDragToMouseMove = true;", f.frontendOptions.CSSDragProperty, f.frontendOptions.CSSDragValue) + "window.wails.setCSSDropProperties('%s', '%s');\n"+ + "window.wails.flags.deferDragToMouseMove = true;", + f.frontendOptions.CSSDragProperty, + f.frontendOptions.CSSDragValue, + f.frontendOptions.DragAndDrop.CSSDropProperty, + f.frontendOptions.DragAndDrop.CSSDropValue, + ) + f.ExecJS(cmd) if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false { f.ExecJS("window.wails.flags.enableResize = true;") } + + if f.frontendOptions.DragAndDrop.EnableFileDrop { + f.ExecJS("window.wails.flags.enableWailsDragAndDrop = true;") + } + return } @@ -485,7 +542,13 @@ func (f *Frontend) ExecJS(js string) { f.mainWindow.ExecJS(js) } +type bindingsMessage struct { + message string + source string +} + var messageBuffer = make(chan string, 100) +var bindingsMessageBuffer = make(chan *bindingsMessage, 100) //export processMessage func processMessage(message *C.char) { @@ -493,6 +556,16 @@ func processMessage(message *C.char) { messageBuffer <- goMessage } +//export processBindingMessage +func processBindingMessage(message *C.char, source *C.char) { + goMessage := C.GoString(message) + goSource := C.GoString(source) + bindingsMessageBuffer <- &bindingsMessage{ + message: goMessage, + source: goSource, + } +} + var requestBuffer = make(chan webview.Request, 100) func (f *Frontend) startRequestProcessor() { @@ -505,3 +578,12 @@ func (f *Frontend) startRequestProcessor() { func processURLRequest(request unsafe.Pointer) { requestBuffer <- webview.NewRequest(request) } + +func (f *Frontend) startSecondInstanceProcessor() { + for secondInstanceData := range secondInstanceBuffer { + if f.frontendOptions.SingleInstanceLock != nil && + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + } + } +} diff --git a/v2/internal/frontend/desktop/linux/gtk.go b/v2/internal/frontend/desktop/linux/gtk.go index f4bc531b3..67a38c7a0 100644 --- a/v2/internal/frontend/desktop/linux/gtk.go +++ b/v2/internal/frontend/desktop/linux/gtk.go @@ -4,7 +4,9 @@ package linux /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 #include "gtk/gtk.h" diff --git a/v2/internal/frontend/desktop/linux/keys.go b/v2/internal/frontend/desktop/linux/keys.go index 1c095fea9..e5a127dbd 100644 --- a/v2/internal/frontend/desktop/linux/keys.go +++ b/v2/internal/frontend/desktop/linux/keys.go @@ -4,7 +4,10 @@ package linux /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + #include "gtk/gtk.h" diff --git a/v2/internal/frontend/desktop/linux/menu.go b/v2/internal/frontend/desktop/linux/menu.go index bc3d2740b..a61d190bd 100644 --- a/v2/internal/frontend/desktop/linux/menu.go +++ b/v2/internal/frontend/desktop/linux/menu.go @@ -4,7 +4,9 @@ package linux /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 #include "gtk/gtk.h" @@ -32,8 +34,11 @@ void addAccelerator(GtkWidget* menuItem, GtkAccelGroup* group, guint key, GdkMod } */ import "C" -import "github.com/wailsapp/wails/v2/pkg/menu" -import "unsafe" +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/menu" +) var menuIdCounter int var menuItemToId map[*menu.MenuItem]int @@ -79,8 +84,10 @@ func (w *Window) SetApplicationMenu(inmenu *menu.Menu) { func processMenu(window *Window, menu *menu.Menu) { for _, menuItem := range menu.Items { - submenu := processSubmenu(menuItem, window.accels) - C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(window.menubar)), submenu) + if menuItem.SubMenu != nil { + submenu := processSubmenu(menuItem, window.accels) + C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(window.menubar)), submenu) + } } } diff --git a/v2/internal/frontend/desktop/linux/notifications.go b/v2/internal/frontend/desktop/linux/notifications.go new file mode 100644 index 000000000..80f0ae569 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/notifications.go @@ -0,0 +1,594 @@ +//go:build linux +// +build linux + +package linux + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/godbus/dbus/v5" + "github.com/wailsapp/wails/v2/internal/frontend" +) + +var ( + conn *dbus.Conn + categories map[string]frontend.NotificationCategory = make(map[string]frontend.NotificationCategory) + categoriesLock sync.RWMutex + notifications map[uint32]*notificationData = make(map[uint32]*notificationData) + notificationsLock sync.RWMutex + notificationResultCallback func(result frontend.NotificationResult) + callbackLock sync.RWMutex + appName string + cancel context.CancelFunc +) + +type notificationData struct { + ID string + Title string + Subtitle string + Body string + CategoryID string + Data map[string]interface{} + DBusID uint32 + ActionMap map[string]string +} + +const ( + dbusNotificationInterface = "org.freedesktop.Notifications" + dbusNotificationPath = "/org/freedesktop/Notifications" + DefaultActionIdentifier = "DEFAULT_ACTION" +) + +// Creates a new Notifications Service. +func (f *Frontend) InitializeNotifications() error { + // Clean up any previous initialization + f.CleanupNotifications() + + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable: %w", err) + } + appName = filepath.Base(exe) + + _conn, err := dbus.ConnectSessionBus() + if err != nil { + return fmt.Errorf("failed to connect to session bus: %w", err) + } + conn = _conn + + if err := f.loadCategories(); err != nil { + f.logger.Warning("Failed to load notification categories: %v", err) + } + + var signalCtx context.Context + signalCtx, cancel = context.WithCancel(context.Background()) + + if err := f.setupSignalHandling(signalCtx); err != nil { + return fmt.Errorf("failed to set up notification signal handling: %w", err) + } + + return nil +} + +// CleanupNotifications cleans up notification resources +func (f *Frontend) CleanupNotifications() { + if cancel != nil { + cancel() + cancel = nil + } + + if conn != nil { + conn.Close() + conn = nil + } +} + +func (f *Frontend) IsNotificationAvailable() bool { + return true +} + +// RequestNotificationAuthorization is a Linux stub that always returns true, nil. +// (authorization is macOS-specific) +func (f *Frontend) RequestNotificationAuthorization() (bool, error) { + return true, nil +} + +// CheckNotificationAuthorization is a Linux stub that always returns true. +// (authorization is macOS-specific) +func (f *Frontend) CheckNotificationAuthorization() (bool, error) { + return true, nil +} + +// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body. +func (f *Frontend) SendNotification(options frontend.NotificationOptions) error { + if conn == nil { + return fmt.Errorf("notifications not initialized") + } + + hints := map[string]dbus.Variant{} + + body := options.Body + if options.Subtitle != "" { + body = options.Subtitle + "\n" + body + } + + defaultActionID := "default" + actions := []string{defaultActionID, "Default"} + + actionMap := map[string]string{ + defaultActionID: DefaultActionIdentifier, + } + + hints["x-notification-id"] = dbus.MakeVariant(options.ID) + + if options.Data != nil { + userData, err := json.Marshal(options.Data) + if err == nil { + hints["x-user-data"] = dbus.MakeVariant(string(userData)) + } + } + + // Call the Notify method on the D-Bus interface + obj := conn.Object(dbusNotificationInterface, dbusNotificationPath) + call := obj.Call( + dbusNotificationInterface+".Notify", + 0, + appName, + uint32(0), + "", // Icon + options.Title, + body, + actions, + hints, + int32(-1), + ) + + if call.Err != nil { + return fmt.Errorf("failed to send notification: %w", call.Err) + } + + var dbusID uint32 + if err := call.Store(&dbusID); err != nil { + return fmt.Errorf("failed to store notification ID: %w", err) + } + + notification := ¬ificationData{ + ID: options.ID, + Title: options.Title, + Subtitle: options.Subtitle, + Body: options.Body, + Data: options.Data, + DBusID: dbusID, + ActionMap: actionMap, + } + + notificationsLock.Lock() + notifications[dbusID] = notification + notificationsLock.Unlock() + + return nil +} + +// SendNotificationWithActions sends a notification with additional actions. +func (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error { + if conn == nil { + return fmt.Errorf("notifications not initialized") + } + + categoriesLock.RLock() + category, exists := categories[options.CategoryID] + categoriesLock.RUnlock() + + if options.CategoryID == "" || !exists { + // Fall back to basic notification + return f.SendNotification(options) + } + + body := options.Body + if options.Subtitle != "" { + body = options.Subtitle + "\n" + body + } + + var actions []string + actionMap := make(map[string]string) + + defaultActionID := "default" + actions = append(actions, defaultActionID, "Default") + actionMap[defaultActionID] = DefaultActionIdentifier + + for _, action := range category.Actions { + actions = append(actions, action.ID, action.Title) + actionMap[action.ID] = action.ID + } + + hints := map[string]dbus.Variant{} + + hints["x-notification-id"] = dbus.MakeVariant(options.ID) + + hints["x-category-id"] = dbus.MakeVariant(options.CategoryID) + + if options.Data != nil { + userData, err := json.Marshal(options.Data) + if err == nil { + hints["x-user-data"] = dbus.MakeVariant(string(userData)) + } + } + + obj := conn.Object(dbusNotificationInterface, dbusNotificationPath) + call := obj.Call( + dbusNotificationInterface+".Notify", + 0, + appName, + uint32(0), + "", // Icon + options.Title, + body, + actions, + hints, + int32(-1), + ) + + if call.Err != nil { + return fmt.Errorf("failed to send notification: %w", call.Err) + } + + var dbusID uint32 + if err := call.Store(&dbusID); err != nil { + return fmt.Errorf("failed to store notification ID: %w", err) + } + + notification := ¬ificationData{ + ID: options.ID, + Title: options.Title, + Subtitle: options.Subtitle, + Body: options.Body, + CategoryID: options.CategoryID, + Data: options.Data, + DBusID: dbusID, + ActionMap: actionMap, + } + + notificationsLock.Lock() + notifications[dbusID] = notification + notificationsLock.Unlock() + + return nil +} + +// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions. +func (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error { + categoriesLock.Lock() + categories[category.ID] = category + categoriesLock.Unlock() + + if err := f.saveCategories(); err != nil { + f.logger.Warning("Failed to save notification categories: %v", err) + } + + return nil +} + +// RemoveNotificationCategory removes a previously registered NotificationCategory. +func (f *Frontend) RemoveNotificationCategory(categoryId string) error { + categoriesLock.Lock() + delete(categories, categoryId) + categoriesLock.Unlock() + + if err := f.saveCategories(); err != nil { + f.logger.Warning("Failed to save notification categories: %v", err) + } + + return nil +} + +// RemoveAllPendingNotifications attempts to remove all active notifications. +func (f *Frontend) RemoveAllPendingNotifications() error { + notificationsLock.Lock() + dbusIDs := make([]uint32, 0, len(notifications)) + for id := range notifications { + dbusIDs = append(dbusIDs, id) + } + notificationsLock.Unlock() + + for _, id := range dbusIDs { + f.closeNotification(id) + } + + return nil +} + +// RemovePendingNotification removes a pending notification. +func (f *Frontend) RemovePendingNotification(identifier string) error { + var dbusID uint32 + found := false + + notificationsLock.Lock() + for id, notif := range notifications { + if notif.ID == identifier { + dbusID = id + found = true + break + } + } + notificationsLock.Unlock() + + if !found { + return nil + } + + return f.closeNotification(dbusID) +} + +// RemoveAllDeliveredNotifications functionally equivalent to RemoveAllPendingNotification on Linux. +func (f *Frontend) RemoveAllDeliveredNotifications() error { + return f.RemoveAllPendingNotifications() +} + +// RemoveDeliveredNotification functionally equivalent RemovePendingNotification on Linux. +func (f *Frontend) RemoveDeliveredNotification(identifier string) error { + return f.RemovePendingNotification(identifier) +} + +// RemoveNotification removes a notification by identifier. +func (f *Frontend) RemoveNotification(identifier string) error { + return f.RemovePendingNotification(identifier) +} + +func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) { + callbackLock.Lock() + defer callbackLock.Unlock() + + notificationResultCallback = callback +} + +// Helper method to close a notification. +func (f *Frontend) closeNotification(id uint32) error { + if conn == nil { + return fmt.Errorf("notifications not initialized") + } + + obj := conn.Object(dbusNotificationInterface, dbusNotificationPath) + call := obj.Call(dbusNotificationInterface+".CloseNotification", 0, id) + + if call.Err != nil { + return fmt.Errorf("failed to close notification: %w", call.Err) + } + + return nil +} + +func (f *Frontend) getConfigDir() (string, error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", fmt.Errorf("failed to get user config directory: %w", err) + } + + appConfigDir := filepath.Join(configDir, appName) + if err := os.MkdirAll(appConfigDir, 0755); err != nil { + return "", fmt.Errorf("failed to create app config directory: %w", err) + } + + return appConfigDir, nil +} + +// Save notification categories. +func (f *Frontend) saveCategories() error { + configDir, err := f.getConfigDir() + if err != nil { + return err + } + + categoriesFile := filepath.Join(configDir, "notification-categories.json") + + categoriesLock.RLock() + categoriesData, err := json.MarshalIndent(categories, "", " ") + categoriesLock.RUnlock() + + if err != nil { + return fmt.Errorf("failed to marshal notification categories: %w", err) + } + + if err := os.WriteFile(categoriesFile, categoriesData, 0644); err != nil { + return fmt.Errorf("failed to write notification categories to disk: %w", err) + } + + return nil +} + +// Load notification categories. +func (f *Frontend) loadCategories() error { + configDir, err := f.getConfigDir() + if err != nil { + return err + } + + categoriesFile := filepath.Join(configDir, "notification-categories.json") + + if _, err := os.Stat(categoriesFile); os.IsNotExist(err) { + return nil + } + + categoriesData, err := os.ReadFile(categoriesFile) + if err != nil { + return fmt.Errorf("failed to read notification categories from disk: %w", err) + } + + _categories := make(map[string]frontend.NotificationCategory) + if err := json.Unmarshal(categoriesData, &_categories); err != nil { + return fmt.Errorf("failed to unmarshal notification categories: %w", err) + } + + categoriesLock.Lock() + categories = _categories + categoriesLock.Unlock() + + return nil +} + +// Setup signal handling for notification actions. +func (f *Frontend) setupSignalHandling(ctx context.Context) error { + if err := conn.AddMatchSignal( + dbus.WithMatchInterface(dbusNotificationInterface), + dbus.WithMatchMember("ActionInvoked"), + ); err != nil { + return err + } + + if err := conn.AddMatchSignal( + dbus.WithMatchInterface(dbusNotificationInterface), + dbus.WithMatchMember("NotificationClosed"), + ); err != nil { + return err + } + + c := make(chan *dbus.Signal, 10) + conn.Signal(c) + + go f.handleSignals(ctx, c) + + return nil +} + +// Handle incoming D-Bus signals. +func (f *Frontend) handleSignals(ctx context.Context, c chan *dbus.Signal) { + for { + select { + case <-ctx.Done(): + return + case signal, ok := <-c: + if !ok { + return + } + + switch signal.Name { + case dbusNotificationInterface + ".ActionInvoked": + f.handleActionInvoked(signal) + case dbusNotificationInterface + ".NotificationClosed": + f.handleNotificationClosed(signal) + } + } + } +} + +// Handle ActionInvoked signal. +func (f *Frontend) handleActionInvoked(signal *dbus.Signal) { + if len(signal.Body) < 2 { + return + } + + dbusID, ok := signal.Body[0].(uint32) + if !ok { + return + } + + actionID, ok := signal.Body[1].(string) + if !ok { + return + } + + notificationsLock.Lock() + notification, exists := notifications[dbusID] + if exists { + delete(notifications, dbusID) + } + notificationsLock.Unlock() + + if !exists { + return + } + + appActionID, ok := notification.ActionMap[actionID] + if !ok { + appActionID = actionID + } + + response := frontend.NotificationResponse{ + ID: notification.ID, + ActionIdentifier: appActionID, + Title: notification.Title, + Subtitle: notification.Subtitle, + Body: notification.Body, + CategoryID: notification.CategoryID, + UserInfo: notification.Data, + } + + result := frontend.NotificationResult{ + Response: response, + } + + handleNotificationResult(result) +} + +func handleNotificationResult(result frontend.NotificationResult) { + callbackLock.Lock() + callback := notificationResultCallback + callbackLock.Unlock() + + if callback != nil { + go func() { + defer func() { + if r := recover(); r != nil { + // Log panic but don't crash the app + fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r) + } + }() + callback(result) + }() + } +} + +// Handle NotificationClosed signal. +// Reason codes: +// 1 - expired timeout +// 2 - dismissed by user (click on X) +// 3 - closed by CloseNotification call +// 4 - undefined/reserved +func (f *Frontend) handleNotificationClosed(signal *dbus.Signal) { + if len(signal.Body) < 2 { + return + } + + dbusID, ok := signal.Body[0].(uint32) + if !ok { + return + } + + reason, ok := signal.Body[1].(uint32) + if !ok { + reason = 0 // Unknown reason + } + + notificationsLock.Lock() + notification, exists := notifications[dbusID] + if exists { + delete(notifications, dbusID) + } + notificationsLock.Unlock() + + if !exists { + return + } + + if reason == 2 { + response := frontend.NotificationResponse{ + ID: notification.ID, + ActionIdentifier: DefaultActionIdentifier, + Title: notification.Title, + Subtitle: notification.Subtitle, + Body: notification.Body, + CategoryID: notification.CategoryID, + UserInfo: notification.Data, + } + + result := frontend.NotificationResult{ + Response: response, + } + + handleNotificationResult(result) + } +} diff --git a/v2/internal/frontend/desktop/linux/screen.go b/v2/internal/frontend/desktop/linux/screen.go index fdda9b055..0a0507425 100644 --- a/v2/internal/frontend/desktop/linux/screen.go +++ b/v2/internal/frontend/desktop/linux/screen.go @@ -4,7 +4,10 @@ package linux /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 + #cgo CFLAGS: -w #include #include "webkit2/webkit2.h" diff --git a/v2/internal/frontend/desktop/linux/single_instance.go b/v2/internal/frontend/desktop/linux/single_instance.go new file mode 100644 index 000000000..0317dee49 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/single_instance.go @@ -0,0 +1,77 @@ +//go:build linux +// +build linux + +package linux + +import ( + "encoding/json" + "github.com/godbus/dbus/v5" + "github.com/wailsapp/wails/v2/pkg/options" + "log" + "os" + "strings" +) + +type dbusHandler func(string) + +func (f dbusHandler) SendMessage(message string) *dbus.Error { + f(message) + return nil +} + +func SetupSingleInstance(uniqueID string) { + id := "wails_app_" + strings.ReplaceAll(strings.ReplaceAll(uniqueID, "-", "_"), ".", "_") + + dbusName := "org." + id + ".SingleInstance" + dbusPath := "/org/" + id + "/SingleInstance" + + conn, err := dbus.ConnectSessionBus() + // if we will reach any error during establishing connection or sending message we will just continue. + // It should not be the case that such thing will happen actually, but just in case. + if err != nil { + return + } + + f := dbusHandler(func(message string) { + var secondInstanceData options.SecondInstanceData + + err := json.Unmarshal([]byte(message), &secondInstanceData) + if err == nil { + secondInstanceBuffer <- secondInstanceData + } + }) + + err = conn.Export(f, dbus.ObjectPath(dbusPath), dbusName) + if err != nil { + return + } + + reply, err := conn.RequestName(dbusName, dbus.NameFlagDoNotQueue) + if err != nil { + return + } + + // if name already taken, try to send args to existing instance, if no success just launch new instance + if reply == dbus.RequestNameReplyExists { + data := options.SecondInstanceData{ + Args: os.Args[1:], + } + data.WorkingDirectory, err = os.Getwd() + if err != nil { + log.Printf("Failed to get working directory: %v", err) + return + } + + serialized, err := json.Marshal(data) + if err != nil { + log.Printf("Failed to marshal data: %v", err) + return + } + + err = conn.Object(dbusName, dbus.ObjectPath(dbusPath)).Call(dbusName+".SendMessage", 0, string(serialized)).Store() + if err != nil { + return + } + os.Exit(1) + } +} diff --git a/v2/internal/frontend/desktop/linux/webkit2.go b/v2/internal/frontend/desktop/linux/webkit2.go index 843b72604..06e0c7824 100644 --- a/v2/internal/frontend/desktop/linux/webkit2.go +++ b/v2/internal/frontend/desktop/linux/webkit2.go @@ -3,7 +3,8 @@ package linux /* -#cgo linux pkg-config: webkit2gtk-4.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 #include "webkit2/webkit2.h" */ import "C" diff --git a/v2/internal/frontend/desktop/linux/window.c b/v2/internal/frontend/desktop/linux/window.c index 7cd1c249b..5441db022 100644 --- a/v2/internal/frontend/desktop/linux/window.c +++ b/v2/internal/frontend/desktop/linux/window.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "window.h" @@ -13,6 +14,9 @@ static float xroot = 0.0f; static float yroot = 0.0f; static int dragTime = -1; static uint mouseButton = 0; +static int wmIsWayland = -1; +static int decoratorWidth = -1; +static int decoratorHeight = -1; // casts void ExecuteOnMainThread(void *f, gpointer jscallback) @@ -41,11 +45,17 @@ GtkBox *GTKBOX(void *pointer) } extern void processMessage(char *); +extern void processBindingMessage(char *, char *); static void sendMessageToBackend(WebKitUserContentManager *contentManager, WebKitJavascriptResult *result, void *data) { + // Retrieve webview from content manager + WebKitWebView *webview = WEBKIT_WEB_VIEW(g_object_get_data(G_OBJECT(contentManager), "webview")); + const char *current_uri = webview ? webkit_web_view_get_uri(webview) : NULL; + char *uri = current_uri ? g_strdup(current_uri) : NULL; + #if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22 JSCValue *value = webkit_javascript_result_get_js_value(result); char *message = jsc_value_to_string(value); @@ -58,8 +68,11 @@ static void sendMessageToBackend(WebKitUserContentManager *contentManager, JSStringGetUTF8CString(js, message, messageSize); JSStringRelease(js); #endif - processMessage(message); + processBindingMessage(message, uri); g_free(message); + if (uri) { + g_free(uri); + } } static bool isNULLRectangle(GdkRectangle input) @@ -67,6 +80,29 @@ static bool isNULLRectangle(GdkRectangle input) return input.x == -1 && input.y == -1 && input.width == -1 && input.height == -1; } +static gboolean onWayland() +{ + switch (wmIsWayland) + { + case -1: + { + char *gdkBackend = getenv("XDG_SESSION_TYPE"); + if(gdkBackend != NULL && strcmp(gdkBackend, "wayland") == 0) + { + wmIsWayland = 1; + return TRUE; + } + + wmIsWayland = 0; + return FALSE; + } + case 1: + return TRUE; + default: + return FALSE; + } +} + static GdkMonitor *getCurrentMonitor(GtkWindow *window) { // Get the monitor that the window is currently on @@ -237,15 +273,38 @@ void SetMinMaxSize(GtkWindow *window, int min_width, int min_height, int max_wid { return; } + int flags = GDK_HINT_MAX_SIZE | GDK_HINT_MIN_SIZE; + size.max_height = (max_height == 0 ? monitorSize.height : max_height); size.max_width = (max_width == 0 ? monitorSize.width : max_width); size.min_height = min_height; size.min_width = min_width; + + // On Wayland window manager get the decorators and calculate the differences from the windows' size. + if(onWayland()) + { + if(decoratorWidth == -1 && decoratorHeight == -1) + { + int windowWidth, windowHeight; + gtk_window_get_size(window, &windowWidth, &windowHeight); + + GtkAllocation windowAllocation; + gtk_widget_get_allocation(GTK_WIDGET(window), &windowAllocation); + + decoratorWidth = (windowAllocation.width-windowWidth); + decoratorHeight = (windowAllocation.height-windowHeight); + } + + // Add the decorator difference to the window so fullscreen and maximise can fill the window. + size.max_height = decoratorHeight+size.max_height; + size.max_width = decoratorWidth+size.max_width; + } + gtk_window_set_geometry_hints(window, NULL, &size, flags); } -// function to disable the context menu but propogate the event +// function to disable the context menu but propagate the event static gboolean disableContextMenu(GtkWidget *widget, WebKitContextMenu *context_menu, GdkEvent *event, WebKitHitTestResult *hit_test_result, gpointer data) { // return true to disable the context menu @@ -254,7 +313,7 @@ static gboolean disableContextMenu(GtkWidget *widget, WebKitContextMenu *context void DisableContextMenu(void *webview) { - // Disable the context menu but propogate the event + // Disable the context menu but propagate the event g_signal_connect(WEBKIT_WEB_VIEW(webview), "context-menu", G_CALLBACK(disableContextMenu), NULL); } @@ -429,14 +488,95 @@ gboolean close_button_pressed(GtkWidget *widget, GdkEvent *event, void *data) return TRUE; } +char *droppedFiles = NULL; + +static void onDragDataReceived(GtkWidget *self, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint target_type, guint time, gpointer data) +{ + if(selection_data == NULL || (gtk_selection_data_get_length(selection_data) <= 0) || target_type != 2) + { + return; + } + + if(droppedFiles != NULL) { + free(droppedFiles); + droppedFiles = NULL; + } + + gchar **filenames = NULL; + filenames = g_uri_list_extract_uris((const gchar *)gtk_selection_data_get_data(selection_data)); + if (filenames == NULL) // If unable to retrieve filenames: + { + g_strfreev(filenames); + return; + } + + droppedFiles = calloc((size_t)gtk_selection_data_get_length(selection_data), 1); + + int iter = 0; + while(filenames[iter] != NULL) // The last URI list element is NULL. + { + if(iter != 0) + { + strncat(droppedFiles, "\n", 1); + } + char *filename = g_filename_from_uri(filenames[iter], NULL, NULL); + if (filename == NULL) + { + break; + } + strncat(droppedFiles, filename, strlen(filename)); + + free(filename); + iter++; + } + + g_strfreev(filenames); +} + +static gboolean onDragDrop(GtkWidget* self, GdkDragContext* context, gint x, gint y, guint time, gpointer user_data) +{ + if(droppedFiles == NULL) + { + return FALSE; + } + + size_t resLen = strlen(droppedFiles)+(sizeof(gint)*2)+6; + char *res = calloc(resLen, 1); + + snprintf(res, resLen, "DD:%d:%d:%s", x, y, droppedFiles); + + if(droppedFiles != NULL) { + free(droppedFiles); + droppedFiles = NULL; + } + + processMessage(res); + return FALSE; +} + // WebView -GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy) +GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy, int disableWebViewDragAndDrop, int enableDragAndDrop) { GtkWidget *webview = webkit_web_view_new_with_user_content_manager((WebKitUserContentManager *)contentManager); + + // Store webview reference in the content manager + g_object_set_data(G_OBJECT((WebKitUserContentManager *)contentManager), "webview", webview); // gtk_container_add(GTK_CONTAINER(window), webview); WebKitWebContext *context = webkit_web_context_get_default(); webkit_web_context_register_uri_scheme(context, "wails", (WebKitURISchemeRequestCallback)processURLRequest, NULL, NULL); g_signal_connect(G_OBJECT(webview), "load-changed", G_CALLBACK(webviewLoadChanged), NULL); + + if(disableWebViewDragAndDrop) + { + gtk_drag_dest_unset(webview); + } + + if(enableDragAndDrop) + { + g_signal_connect(G_OBJECT(webview), "drag-data-received", G_CALLBACK(onDragDataReceived), NULL); + g_signal_connect(G_OBJECT(webview), "drag-drop", G_CALLBACK(onDragDrop), NULL); + } + if (hideWindowOnClose) { g_signal_connect(GTK_WIDGET(window), "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); @@ -748,4 +888,4 @@ void InstallF12Hotkey(void *window) gtk_window_add_accel_group(GTK_WINDOW(window), accel_group); GClosure *closure = g_cclosure_new(G_CALLBACK(sendShowInspectorMessage), window, NULL); gtk_accel_group_connect(accel_group, GDK_KEY_F12, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_ACCEL_VISIBLE, closure); -} \ No newline at end of file +} diff --git a/v2/internal/frontend/desktop/linux/window.go b/v2/internal/frontend/desktop/linux/window.go index 82030f439..0bf5ac51d 100644 --- a/v2/internal/frontend/desktop/linux/window.go +++ b/v2/internal/frontend/desktop/linux/window.go @@ -4,7 +4,9 @@ package linux /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 #include #include @@ -25,6 +27,7 @@ import ( "github.com/wailsapp/wails/v2/internal/frontend" "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/linux" ) func gtkBool(input bool) C.gboolean { @@ -90,6 +93,9 @@ func NewWindow(appoptions *options.App, debug bool, devtoolsEnabled bool) *Windo var webviewGpuPolicy int if appoptions.Linux != nil { webviewGpuPolicy = int(appoptions.Linux.WebviewGpuPolicy) + } else { + // workaround for https://github.com/wailsapp/wails/issues/2977 + webviewGpuPolicy = int(linux.WebviewGpuPolicyNever) } webview := C.SetupWebview( @@ -97,6 +103,8 @@ func NewWindow(appoptions *options.App, debug bool, devtoolsEnabled bool) *Windo result.asGTKWindow(), bool2Cint(appoptions.HideWindowOnClose), C.int(webviewGpuPolicy), + bool2Cint(appoptions.DragAndDrop != nil && appoptions.DragAndDrop.DisableWebViewDrop), + bool2Cint(appoptions.DragAndDrop != nil && appoptions.DragAndDrop.EnableFileDrop), ) result.webview = unsafe.Pointer(webview) buttonPressedName := C.CString("button-press-event") diff --git a/v2/internal/frontend/desktop/linux/window.h b/v2/internal/frontend/desktop/linux/window.h index aa9499d73..04410959a 100644 --- a/v2/internal/frontend/desktop/linux/window.h +++ b/v2/internal/frontend/desktop/linux/window.h @@ -106,7 +106,7 @@ gboolean Fullscreen(gpointer data); gboolean UnFullscreen(gpointer data); // WebView -GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy); +GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy, int disableWebViewDragAndDrop, int enableDragAndDrop); void LoadIndex(void *webview, char *url); void DevtoolsEnabled(void *webview, int enabled, bool showInspector); void ExecuteJS(void *data); diff --git a/v2/internal/frontend/desktop/windows/browser.go b/v2/internal/frontend/desktop/windows/browser.go index f23b04dbe..13d037b14 100644 --- a/v2/internal/frontend/desktop/windows/browser.go +++ b/v2/internal/frontend/desktop/windows/browser.go @@ -4,11 +4,40 @@ package windows import ( + "fmt" "github.com/pkg/browser" + "github.com/wailsapp/wails/v2/internal/frontend/utils" + "golang.org/x/sys/windows" ) -// BrowserOpenURL Use the default browser to open the url -func (f *Frontend) BrowserOpenURL(url string) { - // Specific method implementation - _ = browser.OpenURL(url) +var fallbackBrowserPaths = []string{ + `\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`, + `\Program Files\Google\Chrome\Application\chrome.exe`, + `\Program Files (x86)\Google\Chrome\Application\chrome.exe`, + `\Program Files\Mozilla Firefox\firefox.exe`, +} + +// BrowserOpenURL Use the default browser to open the url +func (f *Frontend) BrowserOpenURL(rawURL string) { + url, err := utils.ValidateAndSanitizeURL(rawURL) + if err != nil { + f.logger.Error(fmt.Sprintf("Invalid URL %s", err.Error())) + return + } + + // Specific method implementation + err = browser.OpenURL(url) + if err == nil { + return + } + for _, fallback := range fallbackBrowserPaths { + if err := openBrowser(fallback, url); err == nil { + return + } + } + f.logger.Error("Unable to open default system browser") +} + +func openBrowser(path, url string) error { + return windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(path), windows.StringToUTF16Ptr(url), nil, windows.SW_SHOWNORMAL) } diff --git a/v2/internal/frontend/desktop/windows/dialog.go b/v2/internal/frontend/desktop/windows/dialog.go index 1ca422b71..573325886 100644 --- a/v2/internal/frontend/desktop/windows/dialog.go +++ b/v2/internal/frontend/desktop/windows/dialog.go @@ -47,7 +47,7 @@ func (f *Frontend) OpenDirectoryDialog(options frontend.OpenDialogOptions) (stri return cfd.NewSelectFolderDialog(config) }, false) - if err != nil && err != cfd.ErrorCancelled { + if err != nil && err != cfd.ErrCancelled { return "", err } return result.(string), nil @@ -72,7 +72,7 @@ func (f *Frontend) OpenFileDialog(options frontend.OpenDialogOptions) (string, e return cfd.NewOpenFileDialog(config) }, false) - if err != nil && err != cfd.ErrorCancelled { + if err != nil && err != cfd.ErrCancelled { return "", err } return result.(string), nil @@ -99,7 +99,7 @@ func (f *Frontend) OpenMultipleFilesDialog(options frontend.OpenDialogOptions) ( return cfd.NewOpenMultipleFilesDialog(config) }, true) - if err != nil && err != cfd.ErrorCancelled { + if err != nil && err != cfd.ErrCancelled { return nil, err } return result.([]string), nil @@ -121,12 +121,16 @@ func (f *Frontend) SaveFileDialog(options frontend.SaveDialogOptions) (string, e Folder: defaultFolder, } + if len(options.Filters) > 0 { + config.DefaultExtension = strings.TrimPrefix(strings.Split(options.Filters[0].Pattern, ";")[0], "*") + } + result, err := f.showCfdDialog( func() (cfd.Dialog, error) { return cfd.NewSaveFileDialog(config) }, false) - if err != nil && err != cfd.ErrorCancelled { + if err != nil && err != cfd.ErrCancelled { return "", err } return result.(string), nil diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index a753bbd30..5df13ed98 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -16,6 +16,7 @@ import ( "sync" "text/template" "time" + "unsafe" "github.com/bep/debounce" "github.com/wailsapp/go-webview2/pkg/edge" @@ -24,17 +25,22 @@ import ( "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32" "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + "github.com/wailsapp/wails/v2/internal/frontend/originvalidator" wailsruntime "github.com/wailsapp/wails/v2/internal/frontend/runtime" "github.com/wailsapp/wails/v2/internal/logger" + w32consts "github.com/wailsapp/wails/v2/internal/platform/win32" "github.com/wailsapp/wails/v2/internal/system/operatingsystem" "github.com/wailsapp/wails/v2/pkg/assetserver" "github.com/wailsapp/wails/v2/pkg/assetserver/webview" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/windows" + w "golang.org/x/sys/windows" ) const startURL = "http://wails.localhost/" +var secondInstanceBuffer = make(chan options.SecondInstanceData, 1) + type Screen = frontend.Screen type Frontend struct { @@ -59,6 +65,8 @@ type Frontend struct { hasStarted bool + originValidator *originvalidator.OriginValidator + // Windows build number versionInfo *operatingsystem.WindowsVersionInfo resizeDebouncer func(f func()) @@ -69,6 +77,13 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. // Get Windows build number versionInfo, _ := operatingsystem.GetWindowsVersionInfo() + // Apply DLL search path settings if specified + if appoptions.Windows != nil && appoptions.Windows.DLLSearchPaths != 0 { + w.SetDefaultDllDirectories(appoptions.Windows.DLLSearchPaths) + } + // Now initialize packages that load DLLs + w32.Init() + w32consts.Init() result := &Frontend{ frontendOptions: appoptions, logger: myLogger, @@ -86,14 +101,17 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. // We currently can't use wails://wails/ as other platforms do, therefore we map the assets sever onto the following url. result.startURL, _ = url.Parse(startURL) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil { result.startURL = _starturl + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) return result } if port, _ := ctx.Value("assetserverport").(string); port != "" { result.startURL.Host = net.JoinHostPort(result.startURL.Host, port) + result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins) } var bindings string @@ -113,6 +131,8 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. } result.assets = assets + go result.startSecondInstanceProcessor() + return result } @@ -137,6 +157,10 @@ func (f *Frontend) Run(ctx context.Context) error { f.chromium = edge.NewChromium() + if f.frontendOptions.SingleInstanceLock != nil { + SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId) + } + mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo, f.chromium) f.mainWindow = mainWindow @@ -161,10 +185,21 @@ func (f *Frontend) Run(ctx context.Context) error { // depends on the content in the WebView, see https://github.com/wailsapp/wails/issues/1319 event, _ := arg.Data.(*winc.SizeEventData) if event != nil && event.Type == w32.SIZE_MINIMIZED { + // Set minimizing flag to prevent unnecessary redraws during minimize/restore for frameless windows + // 设置最小化标志以防止无边框窗口在最小化/恢复过程中的不必要重绘 + // This fixes window flickering when minimizing/restoring frameless windows + // 这修复了无边框窗口在最小化/恢复时的闪烁问题 + // Reference: https://github.com/wailsapp/wails/issues/3951 + f.mainWindow.isMinimizing = true return } } + // Clear minimizing flag for all non-minimize size events + // 对于所有非最小化的尺寸变化事件,清除最小化标志 + // Reference: https://github.com/wailsapp/wails/issues/3951 + f.mainWindow.isMinimizing = false + if f.resizeDebouncer != nil { f.resizeDebouncer(func() { f.mainWindow.Invoke(func() { @@ -211,6 +246,7 @@ func (f *Frontend) WindowCenter() { func (f *Frontend) WindowSetAlwaysOnTop(b bool) { runtime.LockOSThread() + defer runtime.UnlockOSThread() f.mainWindow.SetAlwaysOnTop(b) } @@ -452,7 +488,14 @@ func (f *Frontend) setupChromium() { chromium.AdditionalBrowserArgs = append(chromium.AdditionalBrowserArgs, arg) } + if f.frontendOptions.DragAndDrop != nil && f.frontendOptions.DragAndDrop.DisableWebViewDrop { + if err := chromium.AllowExternalDrag(false); err != nil { + f.logger.Warning("WebView failed to set AllowExternalDrag to false!") + } + } + chromium.MessageCallback = f.processMessage + chromium.MessageWithAdditionalObjectsCallback = f.processMessageWithAdditionalObjects chromium.WebResourceRequestedCallback = f.processRequest chromium.NavigationCompletedCallback = f.navigationCompleted chromium.AcceleratorKeyCallback = func(vkey uint) bool { @@ -534,6 +577,10 @@ func (f *Frontend) setupChromium() { if err != nil { log.Fatal(err) } + err = settings.PutIsPinchZoomEnabled(!opts.DisablePinchZoom) + if err != nil { + log.Fatal(err) + } } err = settings.PutIsStatusBarEnabled(false) @@ -648,7 +695,24 @@ var edgeMap = map[string]uintptr{ "nw-resize": w32.HTTOPLEFT, } -func (f *Frontend) processMessage(message string) { +func (f *Frontend) processMessage(message string, sender *edge.ICoreWebView2, args *edge.ICoreWebView2WebMessageReceivedEventArgs) { + topSource, err := sender.GetSource() + if err != nil { + f.logger.Error(fmt.Sprintf("Unable to get source from sender: %s", err.Error())) + return + } + + senderSource, err := args.GetSource() + if err != nil { + f.logger.Error(fmt.Sprintf("Unable to get source from args: %s", err.Error())) + return + } + + // verify both topSource and sender are allowed origins + if !f.validBindingOrigin(topSource) || !f.validBindingOrigin(senderSource) { + return + } + if message == "drag" { if !f.mainWindow.IsFullScreen() { err := f.startDrag() @@ -660,7 +724,15 @@ func (f *Frontend) processMessage(message string) { } if message == "runtime:ready" { - cmd := fmt.Sprintf("window.wails.setCSSDragProperties('%s', '%s');", f.frontendOptions.CSSDragProperty, f.frontendOptions.CSSDragValue) + cmd := fmt.Sprintf( + "window.wails.setCSSDragProperties('%s', '%s');\n"+ + "window.wails.setCSSDropProperties('%s', '%s');", + f.frontendOptions.CSSDragProperty, + f.frontendOptions.CSSDragValue, + f.frontendOptions.DragAndDrop.CSSDropProperty, + f.frontendOptions.DragAndDrop.CSSDropValue, + ) + f.ExecJS(cmd) return } @@ -681,25 +753,117 @@ func (f *Frontend) processMessage(message string) { return } - go func() { - result, err := f.dispatcher.ProcessMessage(message, f) - if err != nil { - f.logger.Error(err.Error()) - f.Callback(result) + go f.dispatchMessage(message) +} + +func (f *Frontend) processMessageWithAdditionalObjects(message string, sender *edge.ICoreWebView2, args *edge.ICoreWebView2WebMessageReceivedEventArgs) { + topSource, err := sender.GetSource() + if err != nil { + f.logger.Error(fmt.Sprintf("Unable to get source from sender: %s", err.Error())) + return + } + + senderSource, err := args.GetSource() + if err != nil { + f.logger.Error(fmt.Sprintf("Unable to get source from args: %s", err.Error())) + return + } + + // verify both topSource and sender are allowed origins + if !f.validBindingOrigin(topSource) || !f.validBindingOrigin(senderSource) { + return + } + + if strings.HasPrefix(message, "file:drop") { + if !f.frontendOptions.DragAndDrop.EnableFileDrop { return } - if result == "" { + objs, err := args.GetAdditionalObjects() + if err != nil { + f.logger.Error(err.Error()) return } - switch result[0] { - case 'c': - // Callback from a method call - f.Callback(result[1:]) - default: - f.logger.Info("Unknown message returned from dispatcher: %+v", result) + defer objs.Release() + + count, err := objs.GetCount() + if err != nil { + f.logger.Error(err.Error()) + return } - }() + + files := make([]string, count) + for i := uint32(0); i < count; i++ { + _file, err := objs.GetValueAtIndex(i) + if err != nil { + f.logger.Error("cannot get value at %d : %s", i, err.Error()) + return + } + + if _file == nil { + f.logger.Warning("object at %d is not a file", i) + continue + } + + file := (*edge.ICoreWebView2File)(unsafe.Pointer(_file)) + defer file.Release() + + filepath, err := file.GetPath() + if err != nil { + f.logger.Error("cannot get path for object at %d : %s", i, err.Error()) + return + } + + files[i] = filepath + } + + var ( + x = "0" + y = "0" + ) + coords := strings.SplitN(message[10:], ":", 2) + if len(coords) == 2 { + x = coords[0] + y = coords[1] + } + + go f.dispatchMessage(fmt.Sprintf("DD:%s:%s:%s", x, y, strings.Join(files, "\n"))) + return + } +} + +func (f *Frontend) validBindingOrigin(source string) bool { + origin, err := f.originValidator.GetOriginFromURL(source) + if err != nil { + f.logger.Error(fmt.Sprintf("Error parsing source URL %s: %v", source, err.Error())) + return false + } + allowed := f.originValidator.IsOriginAllowed(origin) + if !allowed { + f.logger.Error("Blocked request from unauthorized origin: %s", origin) + return false + } + return true +} + +func (f *Frontend) dispatchMessage(message string) { + result, err := f.dispatcher.ProcessMessage(message, f) + if err != nil { + f.logger.Error(err.Error()) + f.Callback(result) + return + } + if result == "" { + return + } + + switch result[0] { + case 'c': + // Callback from a method call + f.Callback(result[1:]) + default: + f.logger.Info("Unknown message returned from dispatcher: %+v", result) + } } func (f *Frontend) Callback(message string) { @@ -745,6 +909,10 @@ func (f *Frontend) navigationCompleted(sender *edge.ICoreWebView2, args *edge.IC f.ExecJS("window.wails.flags.enableResize = true;") } + if f.frontendOptions.DragAndDrop != nil && f.frontendOptions.DragAndDrop.EnableFileDrop { + f.ExecJS("window.wails.flags.enableWailsDragAndDrop = true;") + } + if f.hasStarted { return } @@ -825,3 +993,12 @@ func (f *Frontend) ShowWindow() { func (f *Frontend) onFocus(arg *winc.Event) { f.chromium.Focus() } + +func (f *Frontend) startSecondInstanceProcessor() { + for secondInstanceData := range secondInstanceBuffer { + if f.frontendOptions.SingleInstanceLock != nil && + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil { + f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData) + } + } +} diff --git a/v2/internal/frontend/desktop/windows/notifications.go b/v2/internal/frontend/desktop/windows/notifications.go new file mode 100644 index 000000000..0176b7077 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/notifications.go @@ -0,0 +1,489 @@ +//go:build windows +// +build windows + +package windows + +import ( + "encoding/base64" + "encoding/json" + "log" + "sync" + + wintoast "git.sr.ht/~jackmordaunt/go-toast/v2/wintoast" + "github.com/google/uuid" + "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + + "fmt" + "os" + "path/filepath" + _ "unsafe" // for go:linkname + + "git.sr.ht/~jackmordaunt/go-toast/v2" + "golang.org/x/sys/windows/registry" +) + +var ( + categories map[string]frontend.NotificationCategory + categoriesLock sync.RWMutex + appName string + appGUID string + iconPath string = "" + exePath string + iconOnce sync.Once + iconErr error + + notificationResultCallback func(result frontend.NotificationResult) + callbackLock sync.RWMutex +) + +const DefaultActionIdentifier = "DEFAULT_ACTION" + +const ( + ToastRegistryPath = `Software\Classes\AppUserModelId\` + ToastRegistryGuidKey = "CustomActivator" + NotificationCategoriesRegistryPath = `SOFTWARE\%s\NotificationCategories` + NotificationCategoriesRegistryKey = "Categories" +) + +// NotificationPayload combines the action ID and user data into a single structure +type NotificationPayload struct { + Action string `json:"action"` + Options frontend.NotificationOptions `json:"payload,omitempty"` +} + +func (f *Frontend) InitializeNotifications() error { + categoriesLock.Lock() + defer categoriesLock.Unlock() + categories = make(map[string]frontend.NotificationCategory) + + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable: %w", err) + } + exePath = exe + appName = filepath.Base(exePath) + + appGUID, err = getGUID() + if err != nil { + return err + } + + iconPath = filepath.Join(os.TempDir(), appName+appGUID+".png") + + // Create the registry key for the toast activator + key, _, err := registry.CreateKey(registry.CURRENT_USER, + `Software\Classes\CLSID\`+appGUID+`\LocalServer32`, registry.ALL_ACCESS) + if err != nil { + return fmt.Errorf("failed to create CLSID key: %w", err) + } + defer key.Close() + + if err := key.SetStringValue("", fmt.Sprintf("\"%s\" %%1", exePath)); err != nil { + return fmt.Errorf("failed to set CLSID server path: %w", err) + } + + toast.SetAppData(toast.AppData{ + AppID: appName, + GUID: appGUID, + IconPath: iconPath, + ActivationExe: exePath, + }) + + toast.SetActivationCallback(func(args string, data []toast.UserData) { + result := frontend.NotificationResult{} + + actionIdentifier, options, err := parseNotificationResponse(args) + + if err != nil { + result.Error = err + } else { + // Subtitle is retained but was not shown with the notification + response := frontend.NotificationResponse{ + ID: options.ID, + ActionIdentifier: actionIdentifier, + Title: options.Title, + Subtitle: options.Subtitle, + Body: options.Body, + CategoryID: options.CategoryID, + UserInfo: options.Data, + } + + if userText, found := getUserText(data); found { + response.UserText = userText + } + + result.Response = response + } + + handleNotificationResult(result) + }) + + // Register the COM class factory for toast activation. + // This is required for Windows to activate the app when users interact with notifications. + // The go-toast library's SetAppData and SetActivationCallback handle the callback setup, + // but the COM class factory registration is not exposed via public APIs, so we use + // go:linkname to access the internal registerClassFactory function. + if err := registerToastClassFactory(wintoast.ClassFactory); err != nil { + return fmt.Errorf("CoRegisterClassObject failed: %w", err) + } + + return loadCategoriesFromRegistry() +} + +// registerToastClassFactory registers the COM class factory required for Windows toast notification activation. +// This function uses go:linkname to access the unexported registerClassFactory function from go-toast. +// The class factory is necessary for Windows COM activation when users click notification actions. +// Without this registration, notification actions will not activate the application. +// +// This is a workaround until go-toast exports this functionality via a public API. +// See: https://git.sr.ht/~jackmordaunt/go-toast +// +//go:linkname registerToastClassFactory git.sr.ht/~jackmordaunt/go-toast/v2/wintoast.registerClassFactory +func registerToastClassFactory(factory *wintoast.IClassFactory) error + +// CleanupNotifications is a Windows stub that does nothing. +// (Linux-specific cleanup) +func (f *Frontend) CleanupNotifications() { + // No cleanup needed on Windows +} + +func (f *Frontend) IsNotificationAvailable() bool { + return true +} + +func (f *Frontend) RequestNotificationAuthorization() (bool, error) { + return true, nil +} + +func (f *Frontend) CheckNotificationAuthorization() (bool, error) { + return true, nil +} + +// SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows. +// (subtitle is only available on macOS and Linux) +func (f *Frontend) SendNotification(options frontend.NotificationOptions) error { + if err := f.saveIconToDir(); err != nil { + f.logger.Warning("Error saving icon: %v", err) + } + + n := toast.Notification{ + Title: options.Title, + Body: options.Body, + ActivationType: toast.Foreground, + ActivationArguments: DefaultActionIdentifier, + } + + encodedPayload, err := encodePayload(DefaultActionIdentifier, options) + if err != nil { + return fmt.Errorf("failed to encode notification payload: %w", err) + } + n.ActivationArguments = encodedPayload + + return n.Push() +} + +// SendNotificationWithActions sends a notification with additional actions and inputs. +// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category. +// If a NotificationCategory is not registered a basic notification will be sent. +// (subtitle is only available on macOS and Linux) +func (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error { + if err := f.saveIconToDir(); err != nil { + f.logger.Warning("Error saving icon: %v", err) + } + + categoriesLock.RLock() + nCategory, categoryExists := categories[options.CategoryID] + categoriesLock.RUnlock() + + if options.CategoryID == "" || !categoryExists { + f.logger.Warning("Category '%s' not found, sending basic notification without actions", options.CategoryID) + return f.SendNotification(options) + } + + n := toast.Notification{ + Title: options.Title, + Body: options.Body, + ActivationType: toast.Foreground, + ActivationArguments: DefaultActionIdentifier, + } + + for _, action := range nCategory.Actions { + n.Actions = append(n.Actions, toast.Action{ + Content: action.Title, + Arguments: action.ID, + }) + } + + if nCategory.HasReplyField { + n.Inputs = append(n.Inputs, toast.Input{ + ID: "userText", + Placeholder: nCategory.ReplyPlaceholder, + }) + + n.Actions = append(n.Actions, toast.Action{ + Content: nCategory.ReplyButtonTitle, + Arguments: "TEXT_REPLY", + InputID: "userText", + }) + } + + encodedPayload, err := encodePayload(n.ActivationArguments, options) + if err != nil { + return fmt.Errorf("failed to encode notification payload: %w", err) + } + n.ActivationArguments = encodedPayload + + for index := range n.Actions { + encodedPayload, err := encodePayload(n.Actions[index].Arguments, options) + if err != nil { + return fmt.Errorf("failed to encode notification payload: %w", err) + } + n.Actions[index].Arguments = encodedPayload + } + + return n.Push() +} + +// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions. +// Registering a category with the same name as a previously registered NotificationCategory will override it. +func (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error { + categoriesLock.Lock() + defer categoriesLock.Unlock() + + categories[category.ID] = frontend.NotificationCategory{ + ID: category.ID, + Actions: category.Actions, + HasReplyField: category.HasReplyField, + ReplyPlaceholder: category.ReplyPlaceholder, + ReplyButtonTitle: category.ReplyButtonTitle, + } + + return saveCategoriesToRegistry() +} + +// RemoveNotificationCategory removes a previously registered NotificationCategory. +func (f *Frontend) RemoveNotificationCategory(categoryId string) error { + categoriesLock.Lock() + defer categoriesLock.Unlock() + + delete(categories, categoryId) + + return saveCategoriesToRegistry() +} + +// RemoveAllPendingNotifications is a Windows stub that always returns nil. +// (macOS and Linux only) +func (f *Frontend) RemoveAllPendingNotifications() error { + return nil +} + +// RemovePendingNotification is a Windows stub that always returns nil. +// (macOS and Linux only) +func (f *Frontend) RemovePendingNotification(_ string) error { + return nil +} + +// RemoveAllDeliveredNotifications is a Windows stub that always returns nil. +// (macOS and Linux only) +func (f *Frontend) RemoveAllDeliveredNotifications() error { + return nil +} + +// RemoveDeliveredNotification is a Windows stub that always returns nil. +// (macOS and Linux only) +func (f *Frontend) RemoveDeliveredNotification(_ string) error { + return nil +} + +// RemoveNotification is a Windows stub that always returns nil. +// (Linux-specific) +func (f *Frontend) RemoveNotification(identifier string) error { + return nil +} + +func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) { + callbackLock.Lock() + defer callbackLock.Unlock() + + notificationResultCallback = callback +} + +func (f *Frontend) saveIconToDir() error { + iconOnce.Do(func() { + hIcon := w32.ExtractIcon(exePath, 0) + if hIcon == 0 { + iconErr = fmt.Errorf("ExtractIcon failed for %s", exePath) + return + } + defer w32.DestroyIcon(hIcon) + iconErr = winc.SaveHIconAsPNG(hIcon, iconPath) + }) + return iconErr +} + +func saveCategoriesToRegistry() error { + // We assume lock is held by caller + + registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName) + + key, _, err := registry.CreateKey( + registry.CURRENT_USER, + registryPath, + registry.ALL_ACCESS, + ) + if err != nil { + return err + } + defer key.Close() + + data, err := json.Marshal(categories) + if err != nil { + return err + } + + return key.SetStringValue(NotificationCategoriesRegistryKey, string(data)) +} + +func loadCategoriesFromRegistry() error { + // We assume lock is held by caller + + registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName) + + key, err := registry.OpenKey( + registry.CURRENT_USER, + registryPath, + registry.QUERY_VALUE, + ) + if err != nil { + if err == registry.ErrNotExist { + // Not an error, no saved categories + return nil + } + return fmt.Errorf("failed to open registry key: %w", err) + } + defer key.Close() + + data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey) + if err != nil { + if err == registry.ErrNotExist { + // No value yet, but key exists + return nil + } + return fmt.Errorf("failed to read categories from registry: %w", err) + } + + _categories := make(map[string]frontend.NotificationCategory) + if err := json.Unmarshal([]byte(data), &_categories); err != nil { + return fmt.Errorf("failed to parse notification categories from registry: %w", err) + } + + categories = _categories + + return nil +} + +func getUserText(data []toast.UserData) (string, bool) { + for _, d := range data { + if d.Key == "userText" { + return d.Value, true + } + } + return "", false +} + +// encodePayload combines an action ID and user data into a single encoded string +func encodePayload(actionID string, options frontend.NotificationOptions) (string, error) { + payload := NotificationPayload{ + Action: actionID, + Options: options, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return actionID, err + } + + encodedPayload := base64.StdEncoding.EncodeToString(jsonData) + return encodedPayload, nil +} + +// decodePayload extracts the action ID and user data from an encoded payload +func decodePayload(encodedString string) (string, frontend.NotificationOptions, error) { + jsonData, err := base64.StdEncoding.DecodeString(encodedString) + if err != nil { + return encodedString, frontend.NotificationOptions{}, fmt.Errorf("failed to decode base64 payload: %w", err) + } + + var payload NotificationPayload + if err := json.Unmarshal(jsonData, &payload); err != nil { + return encodedString, frontend.NotificationOptions{}, fmt.Errorf("failed to unmarshal notification payload: %w", err) + } + + return payload.Action, payload.Options, nil +} + +// parseNotificationResponse updated to use structured payload decoding +func parseNotificationResponse(response string) (action string, options frontend.NotificationOptions, err error) { + actionID, options, err := decodePayload(response) + + if err != nil { + log.Printf("Warning: Failed to decode notification response: %v", err) + return response, frontend.NotificationOptions{}, err + } + + return actionID, options, nil +} + +func handleNotificationResult(result frontend.NotificationResult) { + callbackLock.RLock() + callback := notificationResultCallback + callbackLock.RUnlock() + + if callback != nil { + go func() { + defer func() { + if r := recover(); r != nil { + // Log panic but don't crash the app + fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r) + } + }() + callback(result) + }() + } +} + +// Helper functions + +func getGUID() (string, error) { + keyPath := ToastRegistryPath + appName + + k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE) + if err == nil { + guid, _, err := k.GetStringValue(ToastRegistryGuidKey) + k.Close() + if err == nil && guid != "" { + return guid, nil + } + } + + guid := generateGUID() + + k, _, err = registry.CreateKey(registry.CURRENT_USER, keyPath, registry.WRITE) + if err != nil { + return "", fmt.Errorf("failed to create registry key: %w", err) + } + defer k.Close() + + if err := k.SetStringValue(ToastRegistryGuidKey, guid); err != nil { + return "", fmt.Errorf("failed to write GUID to registry: %w", err) + } + + return guid, nil +} + +func generateGUID() string { + guid := uuid.New() + return fmt.Sprintf("{%s}", guid.String()) +} diff --git a/v2/internal/frontend/desktop/windows/single_instance.go b/v2/internal/frontend/desktop/windows/single_instance.go new file mode 100644 index 000000000..a02b7edb9 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/single_instance.go @@ -0,0 +1,136 @@ +//go:build windows + +package windows + +import ( + "encoding/json" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" + "github.com/wailsapp/wails/v2/pkg/options" + "golang.org/x/sys/windows" + "log" + "os" + "syscall" + "unsafe" +) + +type COPYDATASTRUCT struct { + dwData uintptr + cbData uint32 + lpData uintptr +} + +// WMCOPYDATA_SINGLE_INSTANCE_DATA we define our own type for WM_COPYDATA message +const WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542 + +func SendMessage(hwnd w32.HWND, data string) { + arrUtf16, _ := syscall.UTF16FromString(data) + + pCopyData := new(COPYDATASTRUCT) + pCopyData.dwData = WMCOPYDATA_SINGLE_INSTANCE_DATA + pCopyData.cbData = uint32(len(arrUtf16)*2 + 1) + pCopyData.lpData = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(data))) + + w32.SendMessage(hwnd, w32.WM_COPYDATA, 0, uintptr(unsafe.Pointer(pCopyData))) +} + +// SetupSingleInstance single instance Windows app +func SetupSingleInstance(uniqueId string) { + id := "wails-app-" + uniqueId + + className := id + "-sic" + windowName := id + "-siw" + mutexName := id + "sim" + + _, err := windows.CreateMutex(nil, false, windows.StringToUTF16Ptr(mutexName)) + + if err != nil { + if err == windows.ERROR_ALREADY_EXISTS { + // app is already running + hwnd := w32.FindWindowW(windows.StringToUTF16Ptr(className), windows.StringToUTF16Ptr(windowName)) + + if hwnd != 0 { + data := options.SecondInstanceData{ + Args: os.Args[1:], + } + data.WorkingDirectory, err = os.Getwd() + if err != nil { + log.Printf("Failed to get working directory: %v", err) + return + } + serialized, err := json.Marshal(data) + if err != nil { + log.Printf("Failed to marshal data: %v", err) + return + } + + SendMessage(hwnd, string(serialized)) + // exit second instance of app after sending message + os.Exit(0) + } + // if we got any other unknown error we will just start new application instance + } + } else { + createEventTargetWindow(className, windowName) + } +} + +func createEventTargetWindow(className string, windowName string) w32.HWND { + // callback handler in the event target window + wndProc := func( + hwnd w32.HWND, msg uint32, wparam w32.WPARAM, lparam w32.LPARAM, + ) w32.LRESULT { + if msg == w32.WM_COPYDATA { + ldata := (*COPYDATASTRUCT)(unsafe.Pointer(lparam)) + + if ldata.dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA { + serialized := windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ldata.lpData))) + + var secondInstanceData options.SecondInstanceData + + err := json.Unmarshal([]byte(serialized), &secondInstanceData) + + if err == nil { + secondInstanceBuffer <- secondInstanceData + } + } + + return w32.LRESULT(0) + } + + return w32.DefWindowProc(hwnd, msg, wparam, lparam) + } + + var class w32.WNDCLASSEX + class.Size = uint32(unsafe.Sizeof(class)) + class.Style = 0 + class.WndProc = syscall.NewCallback(wndProc) + class.ClsExtra = 0 + class.WndExtra = 0 + class.Instance = w32.GetModuleHandle("") + class.Icon = 0 + class.Cursor = 0 + class.Background = 0 + class.MenuName = nil + class.ClassName = windows.StringToUTF16Ptr(className) + class.IconSm = 0 + + w32.RegisterClassEx(&class) + + // create event window that will not be visible for user + hwnd := w32.CreateWindowEx( + 0, + windows.StringToUTF16Ptr(className), + windows.StringToUTF16Ptr(windowName), + 0, + 0, + 0, + 0, + 0, + w32.HWND_MESSAGE, + 0, + w32.GetModuleHandle(""), + nil, + ) + + return hwnd +} diff --git a/v2/internal/frontend/desktop/windows/winc/app.go b/v2/internal/frontend/desktop/windows/winc/app.go index 52b30f591..973880444 100644 --- a/v2/internal/frontend/desktop/windows/winc/app.go +++ b/v2/internal/frontend/desktop/windows/winc/app.go @@ -37,7 +37,7 @@ func init() { w32.InitCommonControlsEx(&initCtrls) } -// SetAppIconID sets recource icon ID for the apps windows. +// SetAppIcon sets resource icon ID for the apps windows. func SetAppIcon(appIconID int) { AppIconID = appIconID } diff --git a/v2/internal/frontend/desktop/windows/winc/combobox.go b/v2/internal/frontend/desktop/windows/winc/combobox.go index 3b4348acb..380ea88d8 100644 --- a/v2/internal/frontend/desktop/windows/winc/combobox.go +++ b/v2/internal/frontend/desktop/windows/winc/combobox.go @@ -54,7 +54,7 @@ func (cb *ComboBox) OnSelectedChange() *EventManager { return &cb.onSelectedChange } -// Message processer +// Message processor func (cb *ComboBox) WndProc(msg uint32, wparam, lparam uintptr) uintptr { switch msg { case w32.WM_COMMAND: diff --git a/v2/internal/frontend/desktop/windows/winc/controlbase.go b/v2/internal/frontend/desktop/windows/winc/controlbase.go index b745cb1b0..cdb29518c 100644 --- a/v2/internal/frontend/desktop/windows/winc/controlbase.go +++ b/v2/internal/frontend/desktop/windows/winc/controlbase.go @@ -65,7 +65,7 @@ type ControlBase struct { dispatchq []func() } -// initControl is called by controls: edit, button, treeview, listview, and so on. +// InitControl is called by controls: edit, button, treeview, listview, and so on. func (cba *ControlBase) InitControl(className string, parent Controller, exstyle, style uint) { cba.hwnd = CreateWindow(className, parent, exstyle, style) if cba.hwnd == 0 { @@ -170,6 +170,14 @@ func (cba *ControlBase) SetTranslucentBackground() { w32.SetWindowCompositionAttribute(cba.hwnd, &data) } +func (cba *ControlBase) SetContentProtection(enable bool) { + if enable { + w32.SetWindowDisplayAffinity(uintptr(cba.hwnd), w32.WDA_EXCLUDEFROMCAPTURE) + } else { + w32.SetWindowDisplayAffinity(uintptr(cba.hwnd), w32.WDA_NONE) + } +} + func min(a, b int) int { if a < b { return a @@ -334,7 +342,23 @@ func (cba *ControlBase) ClientHeight() int { } func (cba *ControlBase) Show() { - w32.ShowWindow(cba.hwnd, w32.SW_SHOWDEFAULT) + // WindowPos is used with HWND_TOPMOST to guarantee bring our app on top + // force set our main window on top + w32.SetWindowPos( + cba.hwnd, + w32.HWND_TOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // remove topmost to allow normal windows manipulations + w32.SetWindowPos( + cba.hwnd, + w32.HWND_NOTOPMOST, + 0, 0, 0, 0, + w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE, + ) + // put main window on tops foreground + w32.SetForegroundWindow(cba.hwnd) } func (cba *ControlBase) Hide() { diff --git a/v2/internal/frontend/desktop/windows/winc/form.go b/v2/internal/frontend/desktop/windows/winc/form.go index 9b9cadb2c..c9acf7278 100644 --- a/v2/internal/frontend/desktop/windows/winc/form.go +++ b/v2/internal/frontend/desktop/windows/winc/form.go @@ -136,7 +136,16 @@ func (fm *Form) Minimise() { } func (fm *Form) Restore() { - w32.ShowWindow(fm.hwnd, w32.SW_RESTORE) + // SC_RESTORE param for WM_SYSCOMMAND to restore app if it is minimized + const SC_RESTORE = 0xF120 + // restore the minimized window, if it is + w32.SendMessage( + fm.hwnd, + w32.WM_SYSCOMMAND, + SC_RESTORE, + 0, + ) + w32.ShowWindow(fm.hwnd, w32.SW_SHOW) } // Public methods diff --git a/v2/internal/frontend/desktop/windows/winc/icon.go b/v2/internal/frontend/desktop/windows/winc/icon.go index 6a3e1a391..94e9198d6 100644 --- a/v2/internal/frontend/desktop/windows/winc/icon.go +++ b/v2/internal/frontend/desktop/windows/winc/icon.go @@ -10,11 +10,86 @@ package winc import ( "errors" "fmt" + "image" + "image/png" + "os" "syscall" + "unsafe" "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" ) +var ( + user32 = syscall.NewLazyDLL("user32.dll") + gdi32 = syscall.NewLazyDLL("gdi32.dll") + procGetIconInfo = user32.NewProc("GetIconInfo") + procDeleteObject = gdi32.NewProc("DeleteObject") + procGetObject = gdi32.NewProc("GetObjectW") + procGetDIBits = gdi32.NewProc("GetDIBits") + procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC") + procSelectObject = gdi32.NewProc("SelectObject") + procDeleteDC = gdi32.NewProc("DeleteDC") +) + +func init() { + // Validate DLL loads at initialization time to surface missing APIs early + if err := user32.Load(); err != nil { + panic(fmt.Sprintf("failed to load user32.dll: %v", err)) + } + if err := gdi32.Load(); err != nil { + panic(fmt.Sprintf("failed to load gdi32.dll: %v", err)) + } +} + +// ICONINFO mirrors the Win32 ICONINFO struct +type ICONINFO struct { + FIcon int32 + XHotspot uint32 + YHotspot uint32 + HbmMask uintptr + HbmColor uintptr +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183376.aspx +type BITMAPINFOHEADER struct { + BiSize uint32 + BiWidth int32 + BiHeight int32 + BiPlanes uint16 + BiBitCount uint16 + BiCompression uint32 + BiSizeImage uint32 + BiXPelsPerMeter int32 + BiYPelsPerMeter int32 + BiClrUsed uint32 + BiClrImportant uint32 +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162938.aspx +type RGBQUAD struct { + RgbBlue byte + RgbGreen byte + RgbRed byte + RgbReserved byte +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183375.aspx +type BITMAPINFO struct { + BmiHeader BITMAPINFOHEADER + BmiColors *RGBQUAD +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183371.aspx +type BITMAP struct { + BmType int32 + BmWidth int32 + BmHeight int32 + BmWidthBytes int32 + BmPlanes uint16 + BmBitsPixel uint16 + BmBits unsafe.Pointer +} + type Icon struct { handle w32.HICON } @@ -46,6 +121,95 @@ func ExtractIcon(fileName string, index int) (*Icon, error) { return ico, err } +func SaveHIconAsPNG(hIcon w32.HICON, filePath string) error { + // Get icon info + var iconInfo ICONINFO + ret, _, err := procGetIconInfo.Call( + uintptr(hIcon), + uintptr(unsafe.Pointer(&iconInfo)), + ) + if ret == 0 { + return err + } + defer procDeleteObject.Call(uintptr(iconInfo.HbmMask)) + defer procDeleteObject.Call(uintptr(iconInfo.HbmColor)) + + // Get bitmap info + var bmp BITMAP + ret, _, err = procGetObject.Call( + uintptr(iconInfo.HbmColor), + unsafe.Sizeof(bmp), + uintptr(unsafe.Pointer(&bmp)), + ) + if ret == 0 { + return err + } + + // Get screen DC for GetDIBits (bitmap must not be selected into a DC) + screenDC := w32.GetDC(0) + if screenDC == 0 { + return fmt.Errorf("failed to get screen DC") + } + defer w32.ReleaseDC(0, screenDC) + + // Prepare bitmap info header + var bi BITMAPINFO + bi.BmiHeader.BiSize = uint32(unsafe.Sizeof(bi.BmiHeader)) + bi.BmiHeader.BiWidth = bmp.BmWidth + bi.BmiHeader.BiHeight = bmp.BmHeight + bi.BmiHeader.BiPlanes = 1 + bi.BmiHeader.BiBitCount = 32 + bi.BmiHeader.BiCompression = w32.BI_RGB + + // Allocate memory for bitmap bits + width, height := int(bmp.BmWidth), int(bmp.BmHeight) + bufferSize := width * height * 4 + bits := make([]byte, bufferSize) + + // Get bitmap bits using screen DC (bitmap must not be selected into any DC) + ret, _, err = procGetDIBits.Call( + uintptr(screenDC), + uintptr(iconInfo.HbmColor), + 0, + uintptr(bmp.BmHeight), + uintptr(unsafe.Pointer(&bits[0])), + uintptr(unsafe.Pointer(&bi)), + w32.DIB_RGB_COLORS, + ) + if ret == 0 { + return fmt.Errorf("failed to get bitmap bits: %w", err) + } + + // Create Go image + img := image.NewRGBA(image.Rect(0, 0, width, height)) + + // Convert DIB to RGBA + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + // DIB is bottom-up, so we need to invert Y + dibIndex := ((height-1-y)*width + x) * 4 + // RGBA image is top-down + imgIndex := (y*width + x) * 4 + + // BGRA to RGBA + img.Pix[imgIndex] = bits[dibIndex+2] // R + img.Pix[imgIndex+1] = bits[dibIndex+1] // G + img.Pix[imgIndex+2] = bits[dibIndex] // B + img.Pix[imgIndex+3] = bits[dibIndex+3] // A + } + } + + // Create output file + outFile, err := os.Create(filePath) + if err != nil { + return err + } + defer outFile.Close() + + // Encode and save the image + return png.Encode(outFile, img) +} + func (ic *Icon) Destroy() bool { return w32.DestroyIcon(ic.handle) } diff --git a/v2/internal/frontend/desktop/windows/winc/layout.go b/v2/internal/frontend/desktop/windows/winc/layout.go index da9895205..7962dc726 100644 --- a/v2/internal/frontend/desktop/windows/winc/layout.go +++ b/v2/internal/frontend/desktop/windows/winc/layout.go @@ -65,7 +65,7 @@ type SimpleDock struct { loadedState bool } -// DockState gets saved and loaded from json +// CtlState gets saved and loaded from json type CtlState struct { X, Y, Width, Height int } diff --git a/v2/internal/frontend/desktop/windows/winc/listview.go b/v2/internal/frontend/desktop/windows/winc/listview.go index c98fc4c62..8edfd1c11 100644 --- a/v2/internal/frontend/desktop/windows/winc/listview.go +++ b/v2/internal/frontend/desktop/windows/winc/listview.go @@ -438,7 +438,7 @@ func (lv *ListView) OnEndScroll() *EventManager { return &lv.onEndScroll } -// Message processer +// Message processor func (lv *ListView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { switch msg { /*case w32.WM_ERASEBKGND: diff --git a/v2/internal/frontend/desktop/windows/winc/treeview.go b/v2/internal/frontend/desktop/windows/winc/treeview.go index 9118f3d05..2cdc0e936 100644 --- a/v2/internal/frontend/desktop/windows/winc/treeview.go +++ b/v2/internal/frontend/desktop/windows/winc/treeview.go @@ -248,7 +248,7 @@ func (tv *TreeView) OnViewChange() *EventManager { return &tv.onViewChange } -// Message processer +// Message processor func (tv *TreeView) WndProc(msg uint32, wparam, lparam uintptr) uintptr { switch msg { case w32.WM_NOTIFY: diff --git a/v2/internal/frontend/desktop/windows/winc/w32/user32.go b/v2/internal/frontend/desktop/windows/winc/w32/user32.go index ec5e4a596..707701f5e 100644 --- a/v2/internal/frontend/desktop/windows/winc/w32/user32.go +++ b/v2/internal/frontend/desktop/windows/winc/w32/user32.go @@ -24,6 +24,7 @@ var ( procShowWindowAsync = moduser32.NewProc("ShowWindowAsync") procUpdateWindow = moduser32.NewProc("UpdateWindow") procCreateWindowEx = moduser32.NewProc("CreateWindowExW") + procFindWindowW = moduser32.NewProc("FindWindowW") procAdjustWindowRect = moduser32.NewProc("AdjustWindowRect") procAdjustWindowRectEx = moduser32.NewProc("AdjustWindowRectEx") procDestroyWindow = moduser32.NewProc("DestroyWindow") @@ -263,6 +264,14 @@ func CreateWindowEx(exStyle uint, className, windowName *uint16, return HWND(ret) } +func FindWindowW(className, windowName *uint16) HWND { + ret, _, _ := procFindWindowW.Call( + uintptr(unsafe.Pointer(className)), + uintptr(unsafe.Pointer(windowName))) + + return HWND(ret) +} + func AdjustWindowRectEx(rect *RECT, style uint, menu bool, exStyle uint) bool { ret, _, _ := procAdjustWindowRectEx.Call( uintptr(unsafe.Pointer(rect)), @@ -630,7 +639,7 @@ func GetSysColorBrush(nIndex int) HBRUSH { return HBRUSH(ret) */ - ret, _, _ := syscall.Syscall(getSysColorBrush, 1, + ret, _, _ := syscall.SyscallN(getSysColorBrush, uintptr(nIndex), 0, 0) @@ -783,11 +792,9 @@ func CreateMenu() HMENU { } func SetMenu(hWnd HWND, hMenu HMENU) bool { - ret, _, _ := syscall.Syscall(setMenu, 2, + ret, _, _ := syscall.SyscallN(setMenu, uintptr(hWnd), - uintptr(hMenu), - 0) - + uintptr(hMenu)) return ret != 0 } @@ -825,11 +832,7 @@ func TrackPopupMenuEx(hMenu HMENU, fuFlags uint32, x, y int32, hWnd HWND, lptpm } func DrawMenuBar(hWnd HWND) bool { - ret, _, _ := syscall.Syscall(drawMenuBar, 1, - uintptr(hWnd), - 0, - 0) - + ret, _, _ := syscall.SyscallN(drawMenuBar, hWnd) return ret != 0 } @@ -1222,11 +1225,8 @@ func CallNextHookEx(hhk HHOOK, nCode int, wParam WPARAM, lParam LPARAM) LRESULT } func GetKeyState(nVirtKey int32) int16 { - ret, _, _ := syscall.Syscall(getKeyState, 1, - uintptr(nVirtKey), - 0, - 0) - + ret, _, _ := syscall.SyscallN(getKeyState, + uintptr(nVirtKey)) return int16(ret) } @@ -1240,17 +1240,15 @@ func DestroyMenu(hMenu HMENU) bool { } func GetWindowPlacement(hWnd HWND, lpwndpl *WINDOWPLACEMENT) bool { - ret, _, _ := syscall.Syscall(getWindowPlacement, 2, - uintptr(hWnd), - uintptr(unsafe.Pointer(lpwndpl)), - 0) - + ret, _, _ := syscall.SyscallN(getWindowPlacement, + hWnd, + uintptr(unsafe.Pointer(lpwndpl))) return ret != 0 } func SetWindowPlacement(hWnd HWND, lpwndpl *WINDOWPLACEMENT) bool { - ret, _, _ := syscall.Syscall(setWindowPlacement, 2, - uintptr(hWnd), + ret, _, _ := syscall.SyscallN(setWindowPlacement, + hWnd, uintptr(unsafe.Pointer(lpwndpl)), 0) @@ -1270,7 +1268,7 @@ func SetScrollInfo(hwnd HWND, fnBar int32, lpsi *SCROLLINFO, fRedraw bool) int32 } func GetScrollInfo(hwnd HWND, fnBar int32, lpsi *SCROLLINFO) bool { - ret, _, _ := syscall.Syscall(getScrollInfo, 3, + ret, _, _ := syscall.SyscallN(getScrollInfo, hwnd, uintptr(fnBar), uintptr(unsafe.Pointer(lpsi))) diff --git a/v2/internal/frontend/desktop/windows/winc/w32/utils.go b/v2/internal/frontend/desktop/windows/winc/w32/utils.go index 8a72d4846..4568b4849 100644 --- a/v2/internal/frontend/desktop/windows/winc/w32/utils.go +++ b/v2/internal/frontend/desktop/windows/winc/w32/utils.go @@ -75,7 +75,7 @@ func UTF16PtrToString(cstr *uint16) string { } func ComAddRef(unknown *IUnknown) int32 { - ret, _, _ := syscall.Syscall(unknown.lpVtbl.pAddRef, 1, + ret, _, _ := syscall.SyscallN(unknown.lpVtbl.pAddRef, uintptr(unsafe.Pointer(unknown)), 0, 0) @@ -83,7 +83,7 @@ func ComAddRef(unknown *IUnknown) int32 { } func ComRelease(unknown *IUnknown) int32 { - ret, _, _ := syscall.Syscall(unknown.lpVtbl.pRelease, 1, + ret, _, _ := syscall.SyscallN(unknown.lpVtbl.pRelease, uintptr(unsafe.Pointer(unknown)), 0, 0) @@ -92,7 +92,7 @@ func ComRelease(unknown *IUnknown) int32 { func ComQueryInterface(unknown *IUnknown, id *GUID) *IDispatch { var disp *IDispatch - hr, _, _ := syscall.Syscall(unknown.lpVtbl.pQueryInterface, 3, + hr, _, _ := syscall.SyscallN(unknown.lpVtbl.pQueryInterface, uintptr(unsafe.Pointer(unknown)), uintptr(unsafe.Pointer(id)), uintptr(unsafe.Pointer(&disp))) diff --git a/v2/internal/frontend/desktop/windows/winc/w32/uxtheme.go b/v2/internal/frontend/desktop/windows/winc/w32/uxtheme.go index 51ec0035f..8a14f0cb7 100644 --- a/v2/internal/frontend/desktop/windows/winc/w32/uxtheme.go +++ b/v2/internal/frontend/desktop/windows/winc/w32/uxtheme.go @@ -69,7 +69,7 @@ var ( setWindowTheme uintptr ) -func init() { +func Init() { // Library libuxtheme = MustLoadLibrary("uxtheme.dll") @@ -83,7 +83,7 @@ func init() { } func CloseThemeData(hTheme HTHEME) HRESULT { - ret, _, _ := syscall.Syscall(closeThemeData, 1, + ret, _, _ := syscall.SyscallN(closeThemeData, uintptr(hTheme), 0, 0) @@ -134,7 +134,7 @@ func GetThemeTextExtent(hTheme HTHEME, hdc HDC, iPartId, iStateId int32, pszText } func OpenThemeData(hwnd HWND, pszClassList *uint16) HTHEME { - ret, _, _ := syscall.Syscall(openThemeData, 2, + ret, _, _ := syscall.SyscallN(openThemeData, uintptr(hwnd), uintptr(unsafe.Pointer(pszClassList)), 0) @@ -143,7 +143,7 @@ func OpenThemeData(hwnd HWND, pszClassList *uint16) HTHEME { } func SetWindowTheme(hwnd HWND, pszSubAppName, pszSubIdList *uint16) HRESULT { - ret, _, _ := syscall.Syscall(setWindowTheme, 3, + ret, _, _ := syscall.SyscallN(setWindowTheme, uintptr(hwnd), uintptr(unsafe.Pointer(pszSubAppName)), uintptr(unsafe.Pointer(pszSubIdList))) diff --git a/v2/internal/frontend/desktop/windows/winc/w32/wda.go b/v2/internal/frontend/desktop/windows/winc/w32/wda.go new file mode 100644 index 000000000..3925f2805 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/winc/w32/wda.go @@ -0,0 +1,47 @@ +//go:build windows + +package w32 + +import ( + "syscall" + + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" +) + +var user32 = syscall.NewLazyDLL("user32.dll") +var procSetWindowDisplayAffinity = user32.NewProc("SetWindowDisplayAffinity") +var windowsVersion, _ = operatingsystem.GetWindowsVersionInfo() + +const ( + WDA_NONE = 0x00000000 + WDA_MONITOR = 0x00000001 + WDA_EXCLUDEFROMCAPTURE = 0x00000011 // windows 10 2004+ +) + +func isWindowsVersionAtLeast(major, minor, build int) bool { + if windowsVersion.Major > major { + return true + } + if windowsVersion.Major < major { + return false + } + if windowsVersion.Minor > minor { + return true + } + if windowsVersion.Minor < minor { + return false + } + return windowsVersion.Build >= build +} + +func SetWindowDisplayAffinity(hwnd uintptr, affinity uint32) bool { + if affinity == WDA_EXCLUDEFROMCAPTURE && !isWindowsVersionAtLeast(10, 0, 19041) { + // for older windows versions, use WDA_MONITOR + affinity = WDA_MONITOR + } + ret, _, _ := procSetWindowDisplayAffinity.Call( + hwnd, + uintptr(affinity), + ) + return ret != 0 +} diff --git a/v2/internal/frontend/desktop/windows/window.go b/v2/internal/frontend/desktop/windows/window.go index a513e875a..b04d61814 100644 --- a/v2/internal/frontend/desktop/windows/window.go +++ b/v2/internal/frontend/desktop/windows/window.go @@ -3,10 +3,11 @@ package windows import ( - "github.com/wailsapp/go-webview2/pkg/edge" "sync" "unsafe" + "github.com/wailsapp/go-webview2/pkg/edge" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32" "github.com/wailsapp/wails/v2/internal/system/operatingsystem" @@ -37,6 +38,13 @@ type Window struct { OnResume func() chromium *edge.Chromium + + // isMinimizing indicates whether the window is currently being minimized + // 标识窗口是否处于最小化状态,用于解决最小化/恢复时的闪屏问题 + // This flag is used to prevent unnecessary redraws during minimize/restore transitions for frameless windows + // 此标志用于防止无边框窗口在最小化/恢复过程中的不必要重绘 + // Reference: https://github.com/wailsapp/wails/issues/3951 + isMinimizing bool } func NewWindow(parent winc.Controller, appoptions *options.App, versionInfo *operatingsystem.WindowsVersionInfo, chromium *edge.Chromium) *Window { @@ -70,8 +78,13 @@ func NewWindow(parent winc.Controller, appoptions *options.App, versionInfo *ope var dwStyle = w32.WS_OVERLAPPEDWINDOW - winc.RegClassOnlyOnce("wailsWindow") - handle := winc.CreateWindow("wailsWindow", parent, uint(exStyle), uint(dwStyle)) + windowClassName := "wailsWindow" + if windowsOptions != nil && windowsOptions.WindowClassName != "" { + windowClassName = windowsOptions.WindowClassName + } + + winc.RegClassOnlyOnce(windowClassName) + handle := winc.CreateWindow(windowClassName, parent, uint(exStyle), uint(dwStyle)) result.SetHandle(handle) winc.RegMsgHandler(result) result.SetParent(parent) @@ -118,6 +131,10 @@ func NewWindow(parent winc.Controller, appoptions *options.App, versionInfo *ope } } + if windowsOptions.ContentProtection { + w32.SetWindowDisplayAffinity(result.Handle(), w32.WDA_EXCLUDEFROMCAPTURE) + } + if windowsOptions.DisableWindowIcon { result.DisableIcon() } @@ -251,7 +268,7 @@ func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr { rgrc := (*w32.RECT)(unsafe.Pointer(lparam)) if w.Form.IsFullScreen() { // In Full-Screen mode we don't need to adjust anything - w.chromium.SetPadding(edge.Rect{}) + w.SetPadding(edge.Rect{}) } else if w.IsMaximised() { // If the window is maximized we must adjust the client area to the work area of the monitor. Otherwise // some content goes beyond the visible part of the monitor. @@ -282,7 +299,7 @@ func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr { } } } - w.chromium.SetPadding(edge.Rect{}) + w.SetPadding(edge.Rect{}) } else { // This is needed to workaround the resize flickering in frameless mode with WindowDecorations // See: https://stackoverflow.com/a/6558508 @@ -291,7 +308,7 @@ func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr { // Increasing the bottom also worksaround the flickering but we would loose 1px of the WebView content // therefore let's pad the content with 1px at the bottom. rgrc.Bottom += 1 - w.chromium.SetPadding(edge.Rect{Bottom: 1}) + w.SetPadding(edge.Rect{Bottom: 1}) } return 0 } @@ -334,3 +351,17 @@ func invokeSync[T any](cba *Window, fn func() (T, error)) (res T, err error) { wg.Wait() return res, err } + +// SetPadding is a filter that wraps chromium.SetPadding to prevent unnecessary redraws during minimize/restore +// 包装了chromium.SetPadding的过滤器,用于防止窗口最小化/恢复过程中的不必要重绘 +// This fixes window flickering when minimizing/restoring frameless windows +// 这修复了无边框窗口在最小化/恢复时的闪烁问题 +// Reference: https://github.com/wailsapp/wails/issues/3951 +func (w *Window) SetPadding(padding edge.Rect) { + // Skip SetPadding if window is being minimized to prevent flickering + // 如果窗口正在最小化,跳过设置padding以防止闪烁 + if w.isMinimizing { + return + } + w.chromium.SetPadding(padding) +} diff --git a/v2/internal/frontend/devserver/devserver.go b/v2/internal/frontend/devserver/devserver.go index 3d623ed6d..8a130890d 100644 --- a/v2/internal/frontend/devserver/devserver.go +++ b/v2/internal/frontend/devserver/devserver.go @@ -20,17 +20,23 @@ import ( "github.com/wailsapp/wails/v2/internal/frontend/runtime" + "github.com/gorilla/websocket" "github.com/labstack/echo/v4" "github.com/wailsapp/wails/v2/internal/binding" "github.com/wailsapp/wails/v2/internal/frontend" "github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/menumanager" "github.com/wailsapp/wails/v2/pkg/options" - "golang.org/x/net/websocket" ) type Screen = frontend.Screen +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { return true }, +} + type DevWebServer struct { server *echo.Echo ctx context.Context @@ -155,51 +161,64 @@ func (d *DevWebServer) handleReloadApp(c echo.Context) error { } func (d *DevWebServer) handleIPCWebSocket(c echo.Context) error { - websocket.Handler(func(c *websocket.Conn) { - d.LogDebug(fmt.Sprintf("Websocket client %p connected", c)) + conn, err := upgrader.Upgrade(c.Response(), c.Request(), nil) + if err != nil { + d.logger.Error("WebSocket upgrade failed %v", err) + return err + } + d.LogDebug(fmt.Sprintf("WebSocket client %p connected", conn)) + + d.socketMutex.Lock() + d.websocketClients[conn] = &sync.Mutex{} + locker := d.websocketClients[conn] + d.socketMutex.Unlock() + + var wg sync.WaitGroup + + defer func() { + wg.Wait() d.socketMutex.Lock() - d.websocketClients[c] = &sync.Mutex{} - locker := d.websocketClients[c] + delete(d.websocketClients, conn) d.socketMutex.Unlock() + d.LogDebug(fmt.Sprintf("WebSocket client %p disconnected", conn)) + conn.Close() + }() - defer func() { - d.socketMutex.Lock() - delete(d.websocketClients, c) - d.socketMutex.Unlock() - d.LogDebug(fmt.Sprintf("Websocket client %p disconnected", c)) - }() + for { + _, msgBytes, err := conn.ReadMessage() + if err != nil { + break + } - var msg string - defer c.Close() - for { - if err := websocket.Message.Receive(c, &msg); err != nil { - break - } - // We do not support drag in browsers - if msg == "drag" { - continue + msg := string(msgBytes) + wg.Add(1) + + go func(m string) { + defer wg.Done() + + if m == "drag" { + return } - // Notify the other browsers of "EventEmit" - if len(msg) > 2 && strings.HasPrefix(string(msg), "EE") { - d.notifyExcludingSender([]byte(msg), c) + if len(m) > 2 && strings.HasPrefix(m, "EE") { + d.notifyExcludingSender([]byte(m), conn) } - // Send the message to dispatch to the frontend - result, err := d.dispatcher.ProcessMessage(string(msg), d) + result, err := d.dispatcher.ProcessMessage(m, d) if err != nil { d.logger.Error(err.Error()) } + if result != "" { locker.Lock() - if err = websocket.Message.Send(c, result); err != nil { - locker.Unlock() - break + defer locker.Unlock() + if err := conn.WriteMessage(websocket.TextMessage, []byte(result)); err != nil { + d.logger.Error("Websocket write message failed %v", err) } - locker.Unlock() } - } - }).ServeHTTP(c.Response(), c.Request()) + }(msg) + } + return nil } @@ -222,7 +241,7 @@ func (d *DevWebServer) broadcast(message string) { return } locker.Lock() - err := websocket.Message.Send(client, message) + err := client.WriteMessage(websocket.TextMessage, []byte(message)) if err != nil { locker.Unlock() d.logger.Error(err.Error()) @@ -256,7 +275,7 @@ func (d *DevWebServer) broadcastExcludingSender(message string, sender *websocke return } locker.Lock() - err := websocket.Message.Send(client, message) + err := client.WriteMessage(websocket.TextMessage, []byte(message)) if err != nil { locker.Unlock() d.logger.Error(err.Error()) diff --git a/v2/internal/frontend/dispatcher/calls.go b/v2/internal/frontend/dispatcher/calls.go index 491d17fe2..ba1062913 100644 --- a/v2/internal/frontend/dispatcher/calls.go +++ b/v2/internal/frontend/dispatcher/calls.go @@ -15,7 +15,6 @@ type callMessage struct { } func (d *Dispatcher) processCallMessage(message string, sender frontend.Frontend) (string, error) { - var payload callMessage err := json.Unmarshal([]byte(message[1:]), &payload) if err != nil { diff --git a/v2/internal/frontend/dispatcher/dispatcher.go b/v2/internal/frontend/dispatcher/dispatcher.go index 56092d370..24a43cfef 100644 --- a/v2/internal/frontend/dispatcher/dispatcher.go +++ b/v2/internal/frontend/dispatcher/dispatcher.go @@ -2,7 +2,7 @@ package dispatcher import ( "context" - + "fmt" "github.com/pkg/errors" "github.com/wailsapp/wails/v2/internal/binding" "github.com/wailsapp/wails/v2/internal/frontend" @@ -11,26 +11,43 @@ import ( ) type Dispatcher struct { - log *logger.Logger - bindings *binding.Bindings - events frontend.Events - bindingsDB *binding.DB - ctx context.Context - errfmt options.ErrorFormatter + log *logger.Logger + bindings *binding.Bindings + events frontend.Events + bindingsDB *binding.DB + ctx context.Context + errfmt options.ErrorFormatter + disablePanicRecovery bool } -func NewDispatcher(ctx context.Context, log *logger.Logger, bindings *binding.Bindings, events frontend.Events, errfmt options.ErrorFormatter) *Dispatcher { +func NewDispatcher(ctx context.Context, log *logger.Logger, bindings *binding.Bindings, events frontend.Events, errfmt options.ErrorFormatter, disablePanicRecovery bool) *Dispatcher { return &Dispatcher{ - log: log, - bindings: bindings, - events: events, - bindingsDB: bindings.DB(), - ctx: ctx, - errfmt: errfmt, + log: log, + bindings: bindings, + events: events, + bindingsDB: bindings.DB(), + ctx: ctx, + errfmt: errfmt, + disablePanicRecovery: disablePanicRecovery, } } -func (d *Dispatcher) ProcessMessage(message string, sender frontend.Frontend) (string, error) { +func (d *Dispatcher) ProcessMessage(message string, sender frontend.Frontend) (_ string, err error) { + if !d.disablePanicRecovery { + defer func() { + if e := recover(); e != nil { + if errPanic, ok := e.(error); ok { + err = errPanic + } else { + err = fmt.Errorf("%v", e) + } + } + if err != nil { + d.log.Error("process message error: %s -> %s", message, err) + } + }() + } + if message == "" { return "", errors.New("No message to process") } @@ -47,6 +64,8 @@ func (d *Dispatcher) ProcessMessage(message string, sender frontend.Frontend) (s return d.processWindowMessage(message, sender) case 'B': return d.processBrowserMessage(message, sender) + case 'D': + return d.processDragAndDropMessage(message) case 'Q': sender.Quit() return "", nil diff --git a/v2/internal/frontend/dispatcher/draganddrop.go b/v2/internal/frontend/dispatcher/draganddrop.go new file mode 100644 index 000000000..8266ec712 --- /dev/null +++ b/v2/internal/frontend/dispatcher/draganddrop.go @@ -0,0 +1,38 @@ +package dispatcher + +import ( + "errors" + "strconv" + "strings" +) + +func (d *Dispatcher) processDragAndDropMessage(message string) (string, error) { + switch message[1] { + case 'D': + msg := strings.SplitN(message[3:], ":", 3) + if len(msg) != 3 { + return "", errors.New("Invalid drag and drop Message: " + message) + } + + x, err := strconv.Atoi(msg[0]) + if err != nil { + return "", errors.New("Invalid x coordinate in drag and drop Message: " + message) + } + + y, err := strconv.Atoi(msg[1]) + if err != nil { + return "", errors.New("Invalid y coordinate in drag and drop Message: " + message) + } + + paths := strings.Split(msg[2], "\n") + if len(paths) < 1 { + return "", errors.New("Invalid drag and drop Message: " + message) + } + + d.events.Emit("wails:file-drop", x, y, paths) + default: + return "", errors.New("Invalid drag and drop Message: " + message) + } + + return "", nil +} diff --git a/v2/internal/frontend/dispatcher/events.go b/v2/internal/frontend/dispatcher/events.go index 11680651b..12fe7b89e 100644 --- a/v2/internal/frontend/dispatcher/events.go +++ b/v2/internal/frontend/dispatcher/events.go @@ -3,6 +3,7 @@ package dispatcher import ( "encoding/json" "errors" + "github.com/wailsapp/wails/v2/internal/frontend" ) diff --git a/v2/internal/frontend/dispatcher/securecalls.go b/v2/internal/frontend/dispatcher/securecalls.go index 40accaa26..8cdcdfb85 100644 --- a/v2/internal/frontend/dispatcher/securecalls.go +++ b/v2/internal/frontend/dispatcher/securecalls.go @@ -3,6 +3,7 @@ package dispatcher import ( "encoding/json" "fmt" + "github.com/wailsapp/wails/v2/internal/frontend" ) @@ -13,7 +14,6 @@ type secureCallMessage struct { } func (d *Dispatcher) processSecureCallMessage(message string, sender frontend.Frontend) (string, error) { - var payload secureCallMessage err := json.Unmarshal([]byte(message[1:]), &payload) if err != nil { diff --git a/v2/internal/frontend/dispatcher/systemcalls.go b/v2/internal/frontend/dispatcher/systemcalls.go index b810d9bea..a13eb03b9 100644 --- a/v2/internal/frontend/dispatcher/systemcalls.go +++ b/v2/internal/frontend/dispatcher/systemcalls.go @@ -4,9 +4,10 @@ import ( "encoding/json" "errors" "fmt" - "github.com/wailsapp/wails/v2/pkg/runtime" "strings" + "github.com/wailsapp/wails/v2/pkg/runtime" + "github.com/wailsapp/wails/v2/internal/frontend" ) @@ -23,7 +24,6 @@ type size struct { } func (d *Dispatcher) processSystemCall(payload callMessage, sender frontend.Frontend) (interface{}, error) { - // Strip prefix name := strings.TrimPrefix(payload.Name, systemCallPrefix) @@ -61,8 +61,103 @@ func (d *Dispatcher) processSystemCall(payload callMessage, sender frontend.Fron return false, err } return true, nil + case "InitializeNotifications": + err := sender.InitializeNotifications() + return nil, err + case "CleanupNotifications": + sender.CleanupNotifications() + return nil, nil + case "IsNotificationAvailable": + return sender.IsNotificationAvailable(), nil + case "RequestNotificationAuthorization": + authorized, err := sender.RequestNotificationAuthorization() + if err != nil { + return nil, err + } + return authorized, nil + case "CheckNotificationAuthorization": + authorized, err := sender.CheckNotificationAuthorization() + if err != nil { + return nil, err + } + return authorized, nil + case "SendNotification": + if len(payload.Args) < 1 { + return nil, errors.New("empty argument, cannot send notification") + } + var options frontend.NotificationOptions + if err := json.Unmarshal(payload.Args[0], &options); err != nil { + return nil, err + } + err := sender.SendNotification(options) + return nil, err + case "SendNotificationWithActions": + if len(payload.Args) < 1 { + return nil, errors.New("empty argument, cannot send notification") + } + var options frontend.NotificationOptions + if err := json.Unmarshal(payload.Args[0], &options); err != nil { + return nil, err + } + err := sender.SendNotificationWithActions(options) + return nil, err + case "RegisterNotificationCategory": + if len(payload.Args) < 1 { + return nil, errors.New("empty argument, cannot register category") + } + var category frontend.NotificationCategory + if err := json.Unmarshal(payload.Args[0], &category); err != nil { + return nil, err + } + err := sender.RegisterNotificationCategory(category) + return nil, err + case "RemoveNotificationCategory": + if len(payload.Args) < 1 { + return nil, errors.New("empty argument, cannot remove category") + } + var categoryId string + if err := json.Unmarshal(payload.Args[0], &categoryId); err != nil { + return nil, err + } + err := sender.RemoveNotificationCategory(categoryId) + return nil, err + case "RemoveAllPendingNotifications": + err := sender.RemoveAllPendingNotifications() + return nil, err + case "RemovePendingNotification": + if len(payload.Args) < 1 { + return nil, errors.New("empty argument, cannot remove notification") + } + var identifier string + if err := json.Unmarshal(payload.Args[0], &identifier); err != nil { + return nil, err + } + err := sender.RemovePendingNotification(identifier) + return nil, err + case "RemoveAllDeliveredNotifications": + err := sender.RemoveAllDeliveredNotifications() + return nil, err + case "RemoveDeliveredNotification": + if len(payload.Args) < 1 { + return nil, errors.New("empty argument, cannot remove notification") + } + var identifier string + if err := json.Unmarshal(payload.Args[0], &identifier); err != nil { + return nil, err + } + err := sender.RemoveDeliveredNotification(identifier) + return nil, err + case "RemoveNotification": + if len(payload.Args) < 1 { + return nil, errors.New("empty argument, cannot remove notification") + } + var identifier string + if err := json.Unmarshal(payload.Args[0], &identifier); err != nil { + return nil, err + } + err := sender.RemoveNotification(identifier) + return nil, err default: return nil, fmt.Errorf("unknown systemcall message: %s", payload.Name) } - } diff --git a/v2/internal/frontend/frontend.go b/v2/internal/frontend/frontend.go index 27eb4a2e7..873b61dc7 100644 --- a/v2/internal/frontend/frontend.go +++ b/v2/internal/frontend/frontend.go @@ -76,8 +76,53 @@ type MessageDialogOptions struct { Icon []byte } +// NotificationOptions contains configuration for a notification. +type NotificationOptions struct { + ID string `json:"id"` + Title string `json:"title"` + Subtitle string `json:"subtitle,omitempty"` // (macOS and Linux only) + Body string `json:"body,omitempty"` + CategoryID string `json:"categoryId,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` +} + +// NotificationAction represents an action button for a notification. +type NotificationAction struct { + ID string `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Destructive bool `json:"destructive,omitempty"` // (macOS-specific) +} + +// NotificationCategory groups actions for notifications. +type NotificationCategory struct { + ID string `json:"id,omitempty"` + Actions []NotificationAction `json:"actions,omitempty"` + HasReplyField bool `json:"hasReplyField,omitempty"` + ReplyPlaceholder string `json:"replyPlaceholder,omitempty"` + ReplyButtonTitle string `json:"replyButtonTitle,omitempty"` +} + +// NotificationResponse represents the response sent by interacting with a notification. +type NotificationResponse struct { + ID string `json:"id,omitempty"` + ActionIdentifier string `json:"actionIdentifier,omitempty"` + CategoryID string `json:"categoryId,omitempty"` // Consistent with NotificationOptions + Title string `json:"title,omitempty"` + Subtitle string `json:"subtitle,omitempty"` // (macOS and Linux only) + Body string `json:"body,omitempty"` + UserText string `json:"userText,omitempty"` + UserInfo map[string]interface{} `json:"userInfo,omitempty"` +} + +// NotificationResult represents the result of a notification response, +// returning the response or any errors that occurred. +type NotificationResult struct { + Response NotificationResponse + Error error +} + type Frontend interface { - Run(context.Context) error + Run(ctx context.Context) error RunMainLoop() ExecJS(js string) Hide() @@ -123,7 +168,7 @@ type Frontend interface { WindowClose() WindowPrint() - //Screen + // Screen ScreenGetAll() ([]Screen, error) // Menus @@ -139,4 +184,21 @@ type Frontend interface { // Clipboard ClipboardGetText() (string, error) ClipboardSetText(text string) error + + // Notifications + InitializeNotifications() error + CleanupNotifications() + IsNotificationAvailable() bool + RequestNotificationAuthorization() (bool, error) + CheckNotificationAuthorization() (bool, error) + OnNotificationResponse(callback func(result NotificationResult)) + SendNotification(options NotificationOptions) error + SendNotificationWithActions(options NotificationOptions) error + RegisterNotificationCategory(category NotificationCategory) error + RemoveNotificationCategory(categoryId string) error + RemoveAllPendingNotifications() error + RemovePendingNotification(identifier string) error + RemoveAllDeliveredNotifications() error + RemoveDeliveredNotification(identifier string) error + RemoveNotification(identifier string) error } diff --git a/v2/internal/frontend/originvalidator/originValidator.go b/v2/internal/frontend/originvalidator/originValidator.go new file mode 100644 index 000000000..fd416f945 --- /dev/null +++ b/v2/internal/frontend/originvalidator/originValidator.go @@ -0,0 +1,116 @@ +package originvalidator + +import ( + "fmt" + "net/url" + "regexp" + "strings" +) + +type OriginValidator struct { + allowedOrigins []string +} + +// NewOriginValidator creates a new validator from a comma-separated string of allowed origins +func NewOriginValidator(startUrl *url.URL, allowedOriginsString string) *OriginValidator { + allowedOrigins := startUrl.Scheme + "://" + startUrl.Host + if allowedOriginsString != "" { + allowedOrigins += "," + allowedOriginsString + } + validator := &OriginValidator{} + validator.parseAllowedOrigins(allowedOrigins) + return validator +} + +// parseAllowedOrigins parses the comma-separated origins string +func (v *OriginValidator) parseAllowedOrigins(originsString string) { + if originsString == "" { + v.allowedOrigins = []string{} + return + } + + origins := strings.Split(originsString, ",") + var trimmedOrigins []string + + for _, origin := range origins { + trimmed := strings.TrimSuffix(strings.TrimSpace(origin), "/") + if trimmed != "" { + trimmedOrigins = append(trimmedOrigins, trimmed) + } + } + + v.allowedOrigins = trimmedOrigins +} + +// IsOriginAllowed checks if the given origin is allowed +func (v *OriginValidator) IsOriginAllowed(origin string) bool { + if origin == "" { + return false + } + + for _, allowedOrigin := range v.allowedOrigins { + if v.matchesOriginPattern(allowedOrigin, origin) { + return true + } + } + + return false +} + +// matchesOriginPattern checks if origin matches the pattern (supports wildcards) +func (v *OriginValidator) matchesOriginPattern(pattern, origin string) bool { + // Exact match + if pattern == origin { + return true + } + + // Wildcard pattern matching + if strings.Contains(pattern, "*") { + regexPattern := v.wildcardPatternToRegex(pattern) + matched, err := regexp.MatchString(regexPattern, origin) + if err != nil { + return false + } + return matched + } + + return false +} + +// wildcardPatternToRegex converts wildcard pattern to regex +func (v *OriginValidator) wildcardPatternToRegex(wildcardPattern string) string { + // Escape special regex characters except * + specialChars := []string{"\\", ".", "+", "?", "^", "$", "{", "}", "(", ")", "|", "[", "]"} + + escaped := wildcardPattern + for _, specialChar := range specialChars { + escaped = strings.ReplaceAll(escaped, specialChar, "\\"+specialChar) + } + + // Replace * with .* (matches any characters) + escaped = strings.ReplaceAll(escaped, "*", ".*") + + // Anchor the pattern to match the entire string + return "^" + escaped + "$" +} + +// GetOriginFromURL extracts origin from URL string +func (v *OriginValidator) GetOriginFromURL(urlString string) (string, error) { + if urlString == "" { + return "", fmt.Errorf("empty URL") + } + + parsedURL, err := url.Parse(urlString) + if err != nil { + return "", fmt.Errorf("invalid URL: %v", err) + } + + if parsedURL.Scheme == "" || parsedURL.Host == "" { + return "", fmt.Errorf("URL missing scheme or host") + } + + // Build origin (scheme + host) + origin := parsedURL.Scheme + "://" + parsedURL.Host + + return origin, nil +} diff --git a/v2/internal/frontend/runtime/desktop/draganddrop.js b/v2/internal/frontend/runtime/desktop/draganddrop.js new file mode 100644 index 000000000..e470e4823 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/draganddrop.js @@ -0,0 +1,276 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +/* jshint esversion: 9 */ + +import {EventsOn, EventsOff} from "./events"; + +const flags = { + registered: false, + defaultUseDropTarget: true, + useDropTarget: true, + nextDeactivate: null, + nextDeactivateTimeout: null, +}; + +const DROP_TARGET_ACTIVE = "wails-drop-target-active"; + +/** + * checkStyleDropTarget checks if the style has the drop target attribute + * + * @param {CSSStyleDeclaration} style + * @returns + */ +function checkStyleDropTarget(style) { + const cssDropValue = style.getPropertyValue(window.wails.flags.cssDropProperty).trim(); + if (cssDropValue) { + if (cssDropValue === window.wails.flags.cssDropValue) { + return true; + } + // if the element has the drop target attribute, but + // the value is not correct, terminate finding process. + // This can be useful to block some child elements from being drop targets. + return false; + } + return false; +} + +/** + * onDragOver is called when the dragover event is emitted. + * @param {DragEvent} e + * @returns + */ +function onDragOver(e) { + // Check if this is an external file drop or internal HTML drag + // External file drops will have "Files" in the types array + // Internal HTML drags typically have "text/plain", "text/html" or custom types + const isFileDrop = e.dataTransfer.types.includes("Files"); + + // Only handle external file drops, let internal HTML5 drag-and-drop work normally + if (!isFileDrop) { + return; + } + + // ALWAYS prevent default for file drops to stop browser navigation + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + + if (!flags.useDropTarget) { + return; + } + + const element = e.target; + + // Trigger debounce function to deactivate drop targets + if(flags.nextDeactivate) flags.nextDeactivate(); + + // if the element is null or element is not child of drop target element + if (!element || !checkStyleDropTarget(getComputedStyle(element))) { + return; + } + + let currentElement = element; + while (currentElement) { + // check if currentElement is drop target element + if (checkStyleDropTarget(getComputedStyle(currentElement))) { + currentElement.classList.add(DROP_TARGET_ACTIVE); + } + currentElement = currentElement.parentElement; + } +} + +/** + * onDragLeave is called when the dragleave event is emitted. + * @param {DragEvent} e + * @returns + */ +function onDragLeave(e) { + // Check if this is an external file drop or internal HTML drag + const isFileDrop = e.dataTransfer.types.includes("Files"); + + // Only handle external file drops, let internal HTML5 drag-and-drop work normally + if (!isFileDrop) { + return; + } + + // ALWAYS prevent default for file drops to stop browser navigation + e.preventDefault(); + + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + + if (!flags.useDropTarget) { + return; + } + + // Find the close drop target element + if (!e.target || !checkStyleDropTarget(getComputedStyle(e.target))) { + return null; + } + + // Trigger debounce function to deactivate drop targets + if(flags.nextDeactivate) flags.nextDeactivate(); + + // Use debounce technique to tacle dragleave events on overlapping elements and drop target elements + flags.nextDeactivate = () => { + // Deactivate all drop targets, new drop target will be activated on next dragover event + Array.from(document.getElementsByClassName(DROP_TARGET_ACTIVE)).forEach(el => el.classList.remove(DROP_TARGET_ACTIVE)); + // Reset nextDeactivate + flags.nextDeactivate = null; + // Clear timeout + if (flags.nextDeactivateTimeout) { + clearTimeout(flags.nextDeactivateTimeout); + flags.nextDeactivateTimeout = null; + } + } + + // Set timeout to deactivate drop targets if not triggered by next drag event + flags.nextDeactivateTimeout = setTimeout(() => { + if(flags.nextDeactivate) flags.nextDeactivate(); + }, 50); +} + +/** + * onDrop is called when the drop event is emitted. + * @param {DragEvent} e + * @returns + */ +function onDrop(e) { + // Check if this is an external file drop or internal HTML drag + const isFileDrop = e.dataTransfer.types.includes("Files"); + + // Only handle external file drops, let internal HTML5 drag-and-drop work normally + if (!isFileDrop) { + return; + } + + // ALWAYS prevent default for file drops to stop browser navigation + e.preventDefault(); + + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + + if (CanResolveFilePaths()) { + // process files + let files = []; + if (e.dataTransfer.items) { + files = [...e.dataTransfer.items].map((item, i) => { + if (item.kind === 'file') { + return item.getAsFile(); + } + }); + } else { + files = [...e.dataTransfer.files]; + } + window.runtime.ResolveFilePaths(e.x, e.y, files); + } + + if (!flags.useDropTarget) { + return; + } + + // Trigger debounce function to deactivate drop targets + if(flags.nextDeactivate) flags.nextDeactivate(); + + // Deactivate all drop targets + Array.from(document.getElementsByClassName(DROP_TARGET_ACTIVE)).forEach(el => el.classList.remove(DROP_TARGET_ACTIVE)); +} + +/** + * postMessageWithAdditionalObjects checks the browser's capability of sending postMessageWithAdditionalObjects + * + * @returns {boolean} + * @constructor + */ +export function CanResolveFilePaths() { + return window.chrome?.webview?.postMessageWithAdditionalObjects != null; +} + +/** + * ResolveFilePaths sends drop events to the GO side to resolve file paths on windows. + * + * @param {number} x + * @param {number} y + * @param {any[]} files + * @constructor + */ +export function ResolveFilePaths(x, y, files) { + // Only for windows webview2 >= 1.0.1774.30 + // https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2webmessagereceivedeventargs2?view=webview2-1.0.1823.32#applies-to + if (window.chrome?.webview?.postMessageWithAdditionalObjects) { + chrome.webview.postMessageWithAdditionalObjects(`file:drop:${x}:${y}`, files); + } +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + if (typeof callback !== "function") { + console.error("DragAndDropCallback is not a function"); + return; + } + + if (flags.registered) { + return; + } + flags.registered = true; + + const uDTPT = typeof useDropTarget; + flags.useDropTarget = uDTPT === "undefined" || uDTPT !== "boolean" ? flags.defaultUseDropTarget : useDropTarget; + window.addEventListener('dragover', onDragOver); + window.addEventListener('dragleave', onDragLeave); + window.addEventListener('drop', onDrop); + + let cb = callback; + if (flags.useDropTarget) { + cb = function (x, y, paths) { + const element = document.elementFromPoint(x, y) + // if the element is null or element is not child of drop target element, return null + if (!element || !checkStyleDropTarget(getComputedStyle(element))) { + return null; + } + callback(x, y, paths); + } + } + + EventsOn("wails:file-drop", cb); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + window.removeEventListener('dragover', onDragOver); + window.removeEventListener('dragleave', onDragLeave); + window.removeEventListener('drop', onDrop); + EventsOff("wails:file-drop"); + flags.registered = false; +} diff --git a/v2/internal/frontend/runtime/desktop/events.js b/v2/internal/frontend/runtime/desktop/events.js index 9548cbc34..e665a8aff 100644 --- a/v2/internal/frontend/runtime/desktop/events.js +++ b/v2/internal/frontend/runtime/desktop/events.js @@ -90,17 +90,17 @@ function notifyListeners(eventData) { // Get the event name let eventName = eventData.name; - // Check if we have any listeners for this event - if (eventListeners[eventName]) { + // Keep a list of listener indexes to destroy + const newEventListenerList = eventListeners[eventName]?.slice() || []; - // Keep a list of listener indexes to destroy - const newEventListenerList = eventListeners[eventName].slice(); + // Check if we have any listeners for this event + if (newEventListenerList.length) { // Iterate listeners - for (let count = eventListeners[eventName].length - 1; count >= 0; count -= 1) { + for (let count = newEventListenerList.length - 1; count >= 0; count -= 1) { // Get next listener - const listener = eventListeners[eventName][count]; + const listener = newEventListenerList[count]; let data = eventData.data; @@ -190,9 +190,9 @@ export function EventsOff(eventName, ...additionalEventNames) { */ export function EventsOffAll() { const eventNames = Object.keys(eventListeners); - for (let i = 0; i !== eventNames.length; i++) { - removeListener(eventNames[i]); - } + eventNames.forEach(eventName => { + removeListener(eventName) + }) } /** @@ -202,6 +202,8 @@ export function EventsOff(eventName, ...additionalEventNames) { */ function listenerOff(listener) { const eventName = listener.eventName; + if (eventListeners[eventName] === undefined) return; + // Remove local listener eventListeners[eventName] = eventListeners[eventName].filter(l => l !== listener); diff --git a/v2/internal/frontend/runtime/desktop/main.js b/v2/internal/frontend/runtime/desktop/main.js index 65d954d95..405d5f60d 100644 --- a/v2/internal/frontend/runtime/desktop/main.js +++ b/v2/internal/frontend/runtime/desktop/main.js @@ -9,15 +9,25 @@ The electron alternative for Go */ /* jshint esversion: 9 */ import * as Log from './log'; -import {eventListeners, EventsEmit, EventsNotify, EventsOff, EventsOn, EventsOnce, EventsOnMultiple} from './events'; -import {Call, Callback, callbacks} from './calls'; -import {SetBindings} from "./bindings"; +import { + eventListeners, + EventsEmit, + EventsNotify, + EventsOff, + EventsOffAll, + EventsOn, + EventsOnce, + EventsOnMultiple, +} from "./events"; +import { Call, Callback, callbacks } from './calls'; +import { SetBindings } from "./bindings"; import * as Window from "./window"; import * as Screen from "./screen"; import * as Browser from "./browser"; import * as Clipboard from "./clipboard"; +import * as DragAndDrop from "./draganddrop"; import * as ContextMenu from "./contextmenu"; - +import * as Notifications from "./notifications"; export function Quit() { window.WailsInvoke('Q'); @@ -42,11 +52,14 @@ window.runtime = { ...Browser, ...Screen, ...Clipboard, + ...DragAndDrop, + ...Notifications, EventsOn, EventsOnce, EventsOnMultiple, EventsEmit, EventsOff, + EventsOffAll, Environment, Show, Hide, @@ -70,6 +83,9 @@ window.wails = { deferDragToMouseMove: true, cssDragProperty: "--wails-draggable", cssDragValue: "drag", + cssDropProperty: "--wails-drop-target", + cssDropValue: "drop", + enableWailsDragAndDrop: false, } }; @@ -84,12 +100,12 @@ if (!DEBUG) { delete window.wailsbindings; } -let dragTest = function (e) { +let dragTest = function(e) { var val = window.getComputedStyle(e.target).getPropertyValue(window.wails.flags.cssDragProperty); if (val) { - val = val.trim(); + val = val.trim(); } - + if (val !== window.wails.flags.cssDragValue) { return false; } @@ -107,13 +123,17 @@ let dragTest = function (e) { return true; }; -window.wails.setCSSDragProperties = function (property, value) { +window.wails.setCSSDragProperties = function(property, value) { window.wails.flags.cssDragProperty = property; window.wails.flags.cssDragValue = value; } -window.addEventListener('mousedown', (e) => { +window.wails.setCSSDropProperties = function(property, value) { + window.wails.flags.cssDropProperty = property; + window.wails.flags.cssDropValue = value; +} +window.addEventListener('mousedown', (e) => { // Check for resizing if (window.wails.flags.resizeEdge) { window.WailsInvoke("resize:" + window.wails.flags.resizeEdge); @@ -149,7 +169,7 @@ function setResize(cursor) { window.wails.flags.resizeEdge = cursor; } -window.addEventListener('mousemove', function (e) { +window.addEventListener('mousemove', function(e) { if (window.wails.flags.shouldDrag) { window.wails.flags.shouldDrag = false; let mousePressed = e.buttons !== undefined ? e.buttons : e.which; @@ -187,7 +207,7 @@ window.addEventListener('mousemove', function (e) { }); // Setup context menu hook -window.addEventListener('contextmenu', function (e) { +window.addEventListener('contextmenu', function(e) { // always show the contextmenu in debug & dev if (DEBUG) return; diff --git a/v2/internal/frontend/runtime/desktop/notifications.js b/v2/internal/frontend/runtime/desktop/notifications.js new file mode 100644 index 000000000..25c01bb34 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/notifications.js @@ -0,0 +1,200 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 9 */ + +import {Call} from "./calls"; + +/** + * Initialize the notification service for the application. + * This must be called before sending any notifications. + * On macOS, this also ensures the notification delegate is properly initialized. + * + * @export + * @return {Promise} + */ +export function InitializeNotifications() { + return Call(":wails:InitializeNotifications"); +} + +/** + * Clean up notification resources and release any held connections. + * This should be called when shutting down the application to properly release resources + * (primarily needed on Linux to close D-Bus connections). + * + * @export + * @return {Promise} + */ +export function CleanupNotifications() { + return Call(":wails:CleanupNotifications"); +} + +/** + * Check if notifications are available on the current platform. + * + * @export + * @return {Promise} True if notifications are available, false otherwise + */ +export function IsNotificationAvailable() { + return Call(":wails:IsNotificationAvailable"); +} + +/** + * Request notification authorization from the user. + * On macOS, this prompts the user to allow notifications. + * On other platforms, this always returns true. + * + * @export + * @return {Promise} True if authorization was granted, false otherwise + */ +export function RequestNotificationAuthorization() { + return Call(":wails:RequestNotificationAuthorization"); +} + +/** + * Check the current notification authorization status. + * On macOS, this checks if the app has notification permissions. + * On other platforms, this always returns true. + * + * @export + * @return {Promise} True if authorized, false otherwise + */ +export function CheckNotificationAuthorization() { + return Call(":wails:CheckNotificationAuthorization"); +} + +/** + * Send a basic notification with the given options. + * The notification will display with the provided title, subtitle (if supported), and body text. + * + * @export + * @param {Object} options - Notification options + * @param {string} options.id - Unique identifier for the notification + * @param {string} options.title - Notification title + * @param {string} [options.subtitle] - Notification subtitle (macOS and Linux only) + * @param {string} [options.body] - Notification body text + * @param {string} [options.categoryId] - Category ID for action buttons (requires SendNotificationWithActions) + * @param {Object} [options.data] - Additional user data to attach to the notification + * @return {Promise} + */ +export function SendNotification(options) { + return Call(":wails:SendNotification", [options]); +} + +/** + * Send a notification with action buttons. + * A NotificationCategory must be registered first using RegisterNotificationCategory. + * The options.categoryId must match a previously registered category ID. + * If the category is not found, a basic notification will be sent instead. + * + * @export + * @param {Object} options - Notification options + * @param {string} options.id - Unique identifier for the notification + * @param {string} options.title - Notification title + * @param {string} [options.subtitle] - Notification subtitle (macOS and Linux only) + * @param {string} [options.body] - Notification body text + * @param {string} options.categoryId - Category ID that matches a registered category + * @param {Object} [options.data] - Additional user data to attach to the notification + * @return {Promise} + */ +export function SendNotificationWithActions(options) { + return Call(":wails:SendNotificationWithActions", [options]); +} + +/** + * Register a notification category that can be used with SendNotificationWithActions. + * Categories define the action buttons and optional reply fields that will appear on notifications. + * Registering a category with the same ID as a previously registered category will override it. + * + * @export + * @param {Object} category - Notification category definition + * @param {string} category.id - Unique identifier for the category + * @param {Array} [category.actions] - Array of action buttons + * @param {string} category.actions[].id - Unique identifier for the action + * @param {string} category.actions[].title - Display title for the action button + * @param {boolean} [category.actions[].destructive] - Whether the action is destructive (macOS-specific) + * @param {boolean} [category.hasReplyField] - Whether to include a text input field for replies + * @param {string} [category.replyPlaceholder] - Placeholder text for the reply field (required if hasReplyField is true) + * @param {string} [category.replyButtonTitle] - Title for the reply button (required if hasReplyField is true) + * @return {Promise} + */ +export function RegisterNotificationCategory(category) { + return Call(":wails:RegisterNotificationCategory", [category]); +} + +/** + * Remove a previously registered notification category. + * + * @export + * @param {string} categoryId - The ID of the category to remove + * @return {Promise} + */ +export function RemoveNotificationCategory(categoryId) { + return Call(":wails:RemoveNotificationCategory", [categoryId]); +} + +/** + * Remove all pending notifications from the notification center. + * On Windows, this is a no-op as the platform manages notification lifecycle automatically. + * + * @export + * @return {Promise} + */ +export function RemoveAllPendingNotifications() { + return Call(":wails:RemoveAllPendingNotifications"); +} + +/** + * Remove a specific pending notification by its identifier. + * On Windows, this is a no-op as the platform manages notification lifecycle automatically. + * + * @export + * @param {string} identifier - The ID of the notification to remove + * @return {Promise} + */ +export function RemovePendingNotification(identifier) { + return Call(":wails:RemovePendingNotification", [identifier]); +} + +/** + * Remove all delivered notifications from the notification center. + * On Windows, this is a no-op as the platform manages notification lifecycle automatically. + * + * @export + * @return {Promise} + */ +export function RemoveAllDeliveredNotifications() { + return Call(":wails:RemoveAllDeliveredNotifications"); +} + +/** + * Remove a specific delivered notification by its identifier. + * On Windows, this is a no-op as the platform manages notification lifecycle automatically. + * + * @export + * @param {string} identifier - The ID of the notification to remove + * @return {Promise} + */ +export function RemoveDeliveredNotification(identifier) { + return Call(":wails:RemoveDeliveredNotification", [identifier]); +} + +/** + * Remove a notification by its identifier. + * This is a convenience function that works across platforms. + * On macOS, use the more specific RemovePendingNotification or RemoveDeliveredNotification functions. + * + * @export + * @param {string} identifier - The ID of the notification to remove + * @return {Promise} + */ +export function RemoveNotification(identifier) { + return Call(":wails:RemoveNotification", [identifier]); +} + diff --git a/v2/internal/frontend/runtime/dev/main.js b/v2/internal/frontend/runtime/dev/main.js index e6e05be54..c7f31b0f2 100644 --- a/v2/internal/frontend/runtime/dev/main.js +++ b/v2/internal/frontend/runtime/dev/main.js @@ -80,7 +80,7 @@ function handleDisconnect() { function _connect() { if (websocket == null) { - websocket = new WebSocket('ws://' + window.location.host + '/wails/ipc'); + websocket = new WebSocket((window.location.protocol.startsWith("https") ? "wss://" : "ws://") + window.location.host + "/wails/ipc"); websocket.onopen = handleConnect; websocket.onerror = function (e) { e.stopImmediatePropagation(); diff --git a/v2/internal/frontend/runtime/events.go b/v2/internal/frontend/runtime/events.go index ac9d6299c..1f2e0a6e4 100644 --- a/v2/internal/frontend/runtime/events.go +++ b/v2/internal/frontend/runtime/events.go @@ -143,7 +143,7 @@ func (e *Events) notifyBackend(eventName string, data ...interface{}) { } // Do we have items to delete? - if itemsToDelete == true { + if itemsToDelete { // Create a new Listeners slice var newListeners []*eventListener diff --git a/v2/internal/frontend/runtime/ipc_websocket.js b/v2/internal/frontend/runtime/ipc_websocket.js index d5dca66af..a0d6b4a70 100644 --- a/v2/internal/frontend/runtime/ipc_websocket.js +++ b/v2/internal/frontend/runtime/ipc_websocket.js @@ -1,10 +1,10 @@ -(()=>{function D(t){console.log("%c wails dev %c "+t+" ","background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem","background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem")}function p(){}var A=t=>t;function N(t){return t()}function it(){return Object.create(null)}function b(t){t.forEach(N)}function w(t){return typeof t=="function"}function L(t,e){return t!=t?e==e:t!==e||t&&typeof t=="object"||typeof t=="function"}function ot(t){return Object.keys(t).length===0}function rt(t,...e){if(t==null)return p;let n=t.subscribe(...e);return n.unsubscribe?()=>n.unsubscribe():n}function st(t,e,n){t.$$.on_destroy.push(rt(e,n))}var ct=typeof window!="undefined",Ot=ct?()=>window.performance.now():()=>Date.now(),P=ct?t=>requestAnimationFrame(t):p;var x=new Set;function lt(t){x.forEach(e=>{e.c(t)||(x.delete(e),e.f())}),x.size!==0&&P(lt)}function Dt(t){let e;return x.size===0&&P(lt),{promise:new Promise(n=>{x.add(e={c:t,f:n})}),abort(){x.delete(e)}}}var ut=!1;function At(){ut=!0}function Lt(){ut=!1}function Bt(t,e){t.appendChild(e)}function at(t,e,n){let i=R(t);if(!i.getElementById(e)){let o=B("style");o.id=e,o.textContent=n,ft(i,o)}}function R(t){if(!t)return document;let e=t.getRootNode?t.getRootNode():t.ownerDocument;return e&&e.host?e:t.ownerDocument}function Tt(t){let e=B("style");return ft(R(t),e),e.sheet}function ft(t,e){return Bt(t.head||t,e),e.sheet}function W(t,e,n){t.insertBefore(e,n||null)}function S(t){t.parentNode.removeChild(t)}function B(t){return document.createElement(t)}function Jt(t){return document.createTextNode(t)}function dt(){return Jt("")}function ht(t,e,n){n==null?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function zt(t){return Array.from(t.childNodes)}function Ht(t,e,{bubbles:n=!1,cancelable:i=!1}={}){let o=document.createEvent("CustomEvent");return o.initCustomEvent(t,n,i,e),o}var T=new Map,J=0;function Gt(t){let e=5381,n=t.length;for(;n--;)e=(e<<5)-e^t.charCodeAt(n);return e>>>0}function qt(t,e){let n={stylesheet:Tt(e),rules:{}};return T.set(t,n),n}function _t(t,e,n,i,o,c,s,l=0){let f=16.666/i,r=`{ +(()=>{function D(t){console.log("%c wails dev %c "+t+" ","background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem","background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem")}function _(){}var A=t=>t;function N(t){return t()}function it(){return Object.create(null)}function b(t){t.forEach(N)}function w(t){return typeof t=="function"}function L(t,e){return t!=t?e==e:t!==e||t&&typeof t=="object"||typeof t=="function"}function ot(t){return Object.keys(t).length===0}function rt(t,...e){if(t==null)return _;let n=t.subscribe(...e);return n.unsubscribe?()=>n.unsubscribe():n}function st(t,e,n){t.$$.on_destroy.push(rt(e,n))}var ct=typeof window!="undefined",Ot=ct?()=>window.performance.now():()=>Date.now(),P=ct?t=>requestAnimationFrame(t):_;var x=new Set;function lt(t){x.forEach(e=>{e.c(t)||(x.delete(e),e.f())}),x.size!==0&&P(lt)}function Dt(t){let e;return x.size===0&&P(lt),{promise:new Promise(n=>{x.add(e={c:t,f:n})}),abort(){x.delete(e)}}}var ut=!1;function At(){ut=!0}function Lt(){ut=!1}function Bt(t,e){t.appendChild(e)}function at(t,e,n){let i=R(t);if(!i.getElementById(e)){let o=B("style");o.id=e,o.textContent=n,ft(i,o)}}function R(t){if(!t)return document;let e=t.getRootNode?t.getRootNode():t.ownerDocument;return e&&e.host?e:t.ownerDocument}function Tt(t){let e=B("style");return ft(R(t),e),e.sheet}function ft(t,e){return Bt(t.head||t,e),e.sheet}function W(t,e,n){t.insertBefore(e,n||null)}function S(t){t.parentNode.removeChild(t)}function B(t){return document.createElement(t)}function Jt(t){return document.createTextNode(t)}function dt(){return Jt("")}function ht(t,e,n){n==null?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function zt(t){return Array.from(t.childNodes)}function Ht(t,e,{bubbles:n=!1,cancelable:i=!1}={}){let o=document.createEvent("CustomEvent");return o.initCustomEvent(t,n,i,e),o}var T=new Map,J=0;function Gt(t){let e=5381,n=t.length;for(;n--;)e=(e<<5)-e^t.charCodeAt(n);return e>>>0}function qt(t,e){let n={stylesheet:Tt(e),rules:{}};return T.set(t,n),n}function pt(t,e,n,i,o,c,s,l=0){let f=16.666/i,r=`{ `;for(let g=0;g<=1;g+=f){let F=e+(n-e)*c(g);r+=g*100+`%{${s(F,1-F)}} `}let y=r+`100% {${s(n,1-n)}} -}`,a=`__svelte_${Gt(y)}_${l}`,u=R(t),{stylesheet:h,rules:_}=T.get(u)||qt(u,t);_[a]||(_[a]=!0,h.insertRule(`@keyframes ${a} ${y}`,h.cssRules.length));let v=t.style.animation||"";return t.style.animation=`${v?`${v}, `:""}${a} ${i}ms linear ${o}ms 1 both`,J+=1,a}function Kt(t,e){let n=(t.style.animation||"").split(", "),i=n.filter(e?c=>c.indexOf(e)<0:c=>c.indexOf("__svelte")===-1),o=n.length-i.length;o&&(t.style.animation=i.join(", "),J-=o,J||Nt())}function Nt(){P(()=>{J||(T.forEach(t=>{let{ownerNode:e}=t.stylesheet;e&&S(e)}),T.clear())})}var V;function C(t){V=t}var k=[];var pt=[],z=[],mt=[],Pt=Promise.resolve(),U=!1;function Rt(){U||(U=!0,Pt.then(yt))}function $(t){z.push(t)}var X=new Set,H=0;function yt(){let t=V;do{for(;H{E=null})),E}function Z(t,e,n){t.dispatchEvent(Ht(`${e?"intro":"outro"}${n}`))}var G=new Set,m;function gt(){m={r:0,c:[],p:m}}function bt(){m.r||b(m.c),m=m.p}function I(t,e){t&&t.i&&(G.delete(t),t.i(e))}function Q(t,e,n,i){if(t&&t.o){if(G.has(t))return;G.add(t),m.c.push(()=>{G.delete(t),i&&(n&&t.d(1),i())}),t.o(e)}else i&&i()}var Ut={duration:0};function Y(t,e,n,i){let o=e(t,n),c=i?0:1,s=null,l=null,f=null;function r(){f&&Kt(t,f)}function y(u,h){let _=u.b-c;return h*=Math.abs(_),{a:c,b:u.b,d:_,duration:h,start:u.start,end:u.start+h,group:u.group}}function a(u){let{delay:h=0,duration:_=300,easing:v=A,tick:g=p,css:F}=o||Ut,K={start:Ot()+h,b:u};u||(K.group=m,m.r+=1),s||l?l=K:(F&&(r(),f=_t(t,c,u,_,h,v,F)),u&&g(0,1),s=y(K,_),$(()=>Z(t,u,"start")),Dt(O=>{if(l&&O>l.start&&(s=y(l,_),l=null,Z(t,s.b,"start"),F&&(r(),f=_t(t,c,s.b,s.duration,0,v,o.css))),s){if(O>=s.end)g(c=s.b,1-c),Z(t,s.b,"end"),l||(s.b?r():--s.group.r||b(s.group.c)),s=null;else if(O>=s.start){let jt=O-s.start;c=s.a+s.d*v(jt/s.duration),g(c,1-c)}}return!!(s||l)}))}return{run(u){w(o)?Vt().then(()=>{o=o(),a(u)}):a(u)},end(){r(),s=l=null}}}var le=typeof window!="undefined"?window:typeof globalThis!="undefined"?globalThis:global;var ue=new Set(["allowfullscreen","allowpaymentrequest","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]);function Xt(t,e,n,i){let{fragment:o,after_update:c}=t.$$;o&&o.m(e,n),i||$(()=>{let s=t.$$.on_mount.map(N).filter(w);t.$$.on_destroy?t.$$.on_destroy.push(...s):b(s),t.$$.on_mount=[]}),c.forEach($)}function wt(t,e){let n=t.$$;n.fragment!==null&&(b(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function Zt(t,e){t.$$.dirty[0]===-1&&(k.push(t),Rt(),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{let _=h.length?h[0]:u;return r.ctx&&o(r.ctx[a],r.ctx[a]=_)&&(!r.skip_bound&&r.bound[a]&&r.bound[a](_),y&&Zt(t,a)),u}):[],r.update(),y=!0,b(r.before_update),r.fragment=i?i(r.ctx):!1,e.target){if(e.hydrate){At();let a=zt(e.target);r.fragment&&r.fragment.l(a),a.forEach(S)}else r.fragment&&r.fragment.c();e.intro&&I(t.$$.fragment),Xt(t,e.target,e.anchor,e.customElement),Lt(),yt()}C(f)}var Qt;typeof HTMLElement=="function"&&(Qt=class extends HTMLElement{constructor(){super();this.attachShadow({mode:"open"})}connectedCallback(){let{on_mount:t}=this.$$;this.$$.on_disconnect=t.map(N).filter(w);for(let e in this.$$.slotted)this.appendChild(this.$$.slotted[e])}attributeChangedCallback(t,e,n){this[t]=n}disconnectedCallback(){b(this.$$.on_disconnect)}$destroy(){wt(this,1),this.$destroy=p}$on(t,e){if(!w(e))return p;let n=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return n.push(e),()=>{let i=n.indexOf(e);i!==-1&&n.splice(i,1)}}$set(t){this.$$set&&!ot(t)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}});var tt=class{$destroy(){wt(this,1),this.$destroy=p}$on(e,n){if(!w(n))return p;let i=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return i.push(n),()=>{let o=i.indexOf(n);o!==-1&&i.splice(o,1)}}$set(e){this.$$set&&!ot(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}};var M=[];function Ft(t,e=p){let n,i=new Set;function o(l){if(L(t,l)&&(t=l,n)){let f=!M.length;for(let r of i)r[1](),M.push(r,t);if(f){for(let r=0;r{i.delete(r),i.size===0&&(n(),n=null)}}return{set:o,update:c,subscribe:s}}var q=Ft(!1);function xt(){q.set(!0)}function $t(){q.set(!1)}function et(t,{delay:e=0,duration:n=400,easing:i=A}={}){let o=+getComputedStyle(t).opacity;return{delay:e,duration:n,easing:i,css:c=>`opacity: ${c*o}`}}function Yt(t){at(t,"svelte-181h7z",`.wails-reconnect-overlay.svelte-181h7z{position:fixed;top:0;left:0;width:100%;height:100%;backdrop-filter:blur(2px) saturate(0%) contrast(50%) brightness(25%);z-index:999999 +}`,a=`__svelte_${Gt(y)}_${l}`,u=R(t),{stylesheet:h,rules:p}=T.get(u)||qt(u,t);p[a]||(p[a]=!0,h.insertRule(`@keyframes ${a} ${y}`,h.cssRules.length));let v=t.style.animation||"";return t.style.animation=`${v?`${v}, `:""}${a} ${i}ms linear ${o}ms 1 both`,J+=1,a}function Kt(t,e){let n=(t.style.animation||"").split(", "),i=n.filter(e?c=>c.indexOf(e)<0:c=>c.indexOf("__svelte")===-1),o=n.length-i.length;o&&(t.style.animation=i.join(", "),J-=o,J||Nt())}function Nt(){P(()=>{J||(T.forEach(t=>{let{ownerNode:e}=t.stylesheet;e&&S(e)}),T.clear())})}var V;function C(t){V=t}var k=[];var _t=[],z=[],mt=[],Pt=Promise.resolve(),U=!1;function Rt(){U||(U=!0,Pt.then(yt))}function $(t){z.push(t)}var X=new Set,H=0;function yt(){let t=V;do{for(;H{E=null})),E}function Z(t,e,n){t.dispatchEvent(Ht(`${e?"intro":"outro"}${n}`))}var G=new Set,m;function gt(){m={r:0,c:[],p:m}}function bt(){m.r||b(m.c),m=m.p}function I(t,e){t&&t.i&&(G.delete(t),t.i(e))}function Q(t,e,n,i){if(t&&t.o){if(G.has(t))return;G.add(t),m.c.push(()=>{G.delete(t),i&&(n&&t.d(1),i())}),t.o(e)}else i&&i()}var Ut={duration:0};function Y(t,e,n,i){let o=e(t,n),c=i?0:1,s=null,l=null,f=null;function r(){f&&Kt(t,f)}function y(u,h){let p=u.b-c;return h*=Math.abs(p),{a:c,b:u.b,d:p,duration:h,start:u.start,end:u.start+h,group:u.group}}function a(u){let{delay:h=0,duration:p=300,easing:v=A,tick:g=_,css:F}=o||Ut,K={start:Ot()+h,b:u};u||(K.group=m,m.r+=1),s||l?l=K:(F&&(r(),f=pt(t,c,u,p,h,v,F)),u&&g(0,1),s=y(K,p),$(()=>Z(t,u,"start")),Dt(O=>{if(l&&O>l.start&&(s=y(l,p),l=null,Z(t,s.b,"start"),F&&(r(),f=pt(t,c,s.b,s.duration,0,v,o.css))),s){if(O>=s.end)g(c=s.b,1-c),Z(t,s.b,"end"),l||(s.b?r():--s.group.r||b(s.group.c)),s=null;else if(O>=s.start){let jt=O-s.start;c=s.a+s.d*v(jt/s.duration),g(c,1-c)}}return!!(s||l)}))}return{run(u){w(o)?Vt().then(()=>{o=o(),a(u)}):a(u)},end(){r(),s=l=null}}}var le=typeof window!="undefined"?window:typeof globalThis!="undefined"?globalThis:global;var ue=new Set(["allowfullscreen","allowpaymentrequest","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]);function Xt(t,e,n,i){let{fragment:o,after_update:c}=t.$$;o&&o.m(e,n),i||$(()=>{let s=t.$$.on_mount.map(N).filter(w);t.$$.on_destroy?t.$$.on_destroy.push(...s):b(s),t.$$.on_mount=[]}),c.forEach($)}function wt(t,e){let n=t.$$;n.fragment!==null&&(b(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function Zt(t,e){t.$$.dirty[0]===-1&&(k.push(t),Rt(),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{let p=h.length?h[0]:u;return r.ctx&&o(r.ctx[a],r.ctx[a]=p)&&(!r.skip_bound&&r.bound[a]&&r.bound[a](p),y&&Zt(t,a)),u}):[],r.update(),y=!0,b(r.before_update),r.fragment=i?i(r.ctx):!1,e.target){if(e.hydrate){At();let a=zt(e.target);r.fragment&&r.fragment.l(a),a.forEach(S)}else r.fragment&&r.fragment.c();e.intro&&I(t.$$.fragment),Xt(t,e.target,e.anchor,e.customElement),Lt(),yt()}C(f)}var Qt;typeof HTMLElement=="function"&&(Qt=class extends HTMLElement{constructor(){super();this.attachShadow({mode:"open"})}connectedCallback(){let{on_mount:t}=this.$$;this.$$.on_disconnect=t.map(N).filter(w);for(let e in this.$$.slotted)this.appendChild(this.$$.slotted[e])}attributeChangedCallback(t,e,n){this[t]=n}disconnectedCallback(){b(this.$$.on_disconnect)}$destroy(){wt(this,1),this.$destroy=_}$on(t,e){if(!w(e))return _;let n=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return n.push(e),()=>{let i=n.indexOf(e);i!==-1&&n.splice(i,1)}}$set(t){this.$$set&&!ot(t)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}});var tt=class{$destroy(){wt(this,1),this.$destroy=_}$on(e,n){if(!w(n))return _;let i=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return i.push(n),()=>{let o=i.indexOf(n);o!==-1&&i.splice(o,1)}}$set(e){this.$$set&&!ot(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}};var M=[];function Ft(t,e=_){let n,i=new Set;function o(l){if(L(t,l)&&(t=l,n)){let f=!M.length;for(let r of i)r[1](),M.push(r,t);if(f){for(let r=0;r{i.delete(r),i.size===0&&(n(),n=null)}}return{set:o,update:c,subscribe:s}}var q=Ft(!1);function xt(){q.set(!0)}function $t(){q.set(!1)}function et(t,{delay:e=0,duration:n=400,easing:i=A}={}){let o=+getComputedStyle(t).opacity;return{delay:e,duration:n,easing:i,css:c=>`opacity: ${c*o}`}}function Yt(t){at(t,"svelte-181h7z",`.wails-reconnect-overlay.svelte-181h7z{position:fixed;top:0;left:0;width:100%;height:100%;backdrop-filter:blur(2px) saturate(0%) contrast(50%) brightness(25%);z-index:999999 }.wails-reconnect-overlay-content.svelte-181h7z{position:relative;top:50%;transform:translateY(-50%);margin:0;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA7CAMAAAAEsocZAAAC91BMVEUAAACzQ0PjMjLkMjLZLS7XLS+vJCjkMjKlEx6uGyHjMDGiFx7GJyrAISjUKy3mMzPlMjLjMzOsGyDKJirkMjK6HyXmMjLgMDC6IiLcMjLULC3MJyrRKSy+IibmMzPmMjK7ISXlMjLIJimzHSLkMjKtGiHZLC7BIifgMDCpGSDFIivcLy+yHSKoGR+eFBzNKCvlMjKxHSPkMTKxHSLmMjLKJyq5ICXDJCe6ISXdLzDkMjLmMzPFJSm2HyTlMTLhMDGyHSKUEBmhFx24HyTCJCjHJijjMzOiFh7mMjJ6BhDaLDCuGyOKABjnMzPGJinJJiquHCGEChSmGB/pMzOiFh7VKy3OKCu1HiSvHCLjMTLMKCrBIyeICxWxHCLDIyjSKizBIyh+CBO9ISa6ISWDChS9Iie1HyXVLC7FJSrLKCrlMjLiMTGPDhicFRywGyKXFBuhFx1/BxO7IiXkMTGeFBx8BxLkMTGnGR/GJCi4ICWsGyGJDxXSLS2yGiHSKi3CJCfnMzPQKiyECRTKJiq6ISWUERq/Iye0HiPDJCjGJSm6ICaPDxiTEBrdLy+3HyXSKiy0HyOQEBi4ICWhFh1+CBO9IieODhfSKyzWLC2LDhh8BxHKKCq7ISWaFBzkMzPqNDTTLC3EJSiHDBacExyvGyO1HyTPKCy+IieoGSC7ISaVEhrMKCvQKyusGyG0HiKACBPIJSq/JCaABxR5BRLEJCnkMzPJJinEJimPDRZ2BRKqHx/jMjLnMzPgMDHULC3NKSvQKSzsNDTWLS7SKyy3HyTKJyrDJSjbLzDYLC6mGB/GJSnVLC61HiPLKCrHJSm/Iye8Iia6ICWzHSKxHCLaLi/PKSupGR+7ICXpMzPbLi/IJinJJSmsGyGrGiCkFx6PDheJCxaFChXBIyfAIieSDxmBCBPlMjLeLzDdLzC5HySMDRe+ISWvGyGcFBzSKSzPJyvMJyrEJCjDIyefFRyWERriMDHUKiy/ISaZExv0NjbwNTXuNDTrMzMI0c+yAAAAu3RSTlMAA8HR/gwGgAj+MEpGCsC+hGpjQjYnIxgWBfzx7urizMrFqqB1bF83KhsR/fz8+/r5+fXv7unZ1tC+t6mmopqKdW1nYVpVRjUeHhIQBPr59/b28/Hx8ODg3NvUw8O/vKeim5aNioiDgn1vZWNjX1xUU1JPTUVFPT08Mi4qJyIh/Pv7+/n4+Pf39fT08/Du7efn5uXj4uHa19XNwsG/vrq2tbSuramlnpyYkpGNiIZ+enRraGVjVVBKOzghdjzRsAAABJVJREFUWMPtllVQG1EYhTc0ASpoobS0FCulUHd3oUjd3d3d3d3d3d2b7CYhnkBCCHGDEIK7Vh56d0NpOgwkYfLQzvA9ZrLfnPvfc+8uVEst/yheBJup3Nya2MjU6pa/jWLZtxjXpZFtVB4uVNI6m5gIruNkVFebqIb5Ug2ym4TIEM/gtUOGbg613oBzjAzZFrZ+lXu/3TIiMXXS5M6HTvrNHeLpZLEh6suGNW9fzZ9zd/qVi2eOHygqi5cDE5GUrJocONgzyqo0UXNSUlKSEhMztFqtXq9vNxImAmS3g7Y6QlbjdBWVGW36jt4wDGTUXjUsafh5zJWRkdFuZGtWGnCRmg+HasiGMUClTTzW0ZuVgLlGDIPM4Lhi0IrVq+tv2hS21fNrSONQgpM9DsJ4t3fM9PkvJuKj2ZjrZwvILKvaSTgciUSirjt6dOfOpyd169bDb9rMOwF9Hj4OD100gY0YXYb299bjzMrqj9doNByJWlVXFB9DT5dmJuvy+cq83JyuS6ayEYSHulKL8dmFnBkrCeZlHKMrC5XRhXGCZB2Ty1fkleRQaMCFT2DBsEafzRFJu7/2MicbKynPhQUDLiZwMWLJZKNLzoLbJBYVcurSmbmn+rcyJ8vCMgmlmaW6gnwun/+3C96VpAUuET1ZgRR36r2xWlnYSnf3oKABA14uXDDvydxHs6cpTV1p3hlJ2rJCiUjIZCByItXg8sHJijuvT64CuMTABUYvb6NN1Jdp1PH7D7f3bo2eS5KvW4RJr7atWT5w4MBBg9zdBw9+37BS7QIoFS5WnIaj12dr1DEXFgdvr4fh4eFl+u/wz8uf3jjHic8s4DL2Dal0IANyUBeCRCcwOBJV26JsjSpGwHVuSai69jvqD+jr56OgtKy0zAAK5mLTVBKVKL5tNthGAR9JneJQ/bFsHNzy+U7IlCYROxtMpIjR0ceoQVnowracLLpAQWETqV361bPoFo3cEbz2zYLZM7t3HWXcxmiBOgttS1ycWkTXMWh4mGigdug9DFdttqCFgTN6nD0q1XEVSoCxEjyFCi2eNC6Z69MRVIImJ6JQSf5gcFVCuF+aDhCa1F6MJFDaiNBQAh2TMfWBjhmLsAxUjG/fmjs0qjJck8D0GPBcuUuZW1LS/tIsPzqmQt17PvZQknlwnf4tHDBc+7t5VV3QQCkdc+Ur8/hdrz0but0RCumWiYbiKmLJ7EVbRomj4Q7+y5wsaXvfTGFpQcHB7n2WbG4MGdniw2Tm8xl5Yhr7MrSYHQ3uampz10aWyHyuzxvqaW/6W4MjXAUD3QV2aw97ZxhGjxCohYf5TpTHMXU1BbsAuoFnkRygVieIGAbqiF7rrH4rfWpKJouBCtyHJF8ctEyGubBa+C6NsMYEUonJFITHZqWBxXUA12Dv76Tf/PgOBmeNiiLG1pcKo1HAq8jLpY4JU1yWEixVNaOgoRJAKBSZHTZTU+wJOMtUDZvlVITC6FTlksyrEBoPHXpxxbzdaqzigUtVDkJVIOtVQ9UEOR4VGUh/kHWq0edJ6CxnZ+eePXva2bnY/cF/I1RLLf8vvwDANdMSMegxcAAAAABJRU5ErkJggg==);background-repeat:no-repeat;background-position:center }.wails-reconnect-overlay-loadingspinner.svelte-181h7z{pointer-events:none;width:2.5em;height:2.5em;border:.4em solid transparent;border-color:#f00 #eee0 #f00 #eee0;border-radius:50%;animation:svelte-181h7z-loadingspin 1s linear infinite;margin:auto;padding:2.5em - }@keyframes svelte-181h7z-loadingspin{100%{transform:rotate(360deg)}}`)}function Mt(t){let e,n,i;return{c(){e=B("div"),e.innerHTML='
',ht(e,"class","wails-reconnect-overlay svelte-181h7z")},m(o,c){W(o,e,c),i=!0},i(o){i||($(()=>{n||(n=Y(e,et,{duration:300},!0)),n.run(1)}),i=!0)},o(o){n||(n=Y(e,et,{duration:300},!1)),n.run(0),i=!1},d(o){o&&S(e),o&&n&&n.end()}}}function te(t){let e,n,i=t[0]&&Mt(t);return{c(){i&&i.c(),e=dt()},m(o,c){i&&i.m(o,c),W(o,e,c),n=!0},p(o,[c]){o[0]?i?c&1&&I(i,1):(i=Mt(o),i.c(),I(i,1),i.m(e.parentNode,e)):i&&(gt(),Q(i,1,1,()=>{i=null}),bt())},i(o){n||(I(i),n=!0)},o(o){Q(i),n=!1},d(o){i&&i.d(o),o&&S(e)}}}function ee(t,e,n){let i;return st(t,q,o=>n(0,i=o)),[i]}var St=class extends tt{constructor(e){super();vt(this,e,ee,te,L,{},Yt)}},Ct=St;var ne={},nt=null,j=[];window.WailsInvoke=t=>{if(!nt){console.log("Queueing: "+t),j.push(t);return}nt(t)};window.addEventListener("DOMContentLoaded",()=>{ne.overlay=new Ct({target:document.body,anchor:document.querySelector("#wails-spinner")})});var d=null,kt;window.onbeforeunload=function(){d&&(d.onclose=function(){},d.close(),d=null)};It();function ie(){nt=t=>{d.send(t)};for(let t=0;t
',ht(e,"class","wails-reconnect-overlay svelte-181h7z")},m(o,c){W(o,e,c),i=!0},i(o){i||($(()=>{n||(n=Y(e,et,{duration:300},!0)),n.run(1)}),i=!0)},o(o){n||(n=Y(e,et,{duration:300},!1)),n.run(0),i=!1},d(o){o&&S(e),o&&n&&n.end()}}}function te(t){let e,n,i=t[0]&&Mt(t);return{c(){i&&i.c(),e=dt()},m(o,c){i&&i.m(o,c),W(o,e,c),n=!0},p(o,[c]){o[0]?i?c&1&&I(i,1):(i=Mt(o),i.c(),I(i,1),i.m(e.parentNode,e)):i&&(gt(),Q(i,1,1,()=>{i=null}),bt())},i(o){n||(I(i),n=!0)},o(o){Q(i),n=!1},d(o){i&&i.d(o),o&&S(e)}}}function ee(t,e,n){let i;return st(t,q,o=>n(0,i=o)),[i]}var St=class extends tt{constructor(e){super();vt(this,e,ee,te,L,{},Yt)}},Ct=St;var ne={},nt=null,j=[];window.WailsInvoke=t=>{if(!nt){console.log("Queueing: "+t),j.push(t);return}nt(t)};window.addEventListener("DOMContentLoaded",()=>{ne.overlay=new Ct({target:document.body,anchor:document.querySelector("#wails-spinner")})});var d=null,kt;window.onbeforeunload=function(){d&&(d.onclose=function(){},d.close(),d=null)};It();function ie(){nt=t=>{d.send(t)};for(let t=0;t= 14", "less": "*", "sass": "*", "stylus": "*", + "sugarss": "*", "terser": "^5.4.0" }, "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, "less": { "optional": true }, @@ -1890,6 +1905,9 @@ "stylus": { "optional": true }, + "sugarss": { + "optional": true + }, "terser": { "optional": true } @@ -2518,9 +2536,9 @@ } }, "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "optional": true }, @@ -2829,9 +2847,9 @@ "dev": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true }, "nice-try": { @@ -2964,12 +2982,12 @@ "dev": true }, "postcss": { - "version": "8.4.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", - "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -3036,9 +3054,9 @@ } }, "rollup": { - "version": "2.78.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.1.tgz", - "integrity": "sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==", + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -3057,9 +3075,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "shebang-command": { @@ -3320,16 +3338,16 @@ } }, "vite": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.1.8.tgz", - "integrity": "sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==", + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz", + "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==", "dev": true, "requires": { "esbuild": "^0.15.9", "fsevents": "~2.3.2", - "postcss": "^8.4.16", + "postcss": "^8.4.18", "resolve": "^1.22.1", - "rollup": "~2.78.0" + "rollup": "^2.79.1" } }, "vitest": { diff --git a/v2/internal/frontend/runtime/runtime_debug_desktop.js b/v2/internal/frontend/runtime/runtime_debug_desktop.js index e680df85d..e646ed532 100644 --- a/v2/internal/frontend/runtime/runtime_debug_desktop.js +++ b/v2/internal/frontend/runtime/runtime_debug_desktop.js @@ -83,10 +83,10 @@ } function notifyListeners(eventData) { let eventName = eventData.name; - if (eventListeners[eventName]) { - const newEventListenerList = eventListeners[eventName].slice(); - for (let count = eventListeners[eventName].length - 1; count >= 0; count -= 1) { - const listener = eventListeners[eventName][count]; + const newEventListenerList = eventListeners[eventName]?.slice() || []; + if (newEventListenerList.length) { + for (let count = newEventListenerList.length - 1; count >= 0; count -= 1) { + const listener = newEventListenerList[count]; let data = eventData.data; const destroy = listener.Callback(data); if (destroy) { @@ -130,8 +130,16 @@ }); } } + function EventsOffAll() { + const eventNames = Object.keys(eventListeners); + eventNames.forEach((eventName) => { + removeListener(eventName); + }); + } function listenerOff(listener) { const eventName = listener.eventName; + if (eventListeners[eventName] === void 0) + return; eventListeners[eventName] = eventListeners[eventName].filter((l) => l !== listener); if (eventListeners[eventName].length === 0) { removeListener(eventName); @@ -424,6 +432,160 @@ return Call(":wails:ClipboardGetText"); } + // desktop/draganddrop.js + var draganddrop_exports = {}; + __export(draganddrop_exports, { + CanResolveFilePaths: () => CanResolveFilePaths, + OnFileDrop: () => OnFileDrop, + OnFileDropOff: () => OnFileDropOff, + ResolveFilePaths: () => ResolveFilePaths + }); + var flags = { + registered: false, + defaultUseDropTarget: true, + useDropTarget: true, + nextDeactivate: null, + nextDeactivateTimeout: null + }; + var DROP_TARGET_ACTIVE = "wails-drop-target-active"; + function checkStyleDropTarget(style) { + const cssDropValue = style.getPropertyValue(window.wails.flags.cssDropProperty).trim(); + if (cssDropValue) { + if (cssDropValue === window.wails.flags.cssDropValue) { + return true; + } + return false; + } + return false; + } + function onDragOver(e) { + const isFileDrop = e.dataTransfer.types.includes("Files"); + if (!isFileDrop) { + return; + } + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + if (!flags.useDropTarget) { + return; + } + const element = e.target; + if (flags.nextDeactivate) + flags.nextDeactivate(); + if (!element || !checkStyleDropTarget(getComputedStyle(element))) { + return; + } + let currentElement = element; + while (currentElement) { + if (checkStyleDropTarget(getComputedStyle(currentElement))) { + currentElement.classList.add(DROP_TARGET_ACTIVE); + } + currentElement = currentElement.parentElement; + } + } + function onDragLeave(e) { + const isFileDrop = e.dataTransfer.types.includes("Files"); + if (!isFileDrop) { + return; + } + e.preventDefault(); + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + if (!flags.useDropTarget) { + return; + } + if (!e.target || !checkStyleDropTarget(getComputedStyle(e.target))) { + return null; + } + if (flags.nextDeactivate) + flags.nextDeactivate(); + flags.nextDeactivate = () => { + Array.from(document.getElementsByClassName(DROP_TARGET_ACTIVE)).forEach((el) => el.classList.remove(DROP_TARGET_ACTIVE)); + flags.nextDeactivate = null; + if (flags.nextDeactivateTimeout) { + clearTimeout(flags.nextDeactivateTimeout); + flags.nextDeactivateTimeout = null; + } + }; + flags.nextDeactivateTimeout = setTimeout(() => { + if (flags.nextDeactivate) + flags.nextDeactivate(); + }, 50); + } + function onDrop(e) { + const isFileDrop = e.dataTransfer.types.includes("Files"); + if (!isFileDrop) { + return; + } + e.preventDefault(); + if (!window.wails.flags.enableWailsDragAndDrop) { + return; + } + if (CanResolveFilePaths()) { + let files = []; + if (e.dataTransfer.items) { + files = [...e.dataTransfer.items].map((item, i) => { + if (item.kind === "file") { + return item.getAsFile(); + } + }); + } else { + files = [...e.dataTransfer.files]; + } + window.runtime.ResolveFilePaths(e.x, e.y, files); + } + if (!flags.useDropTarget) { + return; + } + if (flags.nextDeactivate) + flags.nextDeactivate(); + Array.from(document.getElementsByClassName(DROP_TARGET_ACTIVE)).forEach((el) => el.classList.remove(DROP_TARGET_ACTIVE)); + } + function CanResolveFilePaths() { + return window.chrome?.webview?.postMessageWithAdditionalObjects != null; + } + function ResolveFilePaths(x, y, files) { + if (window.chrome?.webview?.postMessageWithAdditionalObjects) { + chrome.webview.postMessageWithAdditionalObjects(`file:drop:${x}:${y}`, files); + } + } + function OnFileDrop(callback, useDropTarget) { + if (typeof callback !== "function") { + console.error("DragAndDropCallback is not a function"); + return; + } + if (flags.registered) { + return; + } + flags.registered = true; + const uDTPT = typeof useDropTarget; + flags.useDropTarget = uDTPT === "undefined" || uDTPT !== "boolean" ? flags.defaultUseDropTarget : useDropTarget; + window.addEventListener("dragover", onDragOver); + window.addEventListener("dragleave", onDragLeave); + window.addEventListener("drop", onDrop); + let cb = callback; + if (flags.useDropTarget) { + cb = function(x, y, paths) { + const element = document.elementFromPoint(x, y); + if (!element || !checkStyleDropTarget(getComputedStyle(element))) { + return null; + } + callback(x, y, paths); + }; + } + EventsOn("wails:file-drop", cb); + } + function OnFileDropOff() { + window.removeEventListener("dragover", onDragOver); + window.removeEventListener("dragleave", onDragLeave); + window.removeEventListener("drop", onDrop); + EventsOff("wails:file-drop"); + flags.registered = false; + } + // desktop/contextmenu.js function processDefaultContextMenu(event) { const element = event.target; @@ -462,6 +624,67 @@ } } + // desktop/notifications.js + var notifications_exports = {}; + __export(notifications_exports, { + CheckNotificationAuthorization: () => CheckNotificationAuthorization, + CleanupNotifications: () => CleanupNotifications, + InitializeNotifications: () => InitializeNotifications, + IsNotificationAvailable: () => IsNotificationAvailable, + RegisterNotificationCategory: () => RegisterNotificationCategory, + RemoveAllDeliveredNotifications: () => RemoveAllDeliveredNotifications, + RemoveAllPendingNotifications: () => RemoveAllPendingNotifications, + RemoveDeliveredNotification: () => RemoveDeliveredNotification, + RemoveNotification: () => RemoveNotification, + RemoveNotificationCategory: () => RemoveNotificationCategory, + RemovePendingNotification: () => RemovePendingNotification, + RequestNotificationAuthorization: () => RequestNotificationAuthorization, + SendNotification: () => SendNotification, + SendNotificationWithActions: () => SendNotificationWithActions + }); + function InitializeNotifications() { + return Call(":wails:InitializeNotifications"); + } + function CleanupNotifications() { + return Call(":wails:CleanupNotifications"); + } + function IsNotificationAvailable() { + return Call(":wails:IsNotificationAvailable"); + } + function RequestNotificationAuthorization() { + return Call(":wails:RequestNotificationAuthorization"); + } + function CheckNotificationAuthorization() { + return Call(":wails:CheckNotificationAuthorization"); + } + function SendNotification(options) { + return Call(":wails:SendNotification", [options]); + } + function SendNotificationWithActions(options) { + return Call(":wails:SendNotificationWithActions", [options]); + } + function RegisterNotificationCategory(category) { + return Call(":wails:RegisterNotificationCategory", [category]); + } + function RemoveNotificationCategory(categoryId) { + return Call(":wails:RemoveNotificationCategory", [categoryId]); + } + function RemoveAllPendingNotifications() { + return Call(":wails:RemoveAllPendingNotifications"); + } + function RemovePendingNotification(identifier) { + return Call(":wails:RemovePendingNotification", [identifier]); + } + function RemoveAllDeliveredNotifications() { + return Call(":wails:RemoveAllDeliveredNotifications"); + } + function RemoveDeliveredNotification(identifier) { + return Call(":wails:RemoveDeliveredNotification", [identifier]); + } + function RemoveNotification(identifier) { + return Call(":wails:RemoveNotification", [identifier]); + } + // desktop/main.js function Quit() { window.WailsInvoke("Q"); @@ -481,11 +704,14 @@ ...browser_exports, ...screen_exports, ...clipboard_exports, + ...draganddrop_exports, + ...notifications_exports, EventsOn, EventsOnce, EventsOnMultiple, EventsEmit, EventsOff, + EventsOffAll, Environment, Show, Hide, @@ -506,7 +732,10 @@ shouldDrag: false, deferDragToMouseMove: true, cssDragProperty: "--wails-draggable", - cssDragValue: "drag" + cssDragValue: "drag", + cssDropProperty: "--wails-drop-target", + cssDropValue: "drop", + enableWailsDragAndDrop: false } }; if (window.wailsbindings) { @@ -536,6 +765,10 @@ window.wails.flags.cssDragProperty = property; window.wails.flags.cssDragValue = value; }; + window.wails.setCSSDropProperties = function(property, value) { + window.wails.flags.cssDropProperty = property; + window.wails.flags.cssDropValue = value; + }; window.addEventListener("mousedown", (e) => { if (window.wails.flags.resizeEdge) { window.WailsInvoke("resize:" + window.wails.flags.resizeEdge); @@ -618,4 +851,4 @@ }); window.WailsInvoke("runtime:ready"); })(); -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiZGVza3RvcC9sb2cuanMiLCAiZGVza3RvcC9ldmVudHMuanMiLCAiZGVza3RvcC9jYWxscy5qcyIsICJkZXNrdG9wL2JpbmRpbmdzLmpzIiwgImRlc2t0b3Avd2luZG93LmpzIiwgImRlc2t0b3Avc2NyZWVuLmpzIiwgImRlc2t0b3AvYnJvd3Nlci5qcyIsICJkZXNrdG9wL2NsaXBib2FyZC5qcyIsICJkZXNrdG9wL2NvbnRleHRtZW51LmpzIiwgImRlc2t0b3AvbWFpbi5qcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiLypcbiBfICAgICAgIF9fICAgICAgXyBfX1xufCB8ICAgICAvIC9fX18gXyhfKSAvX19fX1xufCB8IC98IC8gLyBfXyBgLyAvIC8gX19fL1xufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cblRoZSBlbGVjdHJvbiBhbHRlcm5hdGl2ZSBmb3IgR29cbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcbiovXG5cbi8qIGpzaGludCBlc3ZlcnNpb246IDYgKi9cblxuLyoqXG4gKiBTZW5kcyBhIGxvZyBtZXNzYWdlIHRvIHRoZSBiYWNrZW5kIHdpdGggdGhlIGdpdmVuIGxldmVsICsgbWVzc2FnZVxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSBsZXZlbFxuICogQHBhcmFtIHtzdHJpbmd9IG1lc3NhZ2VcbiAqL1xuZnVuY3Rpb24gc2VuZExvZ01lc3NhZ2UobGV2ZWwsIG1lc3NhZ2UpIHtcblxuXHQvLyBMb2cgTWVzc2FnZSBmb3JtYXQ6XG5cdC8vIGxbdHlwZV1bbWVzc2FnZV1cblx0d2luZG93LldhaWxzSW52b2tlKCdMJyArIGxldmVsICsgbWVzc2FnZSk7XG59XG5cbi8qKlxuICogTG9nIHRoZSBnaXZlbiB0cmFjZSBtZXNzYWdlIHdpdGggdGhlIGJhY2tlbmRcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZVxuICovXG5leHBvcnQgZnVuY3Rpb24gTG9nVHJhY2UobWVzc2FnZSkge1xuXHRzZW5kTG9nTWVzc2FnZSgnVCcsIG1lc3NhZ2UpO1xufVxuXG4vKipcbiAqIExvZyB0aGUgZ2l2ZW4gbWVzc2FnZSB3aXRoIHRoZSBiYWNrZW5kXG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IG1lc3NhZ2VcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIExvZ1ByaW50KG1lc3NhZ2UpIHtcblx0c2VuZExvZ01lc3NhZ2UoJ1AnLCBtZXNzYWdlKTtcbn1cblxuLyoqXG4gKiBMb2cgdGhlIGdpdmVuIGRlYnVnIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBMb2dEZWJ1ZyhtZXNzYWdlKSB7XG5cdHNlbmRMb2dNZXNzYWdlKCdEJywgbWVzc2FnZSk7XG59XG5cbi8qKlxuICogTG9nIHRoZSBnaXZlbiBpbmZvIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBMb2dJbmZvKG1lc3NhZ2UpIHtcblx0c2VuZExvZ01lc3NhZ2UoJ0knLCBtZXNzYWdlKTtcbn1cblxuLyoqXG4gKiBMb2cgdGhlIGdpdmVuIHdhcm5pbmcgbWVzc2FnZSB3aXRoIHRoZSBiYWNrZW5kXG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IG1lc3NhZ2VcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIExvZ1dhcm5pbmcobWVzc2FnZSkge1xuXHRzZW5kTG9nTWVzc2FnZSgnVycsIG1lc3NhZ2UpO1xufVxuXG4vKipcbiAqIExvZyB0aGUgZ2l2ZW4gZXJyb3IgbWVzc2FnZSB3aXRoIHRoZSBiYWNrZW5kXG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IG1lc3NhZ2VcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIExvZ0Vycm9yKG1lc3NhZ2UpIHtcblx0c2VuZExvZ01lc3NhZ2UoJ0UnLCBtZXNzYWdlKTtcbn1cblxuLyoqXG4gKiBMb2cgdGhlIGdpdmVuIGZhdGFsIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBMb2dGYXRhbChtZXNzYWdlKSB7XG5cdHNlbmRMb2dNZXNzYWdlKCdGJywgbWVzc2FnZSk7XG59XG5cbi8qKlxuICogU2V0cyB0aGUgTG9nIGxldmVsIHRvIHRoZSBnaXZlbiBsb2cgbGV2ZWxcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge251bWJlcn0gbG9nbGV2ZWxcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFNldExvZ0xldmVsKGxvZ2xldmVsKSB7XG5cdHNlbmRMb2dNZXNzYWdlKCdTJywgbG9nbGV2ZWwpO1xufVxuXG4vLyBMb2cgbGV2ZWxzXG5leHBvcnQgY29uc3QgTG9nTGV2ZWwgPSB7XG5cdFRSQUNFOiAxLFxuXHRERUJVRzogMixcblx0SU5GTzogMyxcblx0V0FSTklORzogNCxcblx0RVJST1I6IDUsXG59O1xuIiwgIi8qXG4gXyAgICAgICBfXyAgICAgIF8gX19cbnwgfCAgICAgLyAvX19fIF8oXykgL19fX19cbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKVxufF9fL3xfXy9cXF9fLF8vXy9fL19fX18vXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XG4qL1xuLyoganNoaW50IGVzdmVyc2lvbjogNiAqL1xuXG4vLyBEZWZpbmVzIGEgc2luZ2xlIGxpc3RlbmVyIHdpdGggYSBtYXhpbXVtIG51bWJlciBvZiB0aW1lcyB0byBjYWxsYmFja1xuXG4vKipcbiAqIFRoZSBMaXN0ZW5lciBjbGFzcyBkZWZpbmVzIGEgbGlzdGVuZXIhIDotKVxuICpcbiAqIEBjbGFzcyBMaXN0ZW5lclxuICovXG5jbGFzcyBMaXN0ZW5lciB7XG4gICAgLyoqXG4gICAgICogQ3JlYXRlcyBhbiBpbnN0YW5jZSBvZiBMaXN0ZW5lci5cbiAgICAgKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXG4gICAgICogQHBhcmFtIHtmdW5jdGlvbn0gY2FsbGJhY2tcbiAgICAgKiBAcGFyYW0ge251bWJlcn0gbWF4Q2FsbGJhY2tzXG4gICAgICogQG1lbWJlcm9mIExpc3RlbmVyXG4gICAgICovXG4gICAgY29uc3RydWN0b3IoZXZlbnROYW1lLCBjYWxsYmFjaywgbWF4Q2FsbGJhY2tzKSB7XG4gICAgICAgIHRoaXMuZXZlbnROYW1lID0gZXZlbnROYW1lO1xuICAgICAgICAvLyBEZWZhdWx0IG9mIC0xIG1lYW5zIGluZmluaXRlXG4gICAgICAgIHRoaXMubWF4Q2FsbGJhY2tzID0gbWF4Q2FsbGJhY2tzIHx8IC0xO1xuICAgICAgICAvLyBDYWxsYmFjayBpbnZva2VzIHRoZSBjYWxsYmFjayB3aXRoIHRoZSBnaXZlbiBkYXRhXG4gICAgICAgIC8vIFJldHVybnMgdHJ1ZSBpZiB0aGlzIGxpc3RlbmVyIHNob3VsZCBiZSBkZXN0cm95ZWRcbiAgICAgICAgdGhpcy5DYWxsYmFjayA9IChkYXRhKSA9PiB7XG4gICAgICAgICAgICBjYWxsYmFjay5hcHBseShudWxsLCBkYXRhKTtcbiAgICAgICAgICAgIC8vIElmIG1heENhbGxiYWNrcyBpcyBpbmZpbml0ZSwgcmV0dXJuIGZhbHNlIChkbyBub3QgZGVzdHJveSlcbiAgICAgICAgICAgIGlmICh0aGlzLm1heENhbGxiYWNrcyA9PT0gLTEpIHtcbiAgICAgICAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICAvLyBEZWNyZW1lbnQgbWF4Q2FsbGJhY2tzLiBSZXR1cm4gdHJ1ZSBpZiBub3cgMCwgb3RoZXJ3aXNlIGZhbHNlXG4gICAgICAgICAgICB0aGlzLm1heENhbGxiYWNrcyAtPSAxO1xuICAgICAgICAgICAgcmV0dXJuIHRoaXMubWF4Q2FsbGJhY2tzID09PSAwO1xuICAgICAgICB9O1xuICAgIH1cbn1cblxuZXhwb3J0IGNvbnN0IGV2ZW50TGlzdGVuZXJzID0ge307XG5cbi8qKlxuICogUmVnaXN0ZXJzIGFuIGV2ZW50IGxpc3RlbmVyIHRoYXQgd2lsbCBiZSBpbnZva2VkIGBtYXhDYWxsYmFja3NgIHRpbWVzIGJlZm9yZSBiZWluZyBkZXN0cm95ZWRcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFja1xuICogQHBhcmFtIHtudW1iZXJ9IG1heENhbGxiYWNrc1xuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09uTXVsdGlwbGUoZXZlbnROYW1lLCBjYWxsYmFjaywgbWF4Q2FsbGJhY2tzKSB7XG4gICAgZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXSA9IGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0gfHwgW107XG4gICAgY29uc3QgdGhpc0xpc3RlbmVyID0gbmV3IExpc3RlbmVyKGV2ZW50TmFtZSwgY2FsbGJhY2ssIG1heENhbGxiYWNrcyk7XG4gICAgZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXS5wdXNoKHRoaXNMaXN0ZW5lcik7XG4gICAgcmV0dXJuICgpID0+IGxpc3RlbmVyT2ZmKHRoaXNMaXN0ZW5lcik7XG59XG5cbi8qKlxuICogUmVnaXN0ZXJzIGFuIGV2ZW50IGxpc3RlbmVyIHRoYXQgd2lsbCBiZSBpbnZva2VkIGV2ZXJ5IHRpbWUgdGhlIGV2ZW50IGlzIGVtaXR0ZWRcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFja1xuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09uKGV2ZW50TmFtZSwgY2FsbGJhY2spIHtcbiAgICByZXR1cm4gRXZlbnRzT25NdWx0aXBsZShldmVudE5hbWUsIGNhbGxiYWNrLCAtMSk7XG59XG5cbi8qKlxuICogUmVnaXN0ZXJzIGFuIGV2ZW50IGxpc3RlbmVyIHRoYXQgd2lsbCBiZSBpbnZva2VkIG9uY2UgdGhlbiBkZXN0cm95ZWRcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFja1xuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09uY2UoZXZlbnROYW1lLCBjYWxsYmFjaykge1xuICAgIHJldHVybiBFdmVudHNPbk11bHRpcGxlKGV2ZW50TmFtZSwgY2FsbGJhY2ssIDEpO1xufVxuXG5mdW5jdGlvbiBub3RpZnlMaXN0ZW5lcnMoZXZlbnREYXRhKSB7XG5cbiAgICAvLyBHZXQgdGhlIGV2ZW50IG5hbWVcbiAgICBsZXQgZXZlbnROYW1lID0gZXZlbnREYXRhLm5hbWU7XG5cbiAgICAvLyBDaGVjayBpZiB3ZSBoYXZlIGFueSBsaXN0ZW5lcnMgZm9yIHRoaXMgZXZlbnRcbiAgICBpZiAoZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXSkge1xuXG4gICAgICAgIC8vIEtlZXAgYSBsaXN0IG9mIGxpc3RlbmVyIGluZGV4ZXMgdG8gZGVzdHJveVxuICAgICAgICBjb25zdCBuZXdFdmVudExpc3RlbmVyTGlzdCA9IGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0uc2xpY2UoKTtcblxuICAgICAgICAvLyBJdGVyYXRlIGxpc3RlbmVyc1xuICAgICAgICBmb3IgKGxldCBjb3VudCA9IGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0ubGVuZ3RoIC0gMTsgY291bnQgPj0gMDsgY291bnQgLT0gMSkge1xuXG4gICAgICAgICAgICAvLyBHZXQgbmV4dCBsaXN0ZW5lclxuICAgICAgICAgICAgY29uc3QgbGlzdGVuZXIgPSBldmVudExpc3RlbmVyc1tldmVudE5hbWVdW2NvdW50XTtcblxuICAgICAgICAgICAgbGV0IGRhdGEgPSBldmVudERhdGEuZGF0YTtcblxuICAgICAgICAgICAgLy8gRG8gdGhlIGNhbGxiYWNrXG4gICAgICAgICAgICBjb25zdCBkZXN0cm95ID0gbGlzdGVuZXIuQ2FsbGJhY2soZGF0YSk7XG4gICAgICAgICAgICBpZiAoZGVzdHJveSkge1xuICAgICAgICAgICAgICAgIC8vIGlmIHRoZSBsaXN0ZW5lciBpbmRpY2F0ZWQgdG8gZGVzdHJveSBpdHNlbGYsIGFkZCBpdCB0byB0aGUgZGVzdHJveSBsaXN0XG4gICAgICAgICAgICAgICAgbmV3RXZlbnRMaXN0ZW5lckxpc3Quc3BsaWNlKGNvdW50LCAxKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuXG4gICAgICAgIC8vIFVwZGF0ZSBjYWxsYmFja3Mgd2l0aCBuZXcgbGlzdCBvZiBsaXN0ZW5lcnNcbiAgICAgICAgaWYgKG5ld0V2ZW50TGlzdGVuZXJMaXN0Lmxlbmd0aCA9PT0gMCkge1xuICAgICAgICAgICAgcmVtb3ZlTGlzdGVuZXIoZXZlbnROYW1lKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0gPSBuZXdFdmVudExpc3RlbmVyTGlzdDtcbiAgICAgICAgfVxuICAgIH1cbn1cblxuLyoqXG4gKiBOb3RpZnkgaW5mb3JtcyBmcm9udGVuZCBsaXN0ZW5lcnMgdGhhdCBhbiBldmVudCB3YXMgZW1pdHRlZCB3aXRoIHRoZSBnaXZlbiBkYXRhXG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IG5vdGlmeU1lc3NhZ2UgLSBlbmNvZGVkIG5vdGlmaWNhdGlvbiBtZXNzYWdlXG5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c05vdGlmeShub3RpZnlNZXNzYWdlKSB7XG4gICAgLy8gUGFyc2UgdGhlIG1lc3NhZ2VcbiAgICBsZXQgbWVzc2FnZTtcbiAgICB0cnkge1xuICAgICAgICBtZXNzYWdlID0gSlNPTi5wYXJzZShub3RpZnlNZXNzYWdlKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNvbnN0IGVycm9yID0gJ0ludmFsaWQgSlNPTiBwYXNzZWQgdG8gTm90aWZ5OiAnICsgbm90aWZ5TWVzc2FnZTtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGVycm9yKTtcbiAgICB9XG4gICAgbm90aWZ5TGlzdGVuZXJzKG1lc3NhZ2UpO1xufVxuXG4vKipcbiAqIEVtaXQgYW4gZXZlbnQgd2l0aCB0aGUgZ2l2ZW4gbmFtZSBhbmQgZGF0YVxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7c3RyaW5nfSBldmVudE5hbWVcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c0VtaXQoZXZlbnROYW1lKSB7XG5cbiAgICBjb25zdCBwYXlsb2FkID0ge1xuICAgICAgICBuYW1lOiBldmVudE5hbWUsXG4gICAgICAgIGRhdGE6IFtdLnNsaWNlLmFwcGx5KGFyZ3VtZW50cykuc2xpY2UoMSksXG4gICAgfTtcblxuICAgIC8vIE5vdGlmeSBKUyBsaXN0ZW5lcnNcbiAgICBub3RpZnlMaXN0ZW5lcnMocGF5bG9hZCk7XG5cbiAgICAvLyBOb3RpZnkgR28gbGlzdGVuZXJzXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdFRScgKyBKU09OLnN0cmluZ2lmeShwYXlsb2FkKSk7XG59XG5cbmZ1bmN0aW9uIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSkge1xuICAgIC8vIFJlbW92ZSBsb2NhbCBsaXN0ZW5lcnNcbiAgICBkZWxldGUgZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXTtcblxuICAgIC8vIE5vdGlmeSBHbyBsaXN0ZW5lcnNcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ0VYJyArIGV2ZW50TmFtZSk7XG59XG5cbi8qKlxuICogT2ZmIHVucmVnaXN0ZXJzIGEgbGlzdGVuZXIgcHJldmlvdXNseSByZWdpc3RlcmVkIHdpdGggT24sXG4gKiBvcHRpb25hbGx5IG11bHRpcGxlIGxpc3RlbmVyZXMgY2FuIGJlIHVucmVnaXN0ZXJlZCB2aWEgYGFkZGl0aW9uYWxFdmVudE5hbWVzYFxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSBldmVudE5hbWVcbiAqIEBwYXJhbSAgey4uLnN0cmluZ30gYWRkaXRpb25hbEV2ZW50TmFtZXNcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09mZihldmVudE5hbWUsIC4uLmFkZGl0aW9uYWxFdmVudE5hbWVzKSB7XG4gICAgcmVtb3ZlTGlzdGVuZXIoZXZlbnROYW1lKVxuXG4gICAgaWYgKGFkZGl0aW9uYWxFdmVudE5hbWVzLmxlbmd0aCA+IDApIHtcbiAgICAgICAgYWRkaXRpb25hbEV2ZW50TmFtZXMuZm9yRWFjaChldmVudE5hbWUgPT4ge1xuICAgICAgICAgICAgcmVtb3ZlTGlzdGVuZXIoZXZlbnROYW1lKVxuICAgICAgICB9KVxuICAgIH1cbn1cblxuLyoqXG4gKiBPZmYgdW5yZWdpc3RlcnMgYWxsIGV2ZW50IGxpc3RlbmVycyBwcmV2aW91c2x5IHJlZ2lzdGVyZWQgd2l0aCBPblxuICovXG4gZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09mZkFsbCgpIHtcbiAgICBjb25zdCBldmVudE5hbWVzID0gT2JqZWN0LmtleXMoZXZlbnRMaXN0ZW5lcnMpO1xuICAgIGZvciAobGV0IGkgPSAwOyBpICE9PSBldmVudE5hbWVzLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZXNbaV0pO1xuICAgIH1cbn1cblxuLyoqXG4gKiBsaXN0ZW5lck9mZiB1bnJlZ2lzdGVycyBhIGxpc3RlbmVyIHByZXZpb3VzbHkgcmVnaXN0ZXJlZCB3aXRoIEV2ZW50c09uXG4gKlxuICogQHBhcmFtIHtMaXN0ZW5lcn0gbGlzdGVuZXJcbiAqL1xuIGZ1bmN0aW9uIGxpc3RlbmVyT2ZmKGxpc3RlbmVyKSB7XG4gICAgY29uc3QgZXZlbnROYW1lID0gbGlzdGVuZXIuZXZlbnROYW1lO1xuICAgIC8vIFJlbW92ZSBsb2NhbCBsaXN0ZW5lclxuICAgIGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0gPSBldmVudExpc3RlbmVyc1tldmVudE5hbWVdLmZpbHRlcihsID0+IGwgIT09IGxpc3RlbmVyKTtcblxuICAgIC8vIENsZWFuIHVwIGlmIHRoZXJlIGFyZSBubyBldmVudCBsaXN0ZW5lcnMgbGVmdFxuICAgIGlmIChldmVudExpc3RlbmVyc1tldmVudE5hbWVdLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICByZW1vdmVMaXN0ZW5lcihldmVudE5hbWUpO1xuICAgIH1cbn1cbiIsICIvKlxuIF8gICAgICAgX18gICAgICBfIF9fXG58IHwgICAgIC8gL19fXyBfKF8pIC9fX19fXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXG58IHwvIHwvIC8gL18vIC8gLyAoX18gIClcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xuKGMpIExlYSBBbnRob255IDIwMTktcHJlc2VudFxuKi9cbi8qIGpzaGludCBlc3ZlcnNpb246IDYgKi9cblxuZXhwb3J0IGNvbnN0IGNhbGxiYWNrcyA9IHt9O1xuXG4vKipcbiAqIFJldHVybnMgYSBudW1iZXIgZnJvbSB0aGUgbmF0aXZlIGJyb3dzZXIgcmFuZG9tIGZ1bmN0aW9uXG4gKlxuICogQHJldHVybnMgbnVtYmVyXG4gKi9cbmZ1bmN0aW9uIGNyeXB0b1JhbmRvbSgpIHtcblx0dmFyIGFycmF5ID0gbmV3IFVpbnQzMkFycmF5KDEpO1xuXHRyZXR1cm4gd2luZG93LmNyeXB0by5nZXRSYW5kb21WYWx1ZXMoYXJyYXkpWzBdO1xufVxuXG4vKipcbiAqIFJldHVybnMgYSBudW1iZXIgdXNpbmcgZGEgb2xkLXNrb29sIE1hdGguUmFuZG9tXG4gKiBJIGxpa2VzIHRvIGNhbGwgaXQgTE9MUmFuZG9tXG4gKlxuICogQHJldHVybnMgbnVtYmVyXG4gKi9cbmZ1bmN0aW9uIGJhc2ljUmFuZG9tKCkge1xuXHRyZXR1cm4gTWF0aC5yYW5kb20oKSAqIDkwMDcxOTkyNTQ3NDA5OTE7XG59XG5cbi8vIFBpY2sgYSByYW5kb20gbnVtYmVyIGZ1bmN0aW9uIGJhc2VkIG9uIGJyb3dzZXIgY2FwYWJpbGl0eVxudmFyIHJhbmRvbUZ1bmM7XG5pZiAod2luZG93LmNyeXB0bykge1xuXHRyYW5kb21GdW5jID0gY3J5cHRvUmFuZG9tO1xufSBlbHNlIHtcblx0cmFuZG9tRnVuYyA9IGJhc2ljUmFuZG9tO1xufVxuXG5cbi8qKlxuICogQ2FsbCBzZW5kcyBhIG1lc3NhZ2UgdG8gdGhlIGJhY2tlbmQgdG8gY2FsbCB0aGUgYmluZGluZyB3aXRoIHRoZVxuICogZ2l2ZW4gZGF0YS4gQSBwcm9taXNlIGlzIHJldHVybmVkIGFuZCB3aWxsIGJlIGNvbXBsZXRlZCB3aGVuIHRoZVxuICogYmFja2VuZCByZXNwb25kcy4gVGhpcyB3aWxsIGJlIHJlc29sdmVkIHdoZW4gdGhlIGNhbGwgd2FzIHN1Y2Nlc3NmdWxcbiAqIG9yIHJlamVjdGVkIGlmIGFuIGVycm9yIGlzIHBhc3NlZCBiYWNrLlxuICogVGhlcmUgaXMgYSB0aW1lb3V0IG1lY2hhbmlzbS4gSWYgdGhlIGNhbGwgZG9lc24ndCByZXNwb25kIGluIHRoZSBnaXZlblxuICogdGltZSAoaW4gbWlsbGlzZWNvbmRzKSB0aGVuIHRoZSBwcm9taXNlIGlzIHJlamVjdGVkLlxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7c3RyaW5nfSBuYW1lXG4gKiBAcGFyYW0ge2FueT19IGFyZ3NcbiAqIEBwYXJhbSB7bnVtYmVyPX0gdGltZW91dFxuICogQHJldHVybnNcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIENhbGwobmFtZSwgYXJncywgdGltZW91dCkge1xuXG5cdC8vIFRpbWVvdXQgaW5maW5pdGUgYnkgZGVmYXVsdFxuXHRpZiAodGltZW91dCA9PSBudWxsKSB7XG5cdFx0dGltZW91dCA9IDA7XG5cdH1cblxuXHQvLyBDcmVhdGUgYSBwcm9taXNlXG5cdHJldHVybiBuZXcgUHJvbWlzZShmdW5jdGlvbiAocmVzb2x2ZSwgcmVqZWN0KSB7XG5cblx0XHQvLyBDcmVhdGUgYSB1bmlxdWUgY2FsbGJhY2tJRFxuXHRcdHZhciBjYWxsYmFja0lEO1xuXHRcdGRvIHtcblx0XHRcdGNhbGxiYWNrSUQgPSBuYW1lICsgJy0nICsgcmFuZG9tRnVuYygpO1xuXHRcdH0gd2hpbGUgKGNhbGxiYWNrc1tjYWxsYmFja0lEXSk7XG5cblx0XHR2YXIgdGltZW91dEhhbmRsZTtcblx0XHQvLyBTZXQgdGltZW91dFxuXHRcdGlmICh0aW1lb3V0ID4gMCkge1xuXHRcdFx0dGltZW91dEhhbmRsZSA9IHNldFRpbWVvdXQoZnVuY3Rpb24gKCkge1xuXHRcdFx0XHRyZWplY3QoRXJyb3IoJ0NhbGwgdG8gJyArIG5hbWUgKyAnIHRpbWVkIG91dC4gUmVxdWVzdCBJRDogJyArIGNhbGxiYWNrSUQpKTtcblx0XHRcdH0sIHRpbWVvdXQpO1xuXHRcdH1cblxuXHRcdC8vIFN0b3JlIGNhbGxiYWNrXG5cdFx0Y2FsbGJhY2tzW2NhbGxiYWNrSURdID0ge1xuXHRcdFx0dGltZW91dEhhbmRsZTogdGltZW91dEhhbmRsZSxcblx0XHRcdHJlamVjdDogcmVqZWN0LFxuXHRcdFx0cmVzb2x2ZTogcmVzb2x2ZVxuXHRcdH07XG5cblx0XHR0cnkge1xuXHRcdFx0Y29uc3QgcGF5bG9hZCA9IHtcblx0XHRcdFx0bmFtZSxcblx0XHRcdFx0YXJncyxcblx0XHRcdFx0Y2FsbGJhY2tJRCxcblx0XHRcdH07XG5cbiAgICAgICAgICAgIC8vIE1ha2UgdGhlIGNhbGxcbiAgICAgICAgICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnQycgKyBKU09OLnN0cmluZ2lmeShwYXlsb2FkKSk7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZVxuICAgICAgICAgICAgY29uc29sZS5lcnJvcihlKTtcbiAgICAgICAgfVxuICAgIH0pO1xufVxuXG53aW5kb3cuT2JmdXNjYXRlZENhbGwgPSAoaWQsIGFyZ3MsIHRpbWVvdXQpID0+IHtcblxuICAgIC8vIFRpbWVvdXQgaW5maW5pdGUgYnkgZGVmYXVsdFxuICAgIGlmICh0aW1lb3V0ID09IG51bGwpIHtcbiAgICAgICAgdGltZW91dCA9IDA7XG4gICAgfVxuXG4gICAgLy8gQ3JlYXRlIGEgcHJvbWlzZVxuICAgIHJldHVybiBuZXcgUHJvbWlzZShmdW5jdGlvbiAocmVzb2x2ZSwgcmVqZWN0KSB7XG5cbiAgICAgICAgLy8gQ3JlYXRlIGEgdW5pcXVlIGNhbGxiYWNrSURcbiAgICAgICAgdmFyIGNhbGxiYWNrSUQ7XG4gICAgICAgIGRvIHtcbiAgICAgICAgICAgIGNhbGxiYWNrSUQgPSBpZCArICctJyArIHJhbmRvbUZ1bmMoKTtcbiAgICAgICAgfSB3aGlsZSAoY2FsbGJhY2tzW2NhbGxiYWNrSURdKTtcblxuICAgICAgICB2YXIgdGltZW91dEhhbmRsZTtcbiAgICAgICAgLy8gU2V0IHRpbWVvdXRcbiAgICAgICAgaWYgKHRpbWVvdXQgPiAwKSB7XG4gICAgICAgICAgICB0aW1lb3V0SGFuZGxlID0gc2V0VGltZW91dChmdW5jdGlvbiAoKSB7XG4gICAgICAgICAgICAgICAgcmVqZWN0KEVycm9yKCdDYWxsIHRvIG1ldGhvZCAnICsgaWQgKyAnIHRpbWVkIG91dC4gUmVxdWVzdCBJRDogJyArIGNhbGxiYWNrSUQpKTtcbiAgICAgICAgICAgIH0sIHRpbWVvdXQpO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gU3RvcmUgY2FsbGJhY2tcbiAgICAgICAgY2FsbGJhY2tzW2NhbGxiYWNrSURdID0ge1xuICAgICAgICAgICAgdGltZW91dEhhbmRsZTogdGltZW91dEhhbmRsZSxcbiAgICAgICAgICAgIHJlamVjdDogcmVqZWN0LFxuICAgICAgICAgICAgcmVzb2x2ZTogcmVzb2x2ZVxuICAgICAgICB9O1xuXG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0ge1xuXHRcdFx0XHRpZCxcblx0XHRcdFx0YXJncyxcblx0XHRcdFx0Y2FsbGJhY2tJRCxcblx0XHRcdH07XG5cbiAgICAgICAgICAgIC8vIE1ha2UgdGhlIGNhbGxcbiAgICAgICAgICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnYycgKyBKU09OLnN0cmluZ2lmeShwYXlsb2FkKSk7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZVxuICAgICAgICAgICAgY29uc29sZS5lcnJvcihlKTtcbiAgICAgICAgfVxuICAgIH0pO1xufTtcblxuXG4vKipcbiAqIENhbGxlZCBieSB0aGUgYmFja2VuZCB0byByZXR1cm4gZGF0YSB0byBhIHByZXZpb3VzbHkgY2FsbGVkXG4gKiBiaW5kaW5nIGludm9jYXRpb25cbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gaW5jb21pbmdNZXNzYWdlXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBDYWxsYmFjayhpbmNvbWluZ01lc3NhZ2UpIHtcblx0Ly8gUGFyc2UgdGhlIG1lc3NhZ2Vcblx0bGV0IG1lc3NhZ2U7XG5cdHRyeSB7XG5cdFx0bWVzc2FnZSA9IEpTT04ucGFyc2UoaW5jb21pbmdNZXNzYWdlKTtcblx0fSBjYXRjaCAoZSkge1xuXHRcdGNvbnN0IGVycm9yID0gYEludmFsaWQgSlNPTiBwYXNzZWQgdG8gY2FsbGJhY2s6ICR7ZS5tZXNzYWdlfS4gTWVzc2FnZTogJHtpbmNvbWluZ01lc3NhZ2V9YDtcblx0XHRydW50aW1lLkxvZ0RlYnVnKGVycm9yKTtcblx0XHR0aHJvdyBuZXcgRXJyb3IoZXJyb3IpO1xuXHR9XG5cdGxldCBjYWxsYmFja0lEID0gbWVzc2FnZS5jYWxsYmFja2lkO1xuXHRsZXQgY2FsbGJhY2tEYXRhID0gY2FsbGJhY2tzW2NhbGxiYWNrSURdO1xuXHRpZiAoIWNhbGxiYWNrRGF0YSkge1xuXHRcdGNvbnN0IGVycm9yID0gYENhbGxiYWNrICcke2NhbGxiYWNrSUR9JyBub3QgcmVnaXN0ZXJlZCEhIWA7XG5cdFx0Y29uc29sZS5lcnJvcihlcnJvcik7IC8vIGVzbGludC1kaXNhYmxlLWxpbmVcblx0XHR0aHJvdyBuZXcgRXJyb3IoZXJyb3IpO1xuXHR9XG5cdGNsZWFyVGltZW91dChjYWxsYmFja0RhdGEudGltZW91dEhhbmRsZSk7XG5cblx0ZGVsZXRlIGNhbGxiYWNrc1tjYWxsYmFja0lEXTtcblxuXHRpZiAobWVzc2FnZS5lcnJvcikge1xuXHRcdGNhbGxiYWNrRGF0YS5yZWplY3QobWVzc2FnZS5lcnJvcik7XG5cdH0gZWxzZSB7XG5cdFx0Y2FsbGJhY2tEYXRhLnJlc29sdmUobWVzc2FnZS5yZXN1bHQpO1xuXHR9XG59XG4iLCAiLypcbiBfICAgICAgIF9fICAgICAgXyBfXyAgICBcbnwgfCAgICAgLyAvX19fIF8oXykgL19fX19cbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKSBcbnxfXy98X18vXFxfXyxfL18vXy9fX19fLyAgXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XG4qL1xuLyoganNoaW50IGVzdmVyc2lvbjogNiAqL1xuXG5pbXBvcnQge0NhbGx9IGZyb20gJy4vY2FsbHMnO1xuXG4vLyBUaGlzIGlzIHdoZXJlIHdlIGJpbmQgZ28gbWV0aG9kIHdyYXBwZXJzXG53aW5kb3cuZ28gPSB7fTtcblxuZXhwb3J0IGZ1bmN0aW9uIFNldEJpbmRpbmdzKGJpbmRpbmdzTWFwKSB7XG5cdHRyeSB7XG5cdFx0YmluZGluZ3NNYXAgPSBKU09OLnBhcnNlKGJpbmRpbmdzTWFwKTtcblx0fSBjYXRjaCAoZSkge1xuXHRcdGNvbnNvbGUuZXJyb3IoZSk7XG5cdH1cblxuXHQvLyBJbml0aWFsaXNlIHRoZSBiaW5kaW5ncyBtYXBcblx0d2luZG93LmdvID0gd2luZG93LmdvIHx8IHt9O1xuXG5cdC8vIEl0ZXJhdGUgcGFja2FnZSBuYW1lc1xuXHRPYmplY3Qua2V5cyhiaW5kaW5nc01hcCkuZm9yRWFjaCgocGFja2FnZU5hbWUpID0+IHtcblxuXHRcdC8vIENyZWF0ZSBpbm5lciBtYXAgaWYgaXQgZG9lc24ndCBleGlzdFxuXHRcdHdpbmRvdy5nb1twYWNrYWdlTmFtZV0gPSB3aW5kb3cuZ29bcGFja2FnZU5hbWVdIHx8IHt9O1xuXG5cdFx0Ly8gSXRlcmF0ZSBzdHJ1Y3QgbmFtZXNcblx0XHRPYmplY3Qua2V5cyhiaW5kaW5nc01hcFtwYWNrYWdlTmFtZV0pLmZvckVhY2goKHN0cnVjdE5hbWUpID0+IHtcblxuXHRcdFx0Ly8gQ3JlYXRlIGlubmVyIG1hcCBpZiBpdCBkb2Vzbid0IGV4aXN0XG5cdFx0XHR3aW5kb3cuZ29bcGFja2FnZU5hbWVdW3N0cnVjdE5hbWVdID0gd2luZG93LmdvW3BhY2thZ2VOYW1lXVtzdHJ1Y3ROYW1lXSB8fCB7fTtcblxuXHRcdFx0T2JqZWN0LmtleXMoYmluZGluZ3NNYXBbcGFja2FnZU5hbWVdW3N0cnVjdE5hbWVdKS5mb3JFYWNoKChtZXRob2ROYW1lKSA9PiB7XG5cblx0XHRcdFx0d2luZG93LmdvW3BhY2thZ2VOYW1lXVtzdHJ1Y3ROYW1lXVttZXRob2ROYW1lXSA9IGZ1bmN0aW9uICgpIHtcblxuXHRcdFx0XHRcdC8vIE5vIHRpbWVvdXQgYnkgZGVmYXVsdFxuXHRcdFx0XHRcdGxldCB0aW1lb3V0ID0gMDtcblxuXHRcdFx0XHRcdC8vIEFjdHVhbCBmdW5jdGlvblxuXHRcdFx0XHRcdGZ1bmN0aW9uIGR5bmFtaWMoKSB7XG5cdFx0XHRcdFx0XHRjb25zdCBhcmdzID0gW10uc2xpY2UuY2FsbChhcmd1bWVudHMpO1xuXHRcdFx0XHRcdFx0cmV0dXJuIENhbGwoW3BhY2thZ2VOYW1lLCBzdHJ1Y3ROYW1lLCBtZXRob2ROYW1lXS5qb2luKCcuJyksIGFyZ3MsIHRpbWVvdXQpO1xuXHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdC8vIEFsbG93IHNldHRpbmcgdGltZW91dCB0byBmdW5jdGlvblxuXHRcdFx0XHRcdGR5bmFtaWMuc2V0VGltZW91dCA9IGZ1bmN0aW9uIChuZXdUaW1lb3V0KSB7XG5cdFx0XHRcdFx0XHR0aW1lb3V0ID0gbmV3VGltZW91dDtcblx0XHRcdFx0XHR9O1xuXG5cdFx0XHRcdFx0Ly8gQWxsb3cgZ2V0dGluZyB0aW1lb3V0IHRvIGZ1bmN0aW9uXG5cdFx0XHRcdFx0ZHluYW1pYy5nZXRUaW1lb3V0ID0gZnVuY3Rpb24gKCkge1xuXHRcdFx0XHRcdFx0cmV0dXJuIHRpbWVvdXQ7XG5cdFx0XHRcdFx0fTtcblxuXHRcdFx0XHRcdHJldHVybiBkeW5hbWljO1xuXHRcdFx0XHR9KCk7XG5cdFx0XHR9KTtcblx0XHR9KTtcblx0fSk7XG59XG4iLCAiLypcbiBfXHQgICBfX1x0ICBfIF9fXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xufCB8IC98IC8gLyBfXyBgLyAvIC8gX19fL1xufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cblRoZSBlbGVjdHJvbiBhbHRlcm5hdGl2ZSBmb3IgR29cbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcbiovXG5cbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cblxuXG5pbXBvcnQge0NhbGx9IGZyb20gXCIuL2NhbGxzXCI7XG5cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dSZWxvYWQoKSB7XG4gICAgd2luZG93LmxvY2F0aW9uLnJlbG9hZCgpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gV2luZG93UmVsb2FkQXBwKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV1InKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFN5c3RlbURlZmF1bHRUaGVtZSgpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dBU0RUJyk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dTZXRMaWdodFRoZW1lKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0FMVCcpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2V0RGFya1RoZW1lKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0FEVCcpO1xufVxuXG4vKipcbiAqIFBsYWNlIHRoZSB3aW5kb3cgaW4gdGhlIGNlbnRlciBvZiB0aGUgc2NyZWVuXG4gKlxuICogQGV4cG9ydFxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93Q2VudGVyKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV2MnKTtcbn1cblxuLyoqXG4gKiBTZXRzIHRoZSB3aW5kb3cgdGl0bGVcbiAqXG4gKiBAcGFyYW0ge3N0cmluZ30gdGl0bGVcbiAqIEBleHBvcnRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFRpdGxlKHRpdGxlKSB7XG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXVCcgKyB0aXRsZSk7XG59XG5cbi8qKlxuICogTWFrZXMgdGhlIHdpbmRvdyBnbyBmdWxsc2NyZWVuXG4gKlxuICogQGV4cG9ydFxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93RnVsbHNjcmVlbigpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dGJyk7XG59XG5cbi8qKlxuICogUmV2ZXJ0cyB0aGUgd2luZG93IGZyb20gZnVsbHNjcmVlblxuICpcbiAqIEBleHBvcnRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1VuZnVsbHNjcmVlbigpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dmJyk7XG59XG5cbi8qKlxuICogUmV0dXJucyB0aGUgc3RhdGUgb2YgdGhlIHdpbmRvdywgaS5lLiB3aGV0aGVyIHRoZSB3aW5kb3cgaXMgaW4gZnVsbCBzY3JlZW4gbW9kZSBvciBub3QuXG4gKlxuICogQGV4cG9ydFxuICogQHJldHVybiB7UHJvbWlzZTxib29sZWFuPn0gVGhlIHN0YXRlIG9mIHRoZSB3aW5kb3dcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd0lzRnVsbHNjcmVlbigpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dJc0Z1bGxzY3JlZW5cIik7XG59XG5cbi8qKlxuICogU2V0IHRoZSBTaXplIG9mIHRoZSB3aW5kb3dcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge251bWJlcn0gd2lkdGhcbiAqIEBwYXJhbSB7bnVtYmVyfSBoZWlnaHRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFNpemUod2lkdGgsIGhlaWdodCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV3M6JyArIHdpZHRoICsgJzonICsgaGVpZ2h0KTtcbn1cblxuLyoqXG4gKiBHZXQgdGhlIFNpemUgb2YgdGhlIHdpbmRvd1xuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8e3c6IG51bWJlciwgaDogbnVtYmVyfT59IFRoZSBzaXplIG9mIHRoZSB3aW5kb3dcblxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93R2V0U2l6ZSgpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dHZXRTaXplXCIpO1xufVxuXG4vKipcbiAqIFNldCB0aGUgbWF4aW11bSBzaXplIG9mIHRoZSB3aW5kb3dcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge251bWJlcn0gd2lkdGhcbiAqIEBwYXJhbSB7bnVtYmVyfSBoZWlnaHRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldE1heFNpemUod2lkdGgsIGhlaWdodCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV1o6JyArIHdpZHRoICsgJzonICsgaGVpZ2h0KTtcbn1cblxuLyoqXG4gKiBTZXQgdGhlIG1pbmltdW0gc2l6ZSBvZiB0aGUgd2luZG93XG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtudW1iZXJ9IHdpZHRoXG4gKiBAcGFyYW0ge251bWJlcn0gaGVpZ2h0XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dTZXRNaW5TaXplKHdpZHRoLCBoZWlnaHQpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1d6OicgKyB3aWR0aCArICc6JyArIGhlaWdodCk7XG59XG5cblxuXG4vKipcbiAqIFNldCB0aGUgd2luZG93IEFsd2F5c09uVG9wIG9yIG5vdCBvbiB0b3BcbiAqXG4gKiBAZXhwb3J0XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dTZXRBbHdheXNPblRvcChiKSB7XG5cbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dBVFA6JyArIChiID8gJzEnIDogJzAnKSk7XG59XG5cblxuXG5cbi8qKlxuICogU2V0IHRoZSBQb3NpdGlvbiBvZiB0aGUgd2luZG93XG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtudW1iZXJ9IHhcbiAqIEBwYXJhbSB7bnVtYmVyfSB5XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dTZXRQb3NpdGlvbih4LCB5KSB7XG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXcDonICsgeCArICc6JyArIHkpO1xufVxuXG4vKipcbiAqIEdldCB0aGUgUG9zaXRpb24gb2YgdGhlIHdpbmRvd1xuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8e3g6IG51bWJlciwgeTogbnVtYmVyfT59IFRoZSBwb3NpdGlvbiBvZiB0aGUgd2luZG93XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dHZXRQb3NpdGlvbigpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dHZXRQb3NcIik7XG59XG5cbi8qKlxuICogSGlkZSB0aGUgV2luZG93XG4gKlxuICogQGV4cG9ydFxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93SGlkZSgpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dIJyk7XG59XG5cbi8qKlxuICogU2hvdyB0aGUgV2luZG93XG4gKlxuICogQGV4cG9ydFxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2hvdygpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dTJyk7XG59XG5cbi8qKlxuICogTWF4aW1pc2UgdGhlIFdpbmRvd1xuICpcbiAqIEBleHBvcnRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd01heGltaXNlKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV00nKTtcbn1cblxuLyoqXG4gKiBUb2dnbGUgdGhlIE1heGltaXNlIG9mIHRoZSBXaW5kb3dcbiAqXG4gKiBAZXhwb3J0XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dUb2dnbGVNYXhpbWlzZSgpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1d0Jyk7XG59XG5cbi8qKlxuICogVW5tYXhpbWlzZSB0aGUgV2luZG93XG4gKlxuICogQGV4cG9ydFxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93VW5tYXhpbWlzZSgpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dVJyk7XG59XG5cbi8qKlxuICogUmV0dXJucyB0aGUgc3RhdGUgb2YgdGhlIHdpbmRvdywgaS5lLiB3aGV0aGVyIHRoZSB3aW5kb3cgaXMgbWF4aW1pc2VkIG9yIG5vdC5cbiAqXG4gKiBAZXhwb3J0XG4gKiBAcmV0dXJuIHtQcm9taXNlPGJvb2xlYW4+fSBUaGUgc3RhdGUgb2YgdGhlIHdpbmRvd1xuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93SXNNYXhpbWlzZWQoKSB7XG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6V2luZG93SXNNYXhpbWlzZWRcIik7XG59XG5cbi8qKlxuICogTWluaW1pc2UgdGhlIFdpbmRvd1xuICpcbiAqIEBleHBvcnRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd01pbmltaXNlKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV20nKTtcbn1cblxuLyoqXG4gKiBVbm1pbmltaXNlIHRoZSBXaW5kb3dcbiAqXG4gKiBAZXhwb3J0XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dVbm1pbmltaXNlKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV3UnKTtcbn1cblxuLyoqXG4gKiBSZXR1cm5zIHRoZSBzdGF0ZSBvZiB0aGUgd2luZG93LCBpLmUuIHdoZXRoZXIgdGhlIHdpbmRvdyBpcyBtaW5pbWlzZWQgb3Igbm90LlxuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8Ym9vbGVhbj59IFRoZSBzdGF0ZSBvZiB0aGUgd2luZG93XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dJc01pbmltaXNlZCgpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dJc01pbmltaXNlZFwiKTtcbn1cblxuLyoqXG4gKiBSZXR1cm5zIHRoZSBzdGF0ZSBvZiB0aGUgd2luZG93LCBpLmUuIHdoZXRoZXIgdGhlIHdpbmRvdyBpcyBub3JtYWwgb3Igbm90LlxuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8Ym9vbGVhbj59IFRoZSBzdGF0ZSBvZiB0aGUgd2luZG93XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dJc05vcm1hbCgpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dJc05vcm1hbFwiKTtcbn1cblxuLyoqXG4gKiBTZXRzIHRoZSBiYWNrZ3JvdW5kIGNvbG91ciBvZiB0aGUgd2luZG93XG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtudW1iZXJ9IFIgUmVkXG4gKiBAcGFyYW0ge251bWJlcn0gRyBHcmVlblxuICogQHBhcmFtIHtudW1iZXJ9IEIgQmx1ZVxuICogQHBhcmFtIHtudW1iZXJ9IEEgQWxwaGFcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldEJhY2tncm91bmRDb2xvdXIoUiwgRywgQiwgQSkge1xuICAgIGxldCByZ2JhID0gSlNPTi5zdHJpbmdpZnkoe3I6IFIgfHwgMCwgZzogRyB8fCAwLCBiOiBCIHx8IDAsIGE6IEEgfHwgMjU1fSk7XG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXcjonICsgcmdiYSk7XG59XG5cbiIsICIvKlxuIF9cdCAgIF9fXHQgIF8gX19cbnwgfFx0IC8gL19fXyBfKF8pIC9fX19fXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXG58IHwvIHwvIC8gL18vIC8gLyAoX18gIClcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xuKGMpIExlYSBBbnRob255IDIwMTktcHJlc2VudFxuKi9cblxuLyoganNoaW50IGVzdmVyc2lvbjogOSAqL1xuXG5cbmltcG9ydCB7Q2FsbH0gZnJvbSBcIi4vY2FsbHNcIjtcblxuXG4vKipcbiAqIEdldHMgdGhlIGFsbCBzY3JlZW5zLiBDYWxsIHRoaXMgYW5ldyBlYWNoIHRpbWUgeW91IHdhbnQgdG8gcmVmcmVzaCBkYXRhIGZyb20gdGhlIHVuZGVybHlpbmcgd2luZG93aW5nIHN5c3RlbS5cbiAqIEBleHBvcnRcbiAqIEB0eXBlZGVmIHtpbXBvcnQoJy4uL3dyYXBwZXIvcnVudGltZScpLlNjcmVlbn0gU2NyZWVuXG4gKiBAcmV0dXJuIHtQcm9taXNlPHtTY3JlZW5bXX0+fSBUaGUgc2NyZWVuc1xuICovXG5leHBvcnQgZnVuY3Rpb24gU2NyZWVuR2V0QWxsKCkge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOlNjcmVlbkdldEFsbFwiKTtcbn1cbiIsICIvKipcbiAqIEBkZXNjcmlwdGlvbjogVXNlIHRoZSBzeXN0ZW0gZGVmYXVsdCBicm93c2VyIHRvIG9wZW4gdGhlIHVybFxuICogQHBhcmFtIHtzdHJpbmd9IHVybCBcbiAqIEByZXR1cm4ge3ZvaWR9XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBCcm93c2VyT3BlblVSTCh1cmwpIHtcbiAgd2luZG93LldhaWxzSW52b2tlKCdCTzonICsgdXJsKTtcbn0iLCAiLypcbiBfXHQgICBfX1x0ICBfIF9fXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xufCB8IC98IC8gLyBfXyBgLyAvIC8gX19fL1xufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cblRoZSBlbGVjdHJvbiBhbHRlcm5hdGl2ZSBmb3IgR29cbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcbiovXG5cbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cblxuaW1wb3J0IHtDYWxsfSBmcm9tIFwiLi9jYWxsc1wiO1xuXG4vKipcbiAqIFNldCB0aGUgU2l6ZSBvZiB0aGUgd2luZG93XG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IHRleHRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIENsaXBib2FyZFNldFRleHQodGV4dCkge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOkNsaXBib2FyZFNldFRleHRcIiwgW3RleHRdKTtcbn1cblxuLyoqXG4gKiBHZXQgdGhlIHRleHQgY29udGVudCBvZiB0aGUgY2xpcGJvYXJkXG4gKlxuICogQGV4cG9ydFxuICogQHJldHVybiB7UHJvbWlzZTx7c3RyaW5nfT59IFRleHQgY29udGVudCBvZiB0aGUgY2xpcGJvYXJkXG5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIENsaXBib2FyZEdldFRleHQoKSB7XG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6Q2xpcGJvYXJkR2V0VGV4dFwiKTtcbn0iLCAiLypcbi0tZGVmYXVsdC1jb250ZXh0bWVudTogYXV0bzsgKGRlZmF1bHQpIHdpbGwgc2hvdyB0aGUgZGVmYXVsdCBjb250ZXh0IG1lbnUgaWYgY29udGVudEVkaXRhYmxlIGlzIHRydWUgT1IgdGV4dCBoYXMgYmVlbiBzZWxlY3RlZCBPUiBlbGVtZW50IGlzIGlucHV0IG9yIHRleHRhcmVhXG4tLWRlZmF1bHQtY29udGV4dG1lbnU6IHNob3c7IHdpbGwgYWx3YXlzIHNob3cgdGhlIGRlZmF1bHQgY29udGV4dCBtZW51XG4tLWRlZmF1bHQtY29udGV4dG1lbnU6IGhpZGU7IHdpbGwgYWx3YXlzIGhpZGUgdGhlIGRlZmF1bHQgY29udGV4dCBtZW51XG5cblRoaXMgcnVsZSBpcyBpbmhlcml0ZWQgbGlrZSBub3JtYWwgQ1NTIHJ1bGVzLCBzbyBuZXN0aW5nIHdvcmtzIGFzIGV4cGVjdGVkXG4qL1xuZXhwb3J0IGZ1bmN0aW9uIHByb2Nlc3NEZWZhdWx0Q29udGV4dE1lbnUoZXZlbnQpIHtcbiAgICAvLyBQcm9jZXNzIGRlZmF1bHQgY29udGV4dCBtZW51XG4gICAgY29uc3QgZWxlbWVudCA9IGV2ZW50LnRhcmdldDtcbiAgICBjb25zdCBjb21wdXRlZFN0eWxlID0gd2luZG93LmdldENvbXB1dGVkU3R5bGUoZWxlbWVudCk7XG4gICAgY29uc3QgZGVmYXVsdENvbnRleHRNZW51QWN0aW9uID0gY29tcHV0ZWRTdHlsZS5nZXRQcm9wZXJ0eVZhbHVlKFwiLS1kZWZhdWx0LWNvbnRleHRtZW51XCIpLnRyaW0oKTtcbiAgICBzd2l0Y2ggKGRlZmF1bHRDb250ZXh0TWVudUFjdGlvbikge1xuICAgICAgICBjYXNlIFwic2hvd1wiOlxuICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICBjYXNlIFwiaGlkZVwiOlxuICAgICAgICAgICAgZXZlbnQucHJldmVudERlZmF1bHQoKTtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgZGVmYXVsdDpcbiAgICAgICAgICAgIC8vIENoZWNrIGlmIGNvbnRlbnRFZGl0YWJsZSBpcyB0cnVlXG4gICAgICAgICAgICBpZiAoZWxlbWVudC5pc0NvbnRlbnRFZGl0YWJsZSkge1xuICAgICAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLy8gQ2hlY2sgaWYgdGV4dCBoYXMgYmVlbiBzZWxlY3RlZCBhbmQgYWN0aW9uIGlzIG9uIHRoZSBzZWxlY3RlZCBlbGVtZW50c1xuICAgICAgICAgICAgY29uc3Qgc2VsZWN0aW9uID0gd2luZG93LmdldFNlbGVjdGlvbigpO1xuICAgICAgICAgICAgY29uc3QgaGFzU2VsZWN0aW9uID0gKHNlbGVjdGlvbi50b1N0cmluZygpLmxlbmd0aCA+IDApXG4gICAgICAgICAgICBpZiAoaGFzU2VsZWN0aW9uKSB7XG4gICAgICAgICAgICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBzZWxlY3Rpb24ucmFuZ2VDb3VudDsgaSsrKSB7XG4gICAgICAgICAgICAgICAgICAgIGNvbnN0IHJhbmdlID0gc2VsZWN0aW9uLmdldFJhbmdlQXQoaSk7XG4gICAgICAgICAgICAgICAgICAgIGNvbnN0IHJlY3RzID0gcmFuZ2UuZ2V0Q2xpZW50UmVjdHMoKTtcbiAgICAgICAgICAgICAgICAgICAgZm9yIChsZXQgaiA9IDA7IGogPCByZWN0cy5sZW5ndGg7IGorKykge1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc3QgcmVjdCA9IHJlY3RzW2pdO1xuICAgICAgICAgICAgICAgICAgICAgICAgaWYgKGRvY3VtZW50LmVsZW1lbnRGcm9tUG9pbnQocmVjdC5sZWZ0LCByZWN0LnRvcCkgPT09IGVsZW1lbnQpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICAvLyBDaGVjayBpZiB0YWduYW1lIGlzIGlucHV0IG9yIHRleHRhcmVhXG4gICAgICAgICAgICBpZiAoZWxlbWVudC50YWdOYW1lID09PSBcIklOUFVUXCIgfHwgZWxlbWVudC50YWdOYW1lID09PSBcIlRFWFRBUkVBXCIpIHtcbiAgICAgICAgICAgICAgICBpZiAoaGFzU2VsZWN0aW9uIHx8ICghZWxlbWVudC5yZWFkT25seSAmJiAhZWxlbWVudC5kaXNhYmxlZCkpIHtcbiAgICAgICAgICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLy8gaGlkZSBkZWZhdWx0IGNvbnRleHQgbWVudVxuICAgICAgICAgICAgZXZlbnQucHJldmVudERlZmF1bHQoKTtcbiAgICB9XG59XG4iLCAiLypcbiBfXHQgICBfX1x0ICBfIF9fXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xufCB8IC98IC8gLyBfXyBgLyAvIC8gX19fL1xufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cblRoZSBlbGVjdHJvbiBhbHRlcm5hdGl2ZSBmb3IgR29cbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcbiovXG4vKiBqc2hpbnQgZXN2ZXJzaW9uOiA5ICovXG5pbXBvcnQgKiBhcyBMb2cgZnJvbSAnLi9sb2cnO1xuaW1wb3J0IHtldmVudExpc3RlbmVycywgRXZlbnRzRW1pdCwgRXZlbnRzTm90aWZ5LCBFdmVudHNPZmYsIEV2ZW50c09uLCBFdmVudHNPbmNlLCBFdmVudHNPbk11bHRpcGxlfSBmcm9tICcuL2V2ZW50cyc7XG5pbXBvcnQge0NhbGwsIENhbGxiYWNrLCBjYWxsYmFja3N9IGZyb20gJy4vY2FsbHMnO1xuaW1wb3J0IHtTZXRCaW5kaW5nc30gZnJvbSBcIi4vYmluZGluZ3NcIjtcbmltcG9ydCAqIGFzIFdpbmRvdyBmcm9tIFwiLi93aW5kb3dcIjtcbmltcG9ydCAqIGFzIFNjcmVlbiBmcm9tIFwiLi9zY3JlZW5cIjtcbmltcG9ydCAqIGFzIEJyb3dzZXIgZnJvbSBcIi4vYnJvd3NlclwiO1xuaW1wb3J0ICogYXMgQ2xpcGJvYXJkIGZyb20gXCIuL2NsaXBib2FyZFwiO1xuaW1wb3J0ICogYXMgQ29udGV4dE1lbnUgZnJvbSBcIi4vY29udGV4dG1lbnVcIjtcblxuXG5leHBvcnQgZnVuY3Rpb24gUXVpdCgpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1EnKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFNob3coKSB7XG4gICAgd2luZG93LldhaWxzSW52b2tlKCdTJyk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBIaWRlKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnSCcpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gRW52aXJvbm1lbnQoKSB7XG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6RW52aXJvbm1lbnRcIik7XG59XG5cbi8vIFRoZSBKUyBydW50aW1lXG53aW5kb3cucnVudGltZSA9IHtcbiAgICAuLi5Mb2csXG4gICAgLi4uV2luZG93LFxuICAgIC4uLkJyb3dzZXIsXG4gICAgLi4uU2NyZWVuLFxuICAgIC4uLkNsaXBib2FyZCxcbiAgICBFdmVudHNPbixcbiAgICBFdmVudHNPbmNlLFxuICAgIEV2ZW50c09uTXVsdGlwbGUsXG4gICAgRXZlbnRzRW1pdCxcbiAgICBFdmVudHNPZmYsXG4gICAgRW52aXJvbm1lbnQsXG4gICAgU2hvdyxcbiAgICBIaWRlLFxuICAgIFF1aXRcbn07XG5cbi8vIEludGVybmFsIHdhaWxzIGVuZHBvaW50c1xud2luZG93LndhaWxzID0ge1xuICAgIENhbGxiYWNrLFxuICAgIEV2ZW50c05vdGlmeSxcbiAgICBTZXRCaW5kaW5ncyxcbiAgICBldmVudExpc3RlbmVycyxcbiAgICBjYWxsYmFja3MsXG4gICAgZmxhZ3M6IHtcbiAgICAgICAgZGlzYWJsZVNjcm9sbGJhckRyYWc6IGZhbHNlLFxuICAgICAgICBkaXNhYmxlRGVmYXVsdENvbnRleHRNZW51OiBmYWxzZSxcbiAgICAgICAgZW5hYmxlUmVzaXplOiBmYWxzZSxcbiAgICAgICAgZGVmYXVsdEN1cnNvcjogbnVsbCxcbiAgICAgICAgYm9yZGVyVGhpY2tuZXNzOiA2LFxuICAgICAgICBzaG91bGREcmFnOiBmYWxzZSxcbiAgICAgICAgZGVmZXJEcmFnVG9Nb3VzZU1vdmU6IHRydWUsXG4gICAgICAgIGNzc0RyYWdQcm9wZXJ0eTogXCItLXdhaWxzLWRyYWdnYWJsZVwiLFxuICAgICAgICBjc3NEcmFnVmFsdWU6IFwiZHJhZ1wiLFxuICAgIH1cbn07XG5cbi8vIFNldCB0aGUgYmluZGluZ3NcbmlmICh3aW5kb3cud2FpbHNiaW5kaW5ncykge1xuICAgIHdpbmRvdy53YWlscy5TZXRCaW5kaW5ncyh3aW5kb3cud2FpbHNiaW5kaW5ncyk7XG4gICAgZGVsZXRlIHdpbmRvdy53YWlscy5TZXRCaW5kaW5ncztcbn1cblxuLy8gKGJvb2wpIFRoaXMgaXMgZXZhbHVhdGVkIGF0IGJ1aWxkIHRpbWUgaW4gcGFja2FnZS5qc29uXG5pZiAoIURFQlVHKSB7XG4gICAgZGVsZXRlIHdpbmRvdy53YWlsc2JpbmRpbmdzO1xufVxuXG5sZXQgZHJhZ1Rlc3QgPSBmdW5jdGlvbiAoZSkge1xuICAgIHZhciB2YWwgPSB3aW5kb3cuZ2V0Q29tcHV0ZWRTdHlsZShlLnRhcmdldCkuZ2V0UHJvcGVydHlWYWx1ZSh3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1Byb3BlcnR5KTtcbiAgICBpZiAodmFsKSB7XG4gICAgICB2YWwgPSB2YWwudHJpbSgpO1xuICAgIH1cbiAgICBcbiAgICBpZiAodmFsICE9PSB3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1ZhbHVlKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG5cbiAgICBpZiAoZS5idXR0b25zICE9PSAxKSB7XG4gICAgICAgIC8vIERvIG5vdCBzdGFydCBkcmFnZ2luZyBpZiBub3QgdGhlIHByaW1hcnkgYnV0dG9uIGhhcyBiZWVuIGNsaWNrZWQuXG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG5cbiAgICBpZiAoZS5kZXRhaWwgIT09IDEpIHtcbiAgICAgICAgLy8gRG8gbm90IHN0YXJ0IGRyYWdnaW5nIGlmIG1vcmUgdGhhbiBvbmNlIGhhcyBiZWVuIGNsaWNrZWQsIGUuZy4gd2hlbiBkb3VibGUgY2xpY2tpbmdcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cblxuICAgIHJldHVybiB0cnVlO1xufTtcblxud2luZG93LndhaWxzLnNldENTU0RyYWdQcm9wZXJ0aWVzID0gZnVuY3Rpb24gKHByb3BlcnR5LCB2YWx1ZSkge1xuICAgIHdpbmRvdy53YWlscy5mbGFncy5jc3NEcmFnUHJvcGVydHkgPSBwcm9wZXJ0eTtcbiAgICB3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1ZhbHVlID0gdmFsdWU7XG59XG5cbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdtb3VzZWRvd24nLCAoZSkgPT4ge1xuXG4gICAgLy8gQ2hlY2sgZm9yIHJlc2l6aW5nXG4gICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5yZXNpemVFZGdlKSB7XG4gICAgICAgIHdpbmRvdy5XYWlsc0ludm9rZShcInJlc2l6ZTpcIiArIHdpbmRvdy53YWlscy5mbGFncy5yZXNpemVFZGdlKTtcbiAgICAgICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgaWYgKGRyYWdUZXN0KGUpKSB7XG4gICAgICAgIGlmICh3aW5kb3cud2FpbHMuZmxhZ3MuZGlzYWJsZVNjcm9sbGJhckRyYWcpIHtcbiAgICAgICAgICAgIC8vIFRoaXMgY2hlY2tzIGZvciBjbGlja3Mgb24gdGhlIHNjcm9sbCBiYXJcbiAgICAgICAgICAgIGlmIChlLm9mZnNldFggPiBlLnRhcmdldC5jbGllbnRXaWR0aCB8fCBlLm9mZnNldFkgPiBlLnRhcmdldC5jbGllbnRIZWlnaHQpIHtcbiAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5kZWZlckRyYWdUb01vdXNlTW92ZSkge1xuICAgICAgICAgICAgd2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcgPSB0cnVlO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgZS5wcmV2ZW50RGVmYXVsdCgpXG4gICAgICAgICAgICB3aW5kb3cuV2FpbHNJbnZva2UoXCJkcmFnXCIpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybjtcbiAgICB9IGVsc2Uge1xuICAgICAgICB3aW5kb3cud2FpbHMuZmxhZ3Muc2hvdWxkRHJhZyA9IGZhbHNlO1xuICAgIH1cbn0pO1xuXG53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcignbW91c2V1cCcsICgpID0+IHtcbiAgICB3aW5kb3cud2FpbHMuZmxhZ3Muc2hvdWxkRHJhZyA9IGZhbHNlO1xufSk7XG5cbmZ1bmN0aW9uIHNldFJlc2l6ZShjdXJzb3IpIHtcbiAgICBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuc3R5bGUuY3Vyc29yID0gY3Vyc29yIHx8IHdpbmRvdy53YWlscy5mbGFncy5kZWZhdWx0Q3Vyc29yO1xuICAgIHdpbmRvdy53YWlscy5mbGFncy5yZXNpemVFZGdlID0gY3Vyc29yO1xufVxuXG53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcignbW91c2Vtb3ZlJywgZnVuY3Rpb24gKGUpIHtcbiAgICBpZiAod2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcpIHtcbiAgICAgICAgd2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcgPSBmYWxzZTtcbiAgICAgICAgbGV0IG1vdXNlUHJlc3NlZCA9IGUuYnV0dG9ucyAhPT0gdW5kZWZpbmVkID8gZS5idXR0b25zIDogZS53aGljaDtcbiAgICAgICAgaWYgKG1vdXNlUHJlc3NlZCA+IDApIHtcbiAgICAgICAgICAgIHdpbmRvdy5XYWlsc0ludm9rZShcImRyYWdcIik7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICB9XG4gICAgaWYgKCF3aW5kb3cud2FpbHMuZmxhZ3MuZW5hYmxlUmVzaXplKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5kZWZhdWx0Q3Vyc29yID09IG51bGwpIHtcbiAgICAgICAgd2luZG93LndhaWxzLmZsYWdzLmRlZmF1bHRDdXJzb3IgPSBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuc3R5bGUuY3Vyc29yO1xuICAgIH1cbiAgICBpZiAod2luZG93Lm91dGVyV2lkdGggLSBlLmNsaWVudFggPCB3aW5kb3cud2FpbHMuZmxhZ3MuYm9yZGVyVGhpY2tuZXNzICYmIHdpbmRvdy5vdXRlckhlaWdodCAtIGUuY2xpZW50WSA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3MpIHtcbiAgICAgICAgZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LnN0eWxlLmN1cnNvciA9IFwic2UtcmVzaXplXCI7XG4gICAgfVxuICAgIGxldCByaWdodEJvcmRlciA9IHdpbmRvdy5vdXRlcldpZHRoIC0gZS5jbGllbnRYIDwgd2luZG93LndhaWxzLmZsYWdzLmJvcmRlclRoaWNrbmVzcztcbiAgICBsZXQgbGVmdEJvcmRlciA9IGUuY2xpZW50WCA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3M7XG4gICAgbGV0IHRvcEJvcmRlciA9IGUuY2xpZW50WSA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3M7XG4gICAgbGV0IGJvdHRvbUJvcmRlciA9IHdpbmRvdy5vdXRlckhlaWdodCAtIGUuY2xpZW50WSA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3M7XG5cbiAgICAvLyBJZiB3ZSBhcmVuJ3Qgb24gYW4gZWRnZSwgYnV0IHdlcmUsIHJlc2V0IHRoZSBjdXJzb3IgdG8gZGVmYXVsdFxuICAgIGlmICghbGVmdEJvcmRlciAmJiAhcmlnaHRCb3JkZXIgJiYgIXRvcEJvcmRlciAmJiAhYm90dG9tQm9yZGVyICYmIHdpbmRvdy53YWlscy5mbGFncy5yZXNpemVFZGdlICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgc2V0UmVzaXplKCk7XG4gICAgfSBlbHNlIGlmIChyaWdodEJvcmRlciAmJiBib3R0b21Cb3JkZXIpIHNldFJlc2l6ZShcInNlLXJlc2l6ZVwiKTtcbiAgICBlbHNlIGlmIChsZWZ0Qm9yZGVyICYmIGJvdHRvbUJvcmRlcikgc2V0UmVzaXplKFwic3ctcmVzaXplXCIpO1xuICAgIGVsc2UgaWYgKGxlZnRCb3JkZXIgJiYgdG9wQm9yZGVyKSBzZXRSZXNpemUoXCJudy1yZXNpemVcIik7XG4gICAgZWxzZSBpZiAodG9wQm9yZGVyICYmIHJpZ2h0Qm9yZGVyKSBzZXRSZXNpemUoXCJuZS1yZXNpemVcIik7XG4gICAgZWxzZSBpZiAobGVmdEJvcmRlcikgc2V0UmVzaXplKFwidy1yZXNpemVcIik7XG4gICAgZWxzZSBpZiAodG9wQm9yZGVyKSBzZXRSZXNpemUoXCJuLXJlc2l6ZVwiKTtcbiAgICBlbHNlIGlmIChib3R0b21Cb3JkZXIpIHNldFJlc2l6ZShcInMtcmVzaXplXCIpO1xuICAgIGVsc2UgaWYgKHJpZ2h0Qm9yZGVyKSBzZXRSZXNpemUoXCJlLXJlc2l6ZVwiKTtcblxufSk7XG5cbi8vIFNldHVwIGNvbnRleHQgbWVudSBob29rXG53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcignY29udGV4dG1lbnUnLCBmdW5jdGlvbiAoZSkge1xuICAgIC8vIGFsd2F5cyBzaG93IHRoZSBjb250ZXh0bWVudSBpbiBkZWJ1ZyAmIGRldlxuICAgIGlmIChERUJVRykgcmV0dXJuO1xuXG4gICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5kaXNhYmxlRGVmYXVsdENvbnRleHRNZW51KSB7XG4gICAgICAgIGUucHJldmVudERlZmF1bHQoKTtcbiAgICB9IGVsc2Uge1xuICAgICAgICBDb250ZXh0TWVudS5wcm9jZXNzRGVmYXVsdENvbnRleHRNZW51KGUpO1xuICAgIH1cbn0pO1xuXG53aW5kb3cuV2FpbHNJbnZva2UoXCJydW50aW1lOnJlYWR5XCIpOyJdLAogICJtYXBwaW5ncyI6ICI7Ozs7Ozs7O0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBa0JBLFdBQVMsZUFBZSxPQUFPLFNBQVM7QUFJdkMsV0FBTyxZQUFZLE1BQU0sUUFBUSxPQUFPO0FBQUEsRUFDekM7QUFRTyxXQUFTLFNBQVMsU0FBUztBQUNqQyxtQkFBZSxLQUFLLE9BQU87QUFBQSxFQUM1QjtBQVFPLFdBQVMsU0FBUyxTQUFTO0FBQ2pDLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxTQUFTLFNBQVM7QUFDakMsbUJBQWUsS0FBSyxPQUFPO0FBQUEsRUFDNUI7QUFRTyxXQUFTLFFBQVEsU0FBUztBQUNoQyxtQkFBZSxLQUFLLE9BQU87QUFBQSxFQUM1QjtBQVFPLFdBQVMsV0FBVyxTQUFTO0FBQ25DLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxTQUFTLFNBQVM7QUFDakMsbUJBQWUsS0FBSyxPQUFPO0FBQUEsRUFDNUI7QUFRTyxXQUFTLFNBQVMsU0FBUztBQUNqQyxtQkFBZSxLQUFLLE9BQU87QUFBQSxFQUM1QjtBQVFPLFdBQVMsWUFBWSxVQUFVO0FBQ3JDLG1CQUFlLEtBQUssUUFBUTtBQUFBLEVBQzdCO0FBR08sTUFBTSxXQUFXO0FBQUEsSUFDdkIsT0FBTztBQUFBLElBQ1AsT0FBTztBQUFBLElBQ1AsTUFBTTtBQUFBLElBQ04sU0FBUztBQUFBLElBQ1QsT0FBTztBQUFBLEVBQ1I7OztBQzlGQSxNQUFNLFdBQU4sTUFBZTtBQUFBLElBUVgsWUFBWSxXQUFXLFVBQVUsY0FBYztBQUMzQyxXQUFLLFlBQVk7QUFFakIsV0FBSyxlQUFlLGdCQUFnQjtBQUdwQyxXQUFLLFdBQVcsQ0FBQyxTQUFTO0FBQ3RCLGlCQUFTLE1BQU0sTUFBTSxJQUFJO0FBRXpCLFlBQUksS0FBSyxpQkFBaUIsSUFBSTtBQUMxQixpQkFBTztBQUFBLFFBQ1g7QUFFQSxhQUFLLGdCQUFnQjtBQUNyQixlQUFPLEtBQUssaUJBQWlCO0FBQUEsTUFDakM7QUFBQSxJQUNKO0FBQUEsRUFDSjtBQUVPLE1BQU0saUJBQWlCLENBQUM7QUFXeEIsV0FBUyxpQkFBaUIsV0FBVyxVQUFVLGNBQWM7QUFDaEUsbUJBQWUsYUFBYSxlQUFlLGNBQWMsQ0FBQztBQUMxRCxVQUFNLGVBQWUsSUFBSSxTQUFTLFdBQVcsVUFBVSxZQUFZO0FBQ25FLG1CQUFlLFdBQVcsS0FBSyxZQUFZO0FBQzNDLFdBQU8sTUFBTSxZQUFZLFlBQVk7QUFBQSxFQUN6QztBQVVPLFdBQVMsU0FBUyxXQUFXLFVBQVU7QUFDMUMsV0FBTyxpQkFBaUIsV0FBVyxVQUFVLEVBQUU7QUFBQSxFQUNuRDtBQVVPLFdBQVMsV0FBVyxXQUFXLFVBQVU7QUFDNUMsV0FBTyxpQkFBaUIsV0FBVyxVQUFVLENBQUM7QUFBQSxFQUNsRDtBQUVBLFdBQVMsZ0JBQWdCLFdBQVc7QUFHaEMsUUFBSSxZQUFZLFVBQVU7QUFHMUIsUUFBSSxlQUFlLFlBQVk7QUFHM0IsWUFBTSx1QkFBdUIsZUFBZSxXQUFXLE1BQU07QUFHN0QsZUFBUyxRQUFRLGVBQWUsV0FBVyxTQUFTLEdBQUcsU0FBUyxHQUFHLFNBQVMsR0FBRztBQUczRSxjQUFNLFdBQVcsZUFBZSxXQUFXO0FBRTNDLFlBQUksT0FBTyxVQUFVO0FBR3JCLGNBQU0sVUFBVSxTQUFTLFNBQVMsSUFBSTtBQUN0QyxZQUFJLFNBQVM7QUFFVCwrQkFBcUIsT0FBTyxPQUFPLENBQUM7QUFBQSxRQUN4QztBQUFBLE1BQ0o7QUFHQSxVQUFJLHFCQUFxQixXQUFXLEdBQUc7QUFDbkMsdUJBQWUsU0FBUztBQUFBLE1BQzVCLE9BQU87QUFDSCx1QkFBZSxhQUFhO0FBQUEsTUFDaEM7QUFBQSxJQUNKO0FBQUEsRUFDSjtBQVNPLFdBQVMsYUFBYSxlQUFlO0FBRXhDLFFBQUk7QUFDSixRQUFJO0FBQ0EsZ0JBQVUsS0FBSyxNQUFNLGFBQWE7QUFBQSxJQUN0QyxTQUFTLEdBQVA7QUFDRSxZQUFNLFFBQVEsb0NBQW9DO0FBQ2xELFlBQU0sSUFBSSxNQUFNLEtBQUs7QUFBQSxJQUN6QjtBQUNBLG9CQUFnQixPQUFPO0FBQUEsRUFDM0I7QUFRTyxXQUFTLFdBQVcsV0FBVztBQUVsQyxVQUFNLFVBQVU7QUFBQSxNQUNaLE1BQU07QUFBQSxNQUNOLE1BQU0sQ0FBQyxFQUFFLE1BQU0sTUFBTSxTQUFTLEVBQUUsTUFBTSxDQUFDO0FBQUEsSUFDM0M7QUFHQSxvQkFBZ0IsT0FBTztBQUd2QixXQUFPLFlBQVksT0FBTyxLQUFLLFVBQVUsT0FBTyxDQUFDO0FBQUEsRUFDckQ7QUFFQSxXQUFTLGVBQWUsV0FBVztBQUUvQixXQUFPLGVBQWU7QUFHdEIsV0FBTyxZQUFZLE9BQU8sU0FBUztBQUFBLEVBQ3ZDO0FBU08sV0FBUyxVQUFVLGNBQWMsc0JBQXNCO0FBQzFELG1CQUFlLFNBQVM7QUFFeEIsUUFBSSxxQkFBcUIsU0FBUyxHQUFHO0FBQ2pDLDJCQUFxQixRQUFRLENBQUFBLGVBQWE7QUFDdEMsdUJBQWVBLFVBQVM7QUFBQSxNQUM1QixDQUFDO0FBQUEsSUFDTDtBQUFBLEVBQ0o7QUFpQkMsV0FBUyxZQUFZLFVBQVU7QUFDNUIsVUFBTSxZQUFZLFNBQVM7QUFFM0IsbUJBQWUsYUFBYSxlQUFlLFdBQVcsT0FBTyxPQUFLLE1BQU0sUUFBUTtBQUdoRixRQUFJLGVBQWUsV0FBVyxXQUFXLEdBQUc7QUFDeEMscUJBQWUsU0FBUztBQUFBLElBQzVCO0FBQUEsRUFDSjs7O0FDeE1PLE1BQU0sWUFBWSxDQUFDO0FBTzFCLFdBQVMsZUFBZTtBQUN2QixRQUFJLFFBQVEsSUFBSSxZQUFZLENBQUM7QUFDN0IsV0FBTyxPQUFPLE9BQU8sZ0JBQWdCLEtBQUssRUFBRTtBQUFBLEVBQzdDO0FBUUEsV0FBUyxjQUFjO0FBQ3RCLFdBQU8sS0FBSyxPQUFPLElBQUk7QUFBQSxFQUN4QjtBQUdBLE1BQUk7QUFDSixNQUFJLE9BQU8sUUFBUTtBQUNsQixpQkFBYTtBQUFBLEVBQ2QsT0FBTztBQUNOLGlCQUFhO0FBQUEsRUFDZDtBQWlCTyxXQUFTLEtBQUssTUFBTSxNQUFNLFNBQVM7QUFHekMsUUFBSSxXQUFXLE1BQU07QUFDcEIsZ0JBQVU7QUFBQSxJQUNYO0FBR0EsV0FBTyxJQUFJLFFBQVEsU0FBVSxTQUFTLFFBQVE7QUFHN0MsVUFBSTtBQUNKLFNBQUc7QUFDRixxQkFBYSxPQUFPLE1BQU0sV0FBVztBQUFBLE1BQ3RDLFNBQVMsVUFBVTtBQUVuQixVQUFJO0FBRUosVUFBSSxVQUFVLEdBQUc7QUFDaEIsd0JBQWdCLFdBQVcsV0FBWTtBQUN0QyxpQkFBTyxNQUFNLGFBQWEsT0FBTyw2QkFBNkIsVUFBVSxDQUFDO0FBQUEsUUFDMUUsR0FBRyxPQUFPO0FBQUEsTUFDWDtBQUdBLGdCQUFVLGNBQWM7QUFBQSxRQUN2QjtBQUFBLFFBQ0E7QUFBQSxRQUNBO0FBQUEsTUFDRDtBQUVBLFVBQUk7QUFDSCxjQUFNLFVBQVU7QUFBQSxVQUNmO0FBQUEsVUFDQTtBQUFBLFVBQ0E7QUFBQSxRQUNEO0FBR1MsZUFBTyxZQUFZLE1BQU0sS0FBSyxVQUFVLE9BQU8sQ0FBQztBQUFBLE1BQ3BELFNBQVMsR0FBUDtBQUVFLGdCQUFRLE1BQU0sQ0FBQztBQUFBLE1BQ25CO0FBQUEsSUFDSixDQUFDO0FBQUEsRUFDTDtBQUVBLFNBQU8saUJBQWlCLENBQUMsSUFBSSxNQUFNLFlBQVk7QUFHM0MsUUFBSSxXQUFXLE1BQU07QUFDakIsZ0JBQVU7QUFBQSxJQUNkO0FBR0EsV0FBTyxJQUFJLFFBQVEsU0FBVSxTQUFTLFFBQVE7QUFHMUMsVUFBSTtBQUNKLFNBQUc7QUFDQyxxQkFBYSxLQUFLLE1BQU0sV0FBVztBQUFBLE1BQ3ZDLFNBQVMsVUFBVTtBQUVuQixVQUFJO0FBRUosVUFBSSxVQUFVLEdBQUc7QUFDYix3QkFBZ0IsV0FBVyxXQUFZO0FBQ25DLGlCQUFPLE1BQU0sb0JBQW9CLEtBQUssNkJBQTZCLFVBQVUsQ0FBQztBQUFBLFFBQ2xGLEdBQUcsT0FBTztBQUFBLE1BQ2Q7QUFHQSxnQkFBVSxjQUFjO0FBQUEsUUFDcEI7QUFBQSxRQUNBO0FBQUEsUUFDQTtBQUFBLE1BQ0o7QUFFQSxVQUFJO0FBQ0EsY0FBTSxVQUFVO0FBQUEsVUFDeEI7QUFBQSxVQUNBO0FBQUEsVUFDQTtBQUFBLFFBQ0Q7QUFHUyxlQUFPLFlBQVksTUFBTSxLQUFLLFVBQVUsT0FBTyxDQUFDO0FBQUEsTUFDcEQsU0FBUyxHQUFQO0FBRUUsZ0JBQVEsTUFBTSxDQUFDO0FBQUEsTUFDbkI7QUFBQSxJQUNKLENBQUM7QUFBQSxFQUNMO0FBVU8sV0FBUyxTQUFTLGlCQUFpQjtBQUV6QyxRQUFJO0FBQ0osUUFBSTtBQUNILGdCQUFVLEtBQUssTUFBTSxlQUFlO0FBQUEsSUFDckMsU0FBUyxHQUFQO0FBQ0QsWUFBTSxRQUFRLG9DQUFvQyxFQUFFLHFCQUFxQjtBQUN6RSxjQUFRLFNBQVMsS0FBSztBQUN0QixZQUFNLElBQUksTUFBTSxLQUFLO0FBQUEsSUFDdEI7QUFDQSxRQUFJLGFBQWEsUUFBUTtBQUN6QixRQUFJLGVBQWUsVUFBVTtBQUM3QixRQUFJLENBQUMsY0FBYztBQUNsQixZQUFNLFFBQVEsYUFBYTtBQUMzQixjQUFRLE1BQU0sS0FBSztBQUNuQixZQUFNLElBQUksTUFBTSxLQUFLO0FBQUEsSUFDdEI7QUFDQSxpQkFBYSxhQUFhLGFBQWE7QUFFdkMsV0FBTyxVQUFVO0FBRWpCLFFBQUksUUFBUSxPQUFPO0FBQ2xCLG1CQUFhLE9BQU8sUUFBUSxLQUFLO0FBQUEsSUFDbEMsT0FBTztBQUNOLG1CQUFhLFFBQVEsUUFBUSxNQUFNO0FBQUEsSUFDcEM7QUFBQSxFQUNEOzs7QUMxS0EsU0FBTyxLQUFLLENBQUM7QUFFTixXQUFTLFlBQVksYUFBYTtBQUN4QyxRQUFJO0FBQ0gsb0JBQWMsS0FBSyxNQUFNLFdBQVc7QUFBQSxJQUNyQyxTQUFTLEdBQVA7QUFDRCxjQUFRLE1BQU0sQ0FBQztBQUFBLElBQ2hCO0FBR0EsV0FBTyxLQUFLLE9BQU8sTUFBTSxDQUFDO0FBRzFCLFdBQU8sS0FBSyxXQUFXLEVBQUUsUUFBUSxDQUFDLGdCQUFnQjtBQUdqRCxhQUFPLEdBQUcsZUFBZSxPQUFPLEdBQUcsZ0JBQWdCLENBQUM7QUFHcEQsYUFBTyxLQUFLLFlBQVksWUFBWSxFQUFFLFFBQVEsQ0FBQyxlQUFlO0FBRzdELGVBQU8sR0FBRyxhQUFhLGNBQWMsT0FBTyxHQUFHLGFBQWEsZUFBZSxDQUFDO0FBRTVFLGVBQU8sS0FBSyxZQUFZLGFBQWEsV0FBVyxFQUFFLFFBQVEsQ0FBQyxlQUFlO0FBRXpFLGlCQUFPLEdBQUcsYUFBYSxZQUFZLGNBQWMsV0FBWTtBQUc1RCxnQkFBSSxVQUFVO0FBR2QscUJBQVMsVUFBVTtBQUNsQixvQkFBTSxPQUFPLENBQUMsRUFBRSxNQUFNLEtBQUssU0FBUztBQUNwQyxxQkFBTyxLQUFLLENBQUMsYUFBYSxZQUFZLFVBQVUsRUFBRSxLQUFLLEdBQUcsR0FBRyxNQUFNLE9BQU87QUFBQSxZQUMzRTtBQUdBLG9CQUFRLGFBQWEsU0FBVSxZQUFZO0FBQzFDLHdCQUFVO0FBQUEsWUFDWDtBQUdBLG9CQUFRLGFBQWEsV0FBWTtBQUNoQyxxQkFBTztBQUFBLFlBQ1I7QUFFQSxtQkFBTztBQUFBLFVBQ1IsRUFBRTtBQUFBLFFBQ0gsQ0FBQztBQUFBLE1BQ0YsQ0FBQztBQUFBLElBQ0YsQ0FBQztBQUFBLEVBQ0Y7OztBQ2xFQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQWVPLFdBQVMsZUFBZTtBQUMzQixXQUFPLFNBQVMsT0FBTztBQUFBLEVBQzNCO0FBRU8sV0FBUyxrQkFBa0I7QUFDOUIsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQUVPLFdBQVMsOEJBQThCO0FBQzFDLFdBQU8sWUFBWSxPQUFPO0FBQUEsRUFDOUI7QUFFTyxXQUFTLHNCQUFzQjtBQUNsQyxXQUFPLFlBQVksTUFBTTtBQUFBLEVBQzdCO0FBRU8sV0FBUyxxQkFBcUI7QUFDakMsV0FBTyxZQUFZLE1BQU07QUFBQSxFQUM3QjtBQU9PLFdBQVMsZUFBZTtBQUMzQixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBUU8sV0FBUyxlQUFlLE9BQU87QUFDbEMsV0FBTyxZQUFZLE9BQU8sS0FBSztBQUFBLEVBQ25DO0FBT08sV0FBUyxtQkFBbUI7QUFDL0IsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQU9PLFdBQVMscUJBQXFCO0FBQ2pDLFdBQU8sWUFBWSxJQUFJO0FBQUEsRUFDM0I7QUFRTyxXQUFTLHFCQUFxQjtBQUNqQyxXQUFPLEtBQUssMkJBQTJCO0FBQUEsRUFDM0M7QUFTTyxXQUFTLGNBQWMsT0FBTyxRQUFRO0FBQ3pDLFdBQU8sWUFBWSxRQUFRLFFBQVEsTUFBTSxNQUFNO0FBQUEsRUFDbkQ7QUFTTyxXQUFTLGdCQUFnQjtBQUM1QixXQUFPLEtBQUssc0JBQXNCO0FBQUEsRUFDdEM7QUFTTyxXQUFTLGlCQUFpQixPQUFPLFFBQVE7QUFDNUMsV0FBTyxZQUFZLFFBQVEsUUFBUSxNQUFNLE1BQU07QUFBQSxFQUNuRDtBQVNPLFdBQVMsaUJBQWlCLE9BQU8sUUFBUTtBQUM1QyxXQUFPLFlBQVksUUFBUSxRQUFRLE1BQU0sTUFBTTtBQUFBLEVBQ25EO0FBU08sV0FBUyxxQkFBcUIsR0FBRztBQUVwQyxXQUFPLFlBQVksV0FBVyxJQUFJLE1BQU0sSUFBSTtBQUFBLEVBQ2hEO0FBWU8sV0FBUyxrQkFBa0IsR0FBRyxHQUFHO0FBQ3BDLFdBQU8sWUFBWSxRQUFRLElBQUksTUFBTSxDQUFDO0FBQUEsRUFDMUM7QUFRTyxXQUFTLG9CQUFvQjtBQUNoQyxXQUFPLEtBQUsscUJBQXFCO0FBQUEsRUFDckM7QUFPTyxXQUFTLGFBQWE7QUFDekIsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQU9PLFdBQVMsYUFBYTtBQUN6QixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBT08sV0FBUyxpQkFBaUI7QUFDN0IsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQU9PLFdBQVMsdUJBQXVCO0FBQ25DLFdBQU8sWUFBWSxJQUFJO0FBQUEsRUFDM0I7QUFPTyxXQUFTLG1CQUFtQjtBQUMvQixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBUU8sV0FBUyxvQkFBb0I7QUFDaEMsV0FBTyxLQUFLLDBCQUEwQjtBQUFBLEVBQzFDO0FBT08sV0FBUyxpQkFBaUI7QUFDN0IsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQU9PLFdBQVMsbUJBQW1CO0FBQy9CLFdBQU8sWUFBWSxJQUFJO0FBQUEsRUFDM0I7QUFRTyxXQUFTLG9CQUFvQjtBQUNoQyxXQUFPLEtBQUssMEJBQTBCO0FBQUEsRUFDMUM7QUFRTyxXQUFTLGlCQUFpQjtBQUM3QixXQUFPLEtBQUssdUJBQXVCO0FBQUEsRUFDdkM7QUFXTyxXQUFTLDBCQUEwQixHQUFHLEdBQUcsR0FBRyxHQUFHO0FBQ2xELFFBQUksT0FBTyxLQUFLLFVBQVUsRUFBQyxHQUFHLEtBQUssR0FBRyxHQUFHLEtBQUssR0FBRyxHQUFHLEtBQUssR0FBRyxHQUFHLEtBQUssSUFBRyxDQUFDO0FBQ3hFLFdBQU8sWUFBWSxRQUFRLElBQUk7QUFBQSxFQUNuQzs7O0FDM1FBO0FBQUE7QUFBQTtBQUFBO0FBc0JPLFdBQVMsZUFBZTtBQUMzQixXQUFPLEtBQUsscUJBQXFCO0FBQUEsRUFDckM7OztBQ3hCQTtBQUFBO0FBQUE7QUFBQTtBQUtPLFdBQVMsZUFBZSxLQUFLO0FBQ2xDLFdBQU8sWUFBWSxRQUFRLEdBQUc7QUFBQSxFQUNoQzs7O0FDUEE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQW9CTyxXQUFTLGlCQUFpQixNQUFNO0FBQ25DLFdBQU8sS0FBSywyQkFBMkIsQ0FBQyxJQUFJLENBQUM7QUFBQSxFQUNqRDtBQVNPLFdBQVMsbUJBQW1CO0FBQy9CLFdBQU8sS0FBSyx5QkFBeUI7QUFBQSxFQUN6Qzs7O0FDMUJPLFdBQVMsMEJBQTBCLE9BQU87QUFFN0MsVUFBTSxVQUFVLE1BQU07QUFDdEIsVUFBTSxnQkFBZ0IsT0FBTyxpQkFBaUIsT0FBTztBQUNyRCxVQUFNLDJCQUEyQixjQUFjLGlCQUFpQix1QkFBdUIsRUFBRSxLQUFLO0FBQzlGLFlBQVEsMEJBQTBCO0FBQUEsTUFDOUIsS0FBSztBQUNEO0FBQUEsTUFDSixLQUFLO0FBQ0QsY0FBTSxlQUFlO0FBQ3JCO0FBQUEsTUFDSjtBQUVJLFlBQUksUUFBUSxtQkFBbUI7QUFDM0I7QUFBQSxRQUNKO0FBR0EsY0FBTSxZQUFZLE9BQU8sYUFBYTtBQUN0QyxjQUFNLGVBQWdCLFVBQVUsU0FBUyxFQUFFLFNBQVM7QUFDcEQsWUFBSSxjQUFjO0FBQ2QsbUJBQVMsSUFBSSxHQUFHLElBQUksVUFBVSxZQUFZLEtBQUs7QUFDM0Msa0JBQU0sUUFBUSxVQUFVLFdBQVcsQ0FBQztBQUNwQyxrQkFBTSxRQUFRLE1BQU0sZUFBZTtBQUNuQyxxQkFBUyxJQUFJLEdBQUcsSUFBSSxNQUFNLFFBQVEsS0FBSztBQUNuQyxvQkFBTSxPQUFPLE1BQU07QUFDbkIsa0JBQUksU0FBUyxpQkFBaUIsS0FBSyxNQUFNLEtBQUssR0FBRyxNQUFNLFNBQVM7QUFDNUQ7QUFBQSxjQUNKO0FBQUEsWUFDSjtBQUFBLFVBQ0o7QUFBQSxRQUNKO0FBRUEsWUFBSSxRQUFRLFlBQVksV0FBVyxRQUFRLFlBQVksWUFBWTtBQUMvRCxjQUFJLGdCQUFpQixDQUFDLFFBQVEsWUFBWSxDQUFDLFFBQVEsVUFBVztBQUMxRDtBQUFBLFVBQ0o7QUFBQSxRQUNKO0FBR0EsY0FBTSxlQUFlO0FBQUEsSUFDN0I7QUFBQSxFQUNKOzs7QUM1Qk8sV0FBUyxPQUFPO0FBQ25CLFdBQU8sWUFBWSxHQUFHO0FBQUEsRUFDMUI7QUFFTyxXQUFTLE9BQU87QUFDbkIsV0FBTyxZQUFZLEdBQUc7QUFBQSxFQUMxQjtBQUVPLFdBQVMsT0FBTztBQUNuQixXQUFPLFlBQVksR0FBRztBQUFBLEVBQzFCO0FBRU8sV0FBUyxjQUFjO0FBQzFCLFdBQU8sS0FBSyxvQkFBb0I7QUFBQSxFQUNwQztBQUdBLFNBQU8sVUFBVTtBQUFBLElBQ2IsR0FBRztBQUFBLElBQ0gsR0FBRztBQUFBLElBQ0gsR0FBRztBQUFBLElBQ0gsR0FBRztBQUFBLElBQ0gsR0FBRztBQUFBLElBQ0g7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLEVBQ0o7QUFHQSxTQUFPLFFBQVE7QUFBQSxJQUNYO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0EsT0FBTztBQUFBLE1BQ0gsc0JBQXNCO0FBQUEsTUFDdEIsMkJBQTJCO0FBQUEsTUFDM0IsY0FBYztBQUFBLE1BQ2QsZUFBZTtBQUFBLE1BQ2YsaUJBQWlCO0FBQUEsTUFDakIsWUFBWTtBQUFBLE1BQ1osc0JBQXNCO0FBQUEsTUFDdEIsaUJBQWlCO0FBQUEsTUFDakIsY0FBYztBQUFBLElBQ2xCO0FBQUEsRUFDSjtBQUdBLE1BQUksT0FBTyxlQUFlO0FBQ3RCLFdBQU8sTUFBTSxZQUFZLE9BQU8sYUFBYTtBQUM3QyxXQUFPLE9BQU8sTUFBTTtBQUFBLEVBQ3hCO0FBR0EsTUFBSSxPQUFRO0FBQ1IsV0FBTyxPQUFPO0FBQUEsRUFDbEI7QUFFQSxNQUFJLFdBQVcsU0FBVSxHQUFHO0FBQ3hCLFFBQUksTUFBTSxPQUFPLGlCQUFpQixFQUFFLE1BQU0sRUFBRSxpQkFBaUIsT0FBTyxNQUFNLE1BQU0sZUFBZTtBQUMvRixRQUFJLEtBQUs7QUFDUCxZQUFNLElBQUksS0FBSztBQUFBLElBQ2pCO0FBRUEsUUFBSSxRQUFRLE9BQU8sTUFBTSxNQUFNLGNBQWM7QUFDekMsYUFBTztBQUFBLElBQ1g7QUFFQSxRQUFJLEVBQUUsWUFBWSxHQUFHO0FBRWpCLGFBQU87QUFBQSxJQUNYO0FBRUEsUUFBSSxFQUFFLFdBQVcsR0FBRztBQUVoQixhQUFPO0FBQUEsSUFDWDtBQUVBLFdBQU87QUFBQSxFQUNYO0FBRUEsU0FBTyxNQUFNLHVCQUF1QixTQUFVLFVBQVUsT0FBTztBQUMzRCxXQUFPLE1BQU0sTUFBTSxrQkFBa0I7QUFDckMsV0FBTyxNQUFNLE1BQU0sZUFBZTtBQUFBLEVBQ3RDO0FBRUEsU0FBTyxpQkFBaUIsYUFBYSxDQUFDLE1BQU07QUFHeEMsUUFBSSxPQUFPLE1BQU0sTUFBTSxZQUFZO0FBQy9CLGFBQU8sWUFBWSxZQUFZLE9BQU8sTUFBTSxNQUFNLFVBQVU7QUFDNUQsUUFBRSxlQUFlO0FBQ2pCO0FBQUEsSUFDSjtBQUVBLFFBQUksU0FBUyxDQUFDLEdBQUc7QUFDYixVQUFJLE9BQU8sTUFBTSxNQUFNLHNCQUFzQjtBQUV6QyxZQUFJLEVBQUUsVUFBVSxFQUFFLE9BQU8sZUFBZSxFQUFFLFVBQVUsRUFBRSxPQUFPLGNBQWM7QUFDdkU7QUFBQSxRQUNKO0FBQUEsTUFDSjtBQUNBLFVBQUksT0FBTyxNQUFNLE1BQU0sc0JBQXNCO0FBQ3pDLGVBQU8sTUFBTSxNQUFNLGFBQWE7QUFBQSxNQUNwQyxPQUFPO0FBQ0gsVUFBRSxlQUFlO0FBQ2pCLGVBQU8sWUFBWSxNQUFNO0FBQUEsTUFDN0I7QUFDQTtBQUFBLElBQ0osT0FBTztBQUNILGFBQU8sTUFBTSxNQUFNLGFBQWE7QUFBQSxJQUNwQztBQUFBLEVBQ0osQ0FBQztBQUVELFNBQU8saUJBQWlCLFdBQVcsTUFBTTtBQUNyQyxXQUFPLE1BQU0sTUFBTSxhQUFhO0FBQUEsRUFDcEMsQ0FBQztBQUVELFdBQVMsVUFBVSxRQUFRO0FBQ3ZCLGFBQVMsZ0JBQWdCLE1BQU0sU0FBUyxVQUFVLE9BQU8sTUFBTSxNQUFNO0FBQ3JFLFdBQU8sTUFBTSxNQUFNLGFBQWE7QUFBQSxFQUNwQztBQUVBLFNBQU8saUJBQWlCLGFBQWEsU0FBVSxHQUFHO0FBQzlDLFFBQUksT0FBTyxNQUFNLE1BQU0sWUFBWTtBQUMvQixhQUFPLE1BQU0sTUFBTSxhQUFhO0FBQ2hDLFVBQUksZUFBZSxFQUFFLFlBQVksU0FBWSxFQUFFLFVBQVUsRUFBRTtBQUMzRCxVQUFJLGVBQWUsR0FBRztBQUNsQixlQUFPLFlBQVksTUFBTTtBQUN6QjtBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQ0EsUUFBSSxDQUFDLE9BQU8sTUFBTSxNQUFNLGNBQWM7QUFDbEM7QUFBQSxJQUNKO0FBQ0EsUUFBSSxPQUFPLE1BQU0sTUFBTSxpQkFBaUIsTUFBTTtBQUMxQyxhQUFPLE1BQU0sTUFBTSxnQkFBZ0IsU0FBUyxnQkFBZ0IsTUFBTTtBQUFBLElBQ3RFO0FBQ0EsUUFBSSxPQUFPLGFBQWEsRUFBRSxVQUFVLE9BQU8sTUFBTSxNQUFNLG1CQUFtQixPQUFPLGNBQWMsRUFBRSxVQUFVLE9BQU8sTUFBTSxNQUFNLGlCQUFpQjtBQUMzSSxlQUFTLGdCQUFnQixNQUFNLFNBQVM7QUFBQSxJQUM1QztBQUNBLFFBQUksY0FBYyxPQUFPLGFBQWEsRUFBRSxVQUFVLE9BQU8sTUFBTSxNQUFNO0FBQ3JFLFFBQUksYUFBYSxFQUFFLFVBQVUsT0FBTyxNQUFNLE1BQU07QUFDaEQsUUFBSSxZQUFZLEVBQUUsVUFBVSxPQUFPLE1BQU0sTUFBTTtBQUMvQyxRQUFJLGVBQWUsT0FBTyxjQUFjLEVBQUUsVUFBVSxPQUFPLE1BQU0sTUFBTTtBQUd2RSxRQUFJLENBQUMsY0FBYyxDQUFDLGVBQWUsQ0FBQyxhQUFhLENBQUMsZ0JBQWdCLE9BQU8sTUFBTSxNQUFNLGVBQWUsUUFBVztBQUMzRyxnQkFBVTtBQUFBLElBQ2QsV0FBVyxlQUFlO0FBQWMsZ0JBQVUsV0FBVztBQUFBLGFBQ3BELGNBQWM7QUFBYyxnQkFBVSxXQUFXO0FBQUEsYUFDakQsY0FBYztBQUFXLGdCQUFVLFdBQVc7QUFBQSxhQUM5QyxhQUFhO0FBQWEsZ0JBQVUsV0FBVztBQUFBLGFBQy9DO0FBQVksZ0JBQVUsVUFBVTtBQUFBLGFBQ2hDO0FBQVcsZ0JBQVUsVUFBVTtBQUFBLGFBQy9CO0FBQWMsZ0JBQVUsVUFBVTtBQUFBLGFBQ2xDO0FBQWEsZ0JBQVUsVUFBVTtBQUFBLEVBRTlDLENBQUM7QUFHRCxTQUFPLGlCQUFpQixlQUFlLFNBQVUsR0FBRztBQUVoRCxRQUFJO0FBQU87QUFFWCxRQUFJLE9BQU8sTUFBTSxNQUFNLDJCQUEyQjtBQUM5QyxRQUFFLGVBQWU7QUFBQSxJQUNyQixPQUFPO0FBQ0gsTUFBWSwwQkFBMEIsQ0FBQztBQUFBLElBQzNDO0FBQUEsRUFDSixDQUFDO0FBRUQsU0FBTyxZQUFZLGVBQWU7IiwKICAibmFtZXMiOiBbImV2ZW50TmFtZSJdCn0K +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiZGVza3RvcC9sb2cuanMiLCAiZGVza3RvcC9ldmVudHMuanMiLCAiZGVza3RvcC9jYWxscy5qcyIsICJkZXNrdG9wL2JpbmRpbmdzLmpzIiwgImRlc2t0b3Avd2luZG93LmpzIiwgImRlc2t0b3Avc2NyZWVuLmpzIiwgImRlc2t0b3AvYnJvd3Nlci5qcyIsICJkZXNrdG9wL2NsaXBib2FyZC5qcyIsICJkZXNrdG9wL2RyYWdhbmRkcm9wLmpzIiwgImRlc2t0b3AvY29udGV4dG1lbnUuanMiLCAiZGVza3RvcC9ub3RpZmljYXRpb25zLmpzIiwgImRlc2t0b3AvbWFpbi5qcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiLypcbiBfICAgICAgIF9fICAgICAgXyBfX1xufCB8ICAgICAvIC9fX18gXyhfKSAvX19fX1xufCB8IC98IC8gLyBfXyBgLyAvIC8gX19fL1xufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cblRoZSBlbGVjdHJvbiBhbHRlcm5hdGl2ZSBmb3IgR29cbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcbiovXG5cbi8qIGpzaGludCBlc3ZlcnNpb246IDYgKi9cblxuLyoqXG4gKiBTZW5kcyBhIGxvZyBtZXNzYWdlIHRvIHRoZSBiYWNrZW5kIHdpdGggdGhlIGdpdmVuIGxldmVsICsgbWVzc2FnZVxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSBsZXZlbFxuICogQHBhcmFtIHtzdHJpbmd9IG1lc3NhZ2VcbiAqL1xuZnVuY3Rpb24gc2VuZExvZ01lc3NhZ2UobGV2ZWwsIG1lc3NhZ2UpIHtcblxuXHQvLyBMb2cgTWVzc2FnZSBmb3JtYXQ6XG5cdC8vIGxbdHlwZV1bbWVzc2FnZV1cblx0d2luZG93LldhaWxzSW52b2tlKCdMJyArIGxldmVsICsgbWVzc2FnZSk7XG59XG5cbi8qKlxuICogTG9nIHRoZSBnaXZlbiB0cmFjZSBtZXNzYWdlIHdpdGggdGhlIGJhY2tlbmRcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZVxuICovXG5leHBvcnQgZnVuY3Rpb24gTG9nVHJhY2UobWVzc2FnZSkge1xuXHRzZW5kTG9nTWVzc2FnZSgnVCcsIG1lc3NhZ2UpO1xufVxuXG4vKipcbiAqIExvZyB0aGUgZ2l2ZW4gbWVzc2FnZSB3aXRoIHRoZSBiYWNrZW5kXG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IG1lc3NhZ2VcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIExvZ1ByaW50KG1lc3NhZ2UpIHtcblx0c2VuZExvZ01lc3NhZ2UoJ1AnLCBtZXNzYWdlKTtcbn1cblxuLyoqXG4gKiBMb2cgdGhlIGdpdmVuIGRlYnVnIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBMb2dEZWJ1ZyhtZXNzYWdlKSB7XG5cdHNlbmRMb2dNZXNzYWdlKCdEJywgbWVzc2FnZSk7XG59XG5cbi8qKlxuICogTG9nIHRoZSBnaXZlbiBpbmZvIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBMb2dJbmZvKG1lc3NhZ2UpIHtcblx0c2VuZExvZ01lc3NhZ2UoJ0knLCBtZXNzYWdlKTtcbn1cblxuLyoqXG4gKiBMb2cgdGhlIGdpdmVuIHdhcm5pbmcgbWVzc2FnZSB3aXRoIHRoZSBiYWNrZW5kXG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IG1lc3NhZ2VcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIExvZ1dhcm5pbmcobWVzc2FnZSkge1xuXHRzZW5kTG9nTWVzc2FnZSgnVycsIG1lc3NhZ2UpO1xufVxuXG4vKipcbiAqIExvZyB0aGUgZ2l2ZW4gZXJyb3IgbWVzc2FnZSB3aXRoIHRoZSBiYWNrZW5kXG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IG1lc3NhZ2VcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIExvZ0Vycm9yKG1lc3NhZ2UpIHtcblx0c2VuZExvZ01lc3NhZ2UoJ0UnLCBtZXNzYWdlKTtcbn1cblxuLyoqXG4gKiBMb2cgdGhlIGdpdmVuIGZhdGFsIG1lc3NhZ2Ugd2l0aCB0aGUgYmFja2VuZFxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBMb2dGYXRhbChtZXNzYWdlKSB7XG5cdHNlbmRMb2dNZXNzYWdlKCdGJywgbWVzc2FnZSk7XG59XG5cbi8qKlxuICogU2V0cyB0aGUgTG9nIGxldmVsIHRvIHRoZSBnaXZlbiBsb2cgbGV2ZWxcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge251bWJlcn0gbG9nbGV2ZWxcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFNldExvZ0xldmVsKGxvZ2xldmVsKSB7XG5cdHNlbmRMb2dNZXNzYWdlKCdTJywgbG9nbGV2ZWwpO1xufVxuXG4vLyBMb2cgbGV2ZWxzXG5leHBvcnQgY29uc3QgTG9nTGV2ZWwgPSB7XG5cdFRSQUNFOiAxLFxuXHRERUJVRzogMixcblx0SU5GTzogMyxcblx0V0FSTklORzogNCxcblx0RVJST1I6IDUsXG59O1xuIiwgIi8qXG4gXyAgICAgICBfXyAgICAgIF8gX19cbnwgfCAgICAgLyAvX19fIF8oXykgL19fX19cbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKVxufF9fL3xfXy9cXF9fLF8vXy9fL19fX18vXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XG4qL1xuLyoganNoaW50IGVzdmVyc2lvbjogNiAqL1xuXG4vLyBEZWZpbmVzIGEgc2luZ2xlIGxpc3RlbmVyIHdpdGggYSBtYXhpbXVtIG51bWJlciBvZiB0aW1lcyB0byBjYWxsYmFja1xuXG4vKipcbiAqIFRoZSBMaXN0ZW5lciBjbGFzcyBkZWZpbmVzIGEgbGlzdGVuZXIhIDotKVxuICpcbiAqIEBjbGFzcyBMaXN0ZW5lclxuICovXG5jbGFzcyBMaXN0ZW5lciB7XG4gICAgLyoqXG4gICAgICogQ3JlYXRlcyBhbiBpbnN0YW5jZSBvZiBMaXN0ZW5lci5cbiAgICAgKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXG4gICAgICogQHBhcmFtIHtmdW5jdGlvbn0gY2FsbGJhY2tcbiAgICAgKiBAcGFyYW0ge251bWJlcn0gbWF4Q2FsbGJhY2tzXG4gICAgICogQG1lbWJlcm9mIExpc3RlbmVyXG4gICAgICovXG4gICAgY29uc3RydWN0b3IoZXZlbnROYW1lLCBjYWxsYmFjaywgbWF4Q2FsbGJhY2tzKSB7XG4gICAgICAgIHRoaXMuZXZlbnROYW1lID0gZXZlbnROYW1lO1xuICAgICAgICAvLyBEZWZhdWx0IG9mIC0xIG1lYW5zIGluZmluaXRlXG4gICAgICAgIHRoaXMubWF4Q2FsbGJhY2tzID0gbWF4Q2FsbGJhY2tzIHx8IC0xO1xuICAgICAgICAvLyBDYWxsYmFjayBpbnZva2VzIHRoZSBjYWxsYmFjayB3aXRoIHRoZSBnaXZlbiBkYXRhXG4gICAgICAgIC8vIFJldHVybnMgdHJ1ZSBpZiB0aGlzIGxpc3RlbmVyIHNob3VsZCBiZSBkZXN0cm95ZWRcbiAgICAgICAgdGhpcy5DYWxsYmFjayA9IChkYXRhKSA9PiB7XG4gICAgICAgICAgICBjYWxsYmFjay5hcHBseShudWxsLCBkYXRhKTtcbiAgICAgICAgICAgIC8vIElmIG1heENhbGxiYWNrcyBpcyBpbmZpbml0ZSwgcmV0dXJuIGZhbHNlIChkbyBub3QgZGVzdHJveSlcbiAgICAgICAgICAgIGlmICh0aGlzLm1heENhbGxiYWNrcyA9PT0gLTEpIHtcbiAgICAgICAgICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICAvLyBEZWNyZW1lbnQgbWF4Q2FsbGJhY2tzLiBSZXR1cm4gdHJ1ZSBpZiBub3cgMCwgb3RoZXJ3aXNlIGZhbHNlXG4gICAgICAgICAgICB0aGlzLm1heENhbGxiYWNrcyAtPSAxO1xuICAgICAgICAgICAgcmV0dXJuIHRoaXMubWF4Q2FsbGJhY2tzID09PSAwO1xuICAgICAgICB9O1xuICAgIH1cbn1cblxuZXhwb3J0IGNvbnN0IGV2ZW50TGlzdGVuZXJzID0ge307XG5cbi8qKlxuICogUmVnaXN0ZXJzIGFuIGV2ZW50IGxpc3RlbmVyIHRoYXQgd2lsbCBiZSBpbnZva2VkIGBtYXhDYWxsYmFja3NgIHRpbWVzIGJlZm9yZSBiZWluZyBkZXN0cm95ZWRcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFja1xuICogQHBhcmFtIHtudW1iZXJ9IG1heENhbGxiYWNrc1xuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09uTXVsdGlwbGUoZXZlbnROYW1lLCBjYWxsYmFjaywgbWF4Q2FsbGJhY2tzKSB7XG4gICAgZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXSA9IGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0gfHwgW107XG4gICAgY29uc3QgdGhpc0xpc3RlbmVyID0gbmV3IExpc3RlbmVyKGV2ZW50TmFtZSwgY2FsbGJhY2ssIG1heENhbGxiYWNrcyk7XG4gICAgZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXS5wdXNoKHRoaXNMaXN0ZW5lcik7XG4gICAgcmV0dXJuICgpID0+IGxpc3RlbmVyT2ZmKHRoaXNMaXN0ZW5lcik7XG59XG5cbi8qKlxuICogUmVnaXN0ZXJzIGFuIGV2ZW50IGxpc3RlbmVyIHRoYXQgd2lsbCBiZSBpbnZva2VkIGV2ZXJ5IHRpbWUgdGhlIGV2ZW50IGlzIGVtaXR0ZWRcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFja1xuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09uKGV2ZW50TmFtZSwgY2FsbGJhY2spIHtcbiAgICByZXR1cm4gRXZlbnRzT25NdWx0aXBsZShldmVudE5hbWUsIGNhbGxiYWNrLCAtMSk7XG59XG5cbi8qKlxuICogUmVnaXN0ZXJzIGFuIGV2ZW50IGxpc3RlbmVyIHRoYXQgd2lsbCBiZSBpbnZva2VkIG9uY2UgdGhlbiBkZXN0cm95ZWRcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFja1xuICogQHJldHVybnMge2Z1bmN0aW9ufSBBIGZ1bmN0aW9uIHRvIGNhbmNlbCB0aGUgbGlzdGVuZXJcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09uY2UoZXZlbnROYW1lLCBjYWxsYmFjaykge1xuICAgIHJldHVybiBFdmVudHNPbk11bHRpcGxlKGV2ZW50TmFtZSwgY2FsbGJhY2ssIDEpO1xufVxuXG5mdW5jdGlvbiBub3RpZnlMaXN0ZW5lcnMoZXZlbnREYXRhKSB7XG5cbiAgICAvLyBHZXQgdGhlIGV2ZW50IG5hbWVcbiAgICBsZXQgZXZlbnROYW1lID0gZXZlbnREYXRhLm5hbWU7XG5cbiAgICAvLyBLZWVwIGEgbGlzdCBvZiBsaXN0ZW5lciBpbmRleGVzIHRvIGRlc3Ryb3lcbiAgICBjb25zdCBuZXdFdmVudExpc3RlbmVyTGlzdCA9IGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0/LnNsaWNlKCkgfHwgW107XG5cbiAgICAvLyBDaGVjayBpZiB3ZSBoYXZlIGFueSBsaXN0ZW5lcnMgZm9yIHRoaXMgZXZlbnRcbiAgICBpZiAobmV3RXZlbnRMaXN0ZW5lckxpc3QubGVuZ3RoKSB7XG5cbiAgICAgICAgLy8gSXRlcmF0ZSBsaXN0ZW5lcnNcbiAgICAgICAgZm9yIChsZXQgY291bnQgPSBuZXdFdmVudExpc3RlbmVyTGlzdC5sZW5ndGggLSAxOyBjb3VudCA+PSAwOyBjb3VudCAtPSAxKSB7XG5cbiAgICAgICAgICAgIC8vIEdldCBuZXh0IGxpc3RlbmVyXG4gICAgICAgICAgICBjb25zdCBsaXN0ZW5lciA9IG5ld0V2ZW50TGlzdGVuZXJMaXN0W2NvdW50XTtcblxuICAgICAgICAgICAgbGV0IGRhdGEgPSBldmVudERhdGEuZGF0YTtcblxuICAgICAgICAgICAgLy8gRG8gdGhlIGNhbGxiYWNrXG4gICAgICAgICAgICBjb25zdCBkZXN0cm95ID0gbGlzdGVuZXIuQ2FsbGJhY2soZGF0YSk7XG4gICAgICAgICAgICBpZiAoZGVzdHJveSkge1xuICAgICAgICAgICAgICAgIC8vIGlmIHRoZSBsaXN0ZW5lciBpbmRpY2F0ZWQgdG8gZGVzdHJveSBpdHNlbGYsIGFkZCBpdCB0byB0aGUgZGVzdHJveSBsaXN0XG4gICAgICAgICAgICAgICAgbmV3RXZlbnRMaXN0ZW5lckxpc3Quc3BsaWNlKGNvdW50LCAxKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuXG4gICAgICAgIC8vIFVwZGF0ZSBjYWxsYmFja3Mgd2l0aCBuZXcgbGlzdCBvZiBsaXN0ZW5lcnNcbiAgICAgICAgaWYgKG5ld0V2ZW50TGlzdGVuZXJMaXN0Lmxlbmd0aCA9PT0gMCkge1xuICAgICAgICAgICAgcmVtb3ZlTGlzdGVuZXIoZXZlbnROYW1lKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0gPSBuZXdFdmVudExpc3RlbmVyTGlzdDtcbiAgICAgICAgfVxuICAgIH1cbn1cblxuLyoqXG4gKiBOb3RpZnkgaW5mb3JtcyBmcm9udGVuZCBsaXN0ZW5lcnMgdGhhdCBhbiBldmVudCB3YXMgZW1pdHRlZCB3aXRoIHRoZSBnaXZlbiBkYXRhXG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IG5vdGlmeU1lc3NhZ2UgLSBlbmNvZGVkIG5vdGlmaWNhdGlvbiBtZXNzYWdlXG5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c05vdGlmeShub3RpZnlNZXNzYWdlKSB7XG4gICAgLy8gUGFyc2UgdGhlIG1lc3NhZ2VcbiAgICBsZXQgbWVzc2FnZTtcbiAgICB0cnkge1xuICAgICAgICBtZXNzYWdlID0gSlNPTi5wYXJzZShub3RpZnlNZXNzYWdlKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNvbnN0IGVycm9yID0gJ0ludmFsaWQgSlNPTiBwYXNzZWQgdG8gTm90aWZ5OiAnICsgbm90aWZ5TWVzc2FnZTtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGVycm9yKTtcbiAgICB9XG4gICAgbm90aWZ5TGlzdGVuZXJzKG1lc3NhZ2UpO1xufVxuXG4vKipcbiAqIEVtaXQgYW4gZXZlbnQgd2l0aCB0aGUgZ2l2ZW4gbmFtZSBhbmQgZGF0YVxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7c3RyaW5nfSBldmVudE5hbWVcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c0VtaXQoZXZlbnROYW1lKSB7XG5cbiAgICBjb25zdCBwYXlsb2FkID0ge1xuICAgICAgICBuYW1lOiBldmVudE5hbWUsXG4gICAgICAgIGRhdGE6IFtdLnNsaWNlLmFwcGx5KGFyZ3VtZW50cykuc2xpY2UoMSksXG4gICAgfTtcblxuICAgIC8vIE5vdGlmeSBKUyBsaXN0ZW5lcnNcbiAgICBub3RpZnlMaXN0ZW5lcnMocGF5bG9hZCk7XG5cbiAgICAvLyBOb3RpZnkgR28gbGlzdGVuZXJzXG4gICAgd2luZG93LldhaWxzSW52b2tlKCdFRScgKyBKU09OLnN0cmluZ2lmeShwYXlsb2FkKSk7XG59XG5cbmZ1bmN0aW9uIHJlbW92ZUxpc3RlbmVyKGV2ZW50TmFtZSkge1xuICAgIC8vIFJlbW92ZSBsb2NhbCBsaXN0ZW5lcnNcbiAgICBkZWxldGUgZXZlbnRMaXN0ZW5lcnNbZXZlbnROYW1lXTtcblxuICAgIC8vIE5vdGlmeSBHbyBsaXN0ZW5lcnNcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ0VYJyArIGV2ZW50TmFtZSk7XG59XG5cbi8qKlxuICogT2ZmIHVucmVnaXN0ZXJzIGEgbGlzdGVuZXIgcHJldmlvdXNseSByZWdpc3RlcmVkIHdpdGggT24sXG4gKiBvcHRpb25hbGx5IG11bHRpcGxlIGxpc3RlbmVyZXMgY2FuIGJlIHVucmVnaXN0ZXJlZCB2aWEgYGFkZGl0aW9uYWxFdmVudE5hbWVzYFxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSBldmVudE5hbWVcbiAqIEBwYXJhbSAgey4uLnN0cmluZ30gYWRkaXRpb25hbEV2ZW50TmFtZXNcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09mZihldmVudE5hbWUsIC4uLmFkZGl0aW9uYWxFdmVudE5hbWVzKSB7XG4gICAgcmVtb3ZlTGlzdGVuZXIoZXZlbnROYW1lKVxuXG4gICAgaWYgKGFkZGl0aW9uYWxFdmVudE5hbWVzLmxlbmd0aCA+IDApIHtcbiAgICAgICAgYWRkaXRpb25hbEV2ZW50TmFtZXMuZm9yRWFjaChldmVudE5hbWUgPT4ge1xuICAgICAgICAgICAgcmVtb3ZlTGlzdGVuZXIoZXZlbnROYW1lKVxuICAgICAgICB9KVxuICAgIH1cbn1cblxuLyoqXG4gKiBPZmYgdW5yZWdpc3RlcnMgYWxsIGV2ZW50IGxpc3RlbmVycyBwcmV2aW91c2x5IHJlZ2lzdGVyZWQgd2l0aCBPblxuICovXG4gZXhwb3J0IGZ1bmN0aW9uIEV2ZW50c09mZkFsbCgpIHtcbiAgICBjb25zdCBldmVudE5hbWVzID0gT2JqZWN0LmtleXMoZXZlbnRMaXN0ZW5lcnMpO1xuICAgIGV2ZW50TmFtZXMuZm9yRWFjaChldmVudE5hbWUgPT4ge1xuICAgICAgICByZW1vdmVMaXN0ZW5lcihldmVudE5hbWUpXG4gICAgfSlcbn1cblxuLyoqXG4gKiBsaXN0ZW5lck9mZiB1bnJlZ2lzdGVycyBhIGxpc3RlbmVyIHByZXZpb3VzbHkgcmVnaXN0ZXJlZCB3aXRoIEV2ZW50c09uXG4gKlxuICogQHBhcmFtIHtMaXN0ZW5lcn0gbGlzdGVuZXJcbiAqL1xuIGZ1bmN0aW9uIGxpc3RlbmVyT2ZmKGxpc3RlbmVyKSB7XG4gICAgY29uc3QgZXZlbnROYW1lID0gbGlzdGVuZXIuZXZlbnROYW1lO1xuICAgIGlmIChldmVudExpc3RlbmVyc1tldmVudE5hbWVdID09PSB1bmRlZmluZWQpIHJldHVybjtcblxuICAgIC8vIFJlbW92ZSBsb2NhbCBsaXN0ZW5lclxuICAgIGV2ZW50TGlzdGVuZXJzW2V2ZW50TmFtZV0gPSBldmVudExpc3RlbmVyc1tldmVudE5hbWVdLmZpbHRlcihsID0+IGwgIT09IGxpc3RlbmVyKTtcblxuICAgIC8vIENsZWFuIHVwIGlmIHRoZXJlIGFyZSBubyBldmVudCBsaXN0ZW5lcnMgbGVmdFxuICAgIGlmIChldmVudExpc3RlbmVyc1tldmVudE5hbWVdLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICByZW1vdmVMaXN0ZW5lcihldmVudE5hbWUpO1xuICAgIH1cbn1cbiIsICIvKlxuIF8gICAgICAgX18gICAgICBfIF9fXG58IHwgICAgIC8gL19fXyBfKF8pIC9fX19fXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXG58IHwvIHwvIC8gL18vIC8gLyAoX18gIClcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xuKGMpIExlYSBBbnRob255IDIwMTktcHJlc2VudFxuKi9cbi8qIGpzaGludCBlc3ZlcnNpb246IDYgKi9cblxuZXhwb3J0IGNvbnN0IGNhbGxiYWNrcyA9IHt9O1xuXG4vKipcbiAqIFJldHVybnMgYSBudW1iZXIgZnJvbSB0aGUgbmF0aXZlIGJyb3dzZXIgcmFuZG9tIGZ1bmN0aW9uXG4gKlxuICogQHJldHVybnMgbnVtYmVyXG4gKi9cbmZ1bmN0aW9uIGNyeXB0b1JhbmRvbSgpIHtcblx0dmFyIGFycmF5ID0gbmV3IFVpbnQzMkFycmF5KDEpO1xuXHRyZXR1cm4gd2luZG93LmNyeXB0by5nZXRSYW5kb21WYWx1ZXMoYXJyYXkpWzBdO1xufVxuXG4vKipcbiAqIFJldHVybnMgYSBudW1iZXIgdXNpbmcgZGEgb2xkLXNrb29sIE1hdGguUmFuZG9tXG4gKiBJIGxpa2VzIHRvIGNhbGwgaXQgTE9MUmFuZG9tXG4gKlxuICogQHJldHVybnMgbnVtYmVyXG4gKi9cbmZ1bmN0aW9uIGJhc2ljUmFuZG9tKCkge1xuXHRyZXR1cm4gTWF0aC5yYW5kb20oKSAqIDkwMDcxOTkyNTQ3NDA5OTE7XG59XG5cbi8vIFBpY2sgYSByYW5kb20gbnVtYmVyIGZ1bmN0aW9uIGJhc2VkIG9uIGJyb3dzZXIgY2FwYWJpbGl0eVxudmFyIHJhbmRvbUZ1bmM7XG5pZiAod2luZG93LmNyeXB0bykge1xuXHRyYW5kb21GdW5jID0gY3J5cHRvUmFuZG9tO1xufSBlbHNlIHtcblx0cmFuZG9tRnVuYyA9IGJhc2ljUmFuZG9tO1xufVxuXG5cbi8qKlxuICogQ2FsbCBzZW5kcyBhIG1lc3NhZ2UgdG8gdGhlIGJhY2tlbmQgdG8gY2FsbCB0aGUgYmluZGluZyB3aXRoIHRoZVxuICogZ2l2ZW4gZGF0YS4gQSBwcm9taXNlIGlzIHJldHVybmVkIGFuZCB3aWxsIGJlIGNvbXBsZXRlZCB3aGVuIHRoZVxuICogYmFja2VuZCByZXNwb25kcy4gVGhpcyB3aWxsIGJlIHJlc29sdmVkIHdoZW4gdGhlIGNhbGwgd2FzIHN1Y2Nlc3NmdWxcbiAqIG9yIHJlamVjdGVkIGlmIGFuIGVycm9yIGlzIHBhc3NlZCBiYWNrLlxuICogVGhlcmUgaXMgYSB0aW1lb3V0IG1lY2hhbmlzbS4gSWYgdGhlIGNhbGwgZG9lc24ndCByZXNwb25kIGluIHRoZSBnaXZlblxuICogdGltZSAoaW4gbWlsbGlzZWNvbmRzKSB0aGVuIHRoZSBwcm9taXNlIGlzIHJlamVjdGVkLlxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7c3RyaW5nfSBuYW1lXG4gKiBAcGFyYW0ge2FueT19IGFyZ3NcbiAqIEBwYXJhbSB7bnVtYmVyPX0gdGltZW91dFxuICogQHJldHVybnNcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIENhbGwobmFtZSwgYXJncywgdGltZW91dCkge1xuXG5cdC8vIFRpbWVvdXQgaW5maW5pdGUgYnkgZGVmYXVsdFxuXHRpZiAodGltZW91dCA9PSBudWxsKSB7XG5cdFx0dGltZW91dCA9IDA7XG5cdH1cblxuXHQvLyBDcmVhdGUgYSBwcm9taXNlXG5cdHJldHVybiBuZXcgUHJvbWlzZShmdW5jdGlvbiAocmVzb2x2ZSwgcmVqZWN0KSB7XG5cblx0XHQvLyBDcmVhdGUgYSB1bmlxdWUgY2FsbGJhY2tJRFxuXHRcdHZhciBjYWxsYmFja0lEO1xuXHRcdGRvIHtcblx0XHRcdGNhbGxiYWNrSUQgPSBuYW1lICsgJy0nICsgcmFuZG9tRnVuYygpO1xuXHRcdH0gd2hpbGUgKGNhbGxiYWNrc1tjYWxsYmFja0lEXSk7XG5cblx0XHR2YXIgdGltZW91dEhhbmRsZTtcblx0XHQvLyBTZXQgdGltZW91dFxuXHRcdGlmICh0aW1lb3V0ID4gMCkge1xuXHRcdFx0dGltZW91dEhhbmRsZSA9IHNldFRpbWVvdXQoZnVuY3Rpb24gKCkge1xuXHRcdFx0XHRyZWplY3QoRXJyb3IoJ0NhbGwgdG8gJyArIG5hbWUgKyAnIHRpbWVkIG91dC4gUmVxdWVzdCBJRDogJyArIGNhbGxiYWNrSUQpKTtcblx0XHRcdH0sIHRpbWVvdXQpO1xuXHRcdH1cblxuXHRcdC8vIFN0b3JlIGNhbGxiYWNrXG5cdFx0Y2FsbGJhY2tzW2NhbGxiYWNrSURdID0ge1xuXHRcdFx0dGltZW91dEhhbmRsZTogdGltZW91dEhhbmRsZSxcblx0XHRcdHJlamVjdDogcmVqZWN0LFxuXHRcdFx0cmVzb2x2ZTogcmVzb2x2ZVxuXHRcdH07XG5cblx0XHR0cnkge1xuXHRcdFx0Y29uc3QgcGF5bG9hZCA9IHtcblx0XHRcdFx0bmFtZSxcblx0XHRcdFx0YXJncyxcblx0XHRcdFx0Y2FsbGJhY2tJRCxcblx0XHRcdH07XG5cbiAgICAgICAgICAgIC8vIE1ha2UgdGhlIGNhbGxcbiAgICAgICAgICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnQycgKyBKU09OLnN0cmluZ2lmeShwYXlsb2FkKSk7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZVxuICAgICAgICAgICAgY29uc29sZS5lcnJvcihlKTtcbiAgICAgICAgfVxuICAgIH0pO1xufVxuXG53aW5kb3cuT2JmdXNjYXRlZENhbGwgPSAoaWQsIGFyZ3MsIHRpbWVvdXQpID0+IHtcblxuICAgIC8vIFRpbWVvdXQgaW5maW5pdGUgYnkgZGVmYXVsdFxuICAgIGlmICh0aW1lb3V0ID09IG51bGwpIHtcbiAgICAgICAgdGltZW91dCA9IDA7XG4gICAgfVxuXG4gICAgLy8gQ3JlYXRlIGEgcHJvbWlzZVxuICAgIHJldHVybiBuZXcgUHJvbWlzZShmdW5jdGlvbiAocmVzb2x2ZSwgcmVqZWN0KSB7XG5cbiAgICAgICAgLy8gQ3JlYXRlIGEgdW5pcXVlIGNhbGxiYWNrSURcbiAgICAgICAgdmFyIGNhbGxiYWNrSUQ7XG4gICAgICAgIGRvIHtcbiAgICAgICAgICAgIGNhbGxiYWNrSUQgPSBpZCArICctJyArIHJhbmRvbUZ1bmMoKTtcbiAgICAgICAgfSB3aGlsZSAoY2FsbGJhY2tzW2NhbGxiYWNrSURdKTtcblxuICAgICAgICB2YXIgdGltZW91dEhhbmRsZTtcbiAgICAgICAgLy8gU2V0IHRpbWVvdXRcbiAgICAgICAgaWYgKHRpbWVvdXQgPiAwKSB7XG4gICAgICAgICAgICB0aW1lb3V0SGFuZGxlID0gc2V0VGltZW91dChmdW5jdGlvbiAoKSB7XG4gICAgICAgICAgICAgICAgcmVqZWN0KEVycm9yKCdDYWxsIHRvIG1ldGhvZCAnICsgaWQgKyAnIHRpbWVkIG91dC4gUmVxdWVzdCBJRDogJyArIGNhbGxiYWNrSUQpKTtcbiAgICAgICAgICAgIH0sIHRpbWVvdXQpO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gU3RvcmUgY2FsbGJhY2tcbiAgICAgICAgY2FsbGJhY2tzW2NhbGxiYWNrSURdID0ge1xuICAgICAgICAgICAgdGltZW91dEhhbmRsZTogdGltZW91dEhhbmRsZSxcbiAgICAgICAgICAgIHJlamVjdDogcmVqZWN0LFxuICAgICAgICAgICAgcmVzb2x2ZTogcmVzb2x2ZVxuICAgICAgICB9O1xuXG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0ge1xuXHRcdFx0XHRpZCxcblx0XHRcdFx0YXJncyxcblx0XHRcdFx0Y2FsbGJhY2tJRCxcblx0XHRcdH07XG5cbiAgICAgICAgICAgIC8vIE1ha2UgdGhlIGNhbGxcbiAgICAgICAgICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnYycgKyBKU09OLnN0cmluZ2lmeShwYXlsb2FkKSk7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZVxuICAgICAgICAgICAgY29uc29sZS5lcnJvcihlKTtcbiAgICAgICAgfVxuICAgIH0pO1xufTtcblxuXG4vKipcbiAqIENhbGxlZCBieSB0aGUgYmFja2VuZCB0byByZXR1cm4gZGF0YSB0byBhIHByZXZpb3VzbHkgY2FsbGVkXG4gKiBiaW5kaW5nIGludm9jYXRpb25cbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gaW5jb21pbmdNZXNzYWdlXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBDYWxsYmFjayhpbmNvbWluZ01lc3NhZ2UpIHtcblx0Ly8gUGFyc2UgdGhlIG1lc3NhZ2Vcblx0bGV0IG1lc3NhZ2U7XG5cdHRyeSB7XG5cdFx0bWVzc2FnZSA9IEpTT04ucGFyc2UoaW5jb21pbmdNZXNzYWdlKTtcblx0fSBjYXRjaCAoZSkge1xuXHRcdGNvbnN0IGVycm9yID0gYEludmFsaWQgSlNPTiBwYXNzZWQgdG8gY2FsbGJhY2s6ICR7ZS5tZXNzYWdlfS4gTWVzc2FnZTogJHtpbmNvbWluZ01lc3NhZ2V9YDtcblx0XHRydW50aW1lLkxvZ0RlYnVnKGVycm9yKTtcblx0XHR0aHJvdyBuZXcgRXJyb3IoZXJyb3IpO1xuXHR9XG5cdGxldCBjYWxsYmFja0lEID0gbWVzc2FnZS5jYWxsYmFja2lkO1xuXHRsZXQgY2FsbGJhY2tEYXRhID0gY2FsbGJhY2tzW2NhbGxiYWNrSURdO1xuXHRpZiAoIWNhbGxiYWNrRGF0YSkge1xuXHRcdGNvbnN0IGVycm9yID0gYENhbGxiYWNrICcke2NhbGxiYWNrSUR9JyBub3QgcmVnaXN0ZXJlZCEhIWA7XG5cdFx0Y29uc29sZS5lcnJvcihlcnJvcik7IC8vIGVzbGludC1kaXNhYmxlLWxpbmVcblx0XHR0aHJvdyBuZXcgRXJyb3IoZXJyb3IpO1xuXHR9XG5cdGNsZWFyVGltZW91dChjYWxsYmFja0RhdGEudGltZW91dEhhbmRsZSk7XG5cblx0ZGVsZXRlIGNhbGxiYWNrc1tjYWxsYmFja0lEXTtcblxuXHRpZiAobWVzc2FnZS5lcnJvcikge1xuXHRcdGNhbGxiYWNrRGF0YS5yZWplY3QobWVzc2FnZS5lcnJvcik7XG5cdH0gZWxzZSB7XG5cdFx0Y2FsbGJhY2tEYXRhLnJlc29sdmUobWVzc2FnZS5yZXN1bHQpO1xuXHR9XG59XG4iLCAiLypcbiBfICAgICAgIF9fICAgICAgXyBfXyAgICBcbnwgfCAgICAgLyAvX19fIF8oXykgL19fX19cbnwgfCAvfCAvIC8gX18gYC8gLyAvIF9fXy9cbnwgfC8gfC8gLyAvXy8gLyAvIChfXyAgKSBcbnxfXy98X18vXFxfXyxfL18vXy9fX19fLyAgXG5UaGUgZWxlY3Ryb24gYWx0ZXJuYXRpdmUgZm9yIEdvXG4oYykgTGVhIEFudGhvbnkgMjAxOS1wcmVzZW50XG4qL1xuLyoganNoaW50IGVzdmVyc2lvbjogNiAqL1xuXG5pbXBvcnQge0NhbGx9IGZyb20gJy4vY2FsbHMnO1xuXG4vLyBUaGlzIGlzIHdoZXJlIHdlIGJpbmQgZ28gbWV0aG9kIHdyYXBwZXJzXG53aW5kb3cuZ28gPSB7fTtcblxuZXhwb3J0IGZ1bmN0aW9uIFNldEJpbmRpbmdzKGJpbmRpbmdzTWFwKSB7XG5cdHRyeSB7XG5cdFx0YmluZGluZ3NNYXAgPSBKU09OLnBhcnNlKGJpbmRpbmdzTWFwKTtcblx0fSBjYXRjaCAoZSkge1xuXHRcdGNvbnNvbGUuZXJyb3IoZSk7XG5cdH1cblxuXHQvLyBJbml0aWFsaXNlIHRoZSBiaW5kaW5ncyBtYXBcblx0d2luZG93LmdvID0gd2luZG93LmdvIHx8IHt9O1xuXG5cdC8vIEl0ZXJhdGUgcGFja2FnZSBuYW1lc1xuXHRPYmplY3Qua2V5cyhiaW5kaW5nc01hcCkuZm9yRWFjaCgocGFja2FnZU5hbWUpID0+IHtcblxuXHRcdC8vIENyZWF0ZSBpbm5lciBtYXAgaWYgaXQgZG9lc24ndCBleGlzdFxuXHRcdHdpbmRvdy5nb1twYWNrYWdlTmFtZV0gPSB3aW5kb3cuZ29bcGFja2FnZU5hbWVdIHx8IHt9O1xuXG5cdFx0Ly8gSXRlcmF0ZSBzdHJ1Y3QgbmFtZXNcblx0XHRPYmplY3Qua2V5cyhiaW5kaW5nc01hcFtwYWNrYWdlTmFtZV0pLmZvckVhY2goKHN0cnVjdE5hbWUpID0+IHtcblxuXHRcdFx0Ly8gQ3JlYXRlIGlubmVyIG1hcCBpZiBpdCBkb2Vzbid0IGV4aXN0XG5cdFx0XHR3aW5kb3cuZ29bcGFja2FnZU5hbWVdW3N0cnVjdE5hbWVdID0gd2luZG93LmdvW3BhY2thZ2VOYW1lXVtzdHJ1Y3ROYW1lXSB8fCB7fTtcblxuXHRcdFx0T2JqZWN0LmtleXMoYmluZGluZ3NNYXBbcGFja2FnZU5hbWVdW3N0cnVjdE5hbWVdKS5mb3JFYWNoKChtZXRob2ROYW1lKSA9PiB7XG5cblx0XHRcdFx0d2luZG93LmdvW3BhY2thZ2VOYW1lXVtzdHJ1Y3ROYW1lXVttZXRob2ROYW1lXSA9IGZ1bmN0aW9uICgpIHtcblxuXHRcdFx0XHRcdC8vIE5vIHRpbWVvdXQgYnkgZGVmYXVsdFxuXHRcdFx0XHRcdGxldCB0aW1lb3V0ID0gMDtcblxuXHRcdFx0XHRcdC8vIEFjdHVhbCBmdW5jdGlvblxuXHRcdFx0XHRcdGZ1bmN0aW9uIGR5bmFtaWMoKSB7XG5cdFx0XHRcdFx0XHRjb25zdCBhcmdzID0gW10uc2xpY2UuY2FsbChhcmd1bWVudHMpO1xuXHRcdFx0XHRcdFx0cmV0dXJuIENhbGwoW3BhY2thZ2VOYW1lLCBzdHJ1Y3ROYW1lLCBtZXRob2ROYW1lXS5qb2luKCcuJyksIGFyZ3MsIHRpbWVvdXQpO1xuXHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdC8vIEFsbG93IHNldHRpbmcgdGltZW91dCB0byBmdW5jdGlvblxuXHRcdFx0XHRcdGR5bmFtaWMuc2V0VGltZW91dCA9IGZ1bmN0aW9uIChuZXdUaW1lb3V0KSB7XG5cdFx0XHRcdFx0XHR0aW1lb3V0ID0gbmV3VGltZW91dDtcblx0XHRcdFx0XHR9O1xuXG5cdFx0XHRcdFx0Ly8gQWxsb3cgZ2V0dGluZyB0aW1lb3V0IHRvIGZ1bmN0aW9uXG5cdFx0XHRcdFx0ZHluYW1pYy5nZXRUaW1lb3V0ID0gZnVuY3Rpb24gKCkge1xuXHRcdFx0XHRcdFx0cmV0dXJuIHRpbWVvdXQ7XG5cdFx0XHRcdFx0fTtcblxuXHRcdFx0XHRcdHJldHVybiBkeW5hbWljO1xuXHRcdFx0XHR9KCk7XG5cdFx0XHR9KTtcblx0XHR9KTtcblx0fSk7XG59XG4iLCAiLypcbiBfXHQgICBfX1x0ICBfIF9fXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xufCB8IC98IC8gLyBfXyBgLyAvIC8gX19fL1xufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cblRoZSBlbGVjdHJvbiBhbHRlcm5hdGl2ZSBmb3IgR29cbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcbiovXG5cbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cblxuXG5pbXBvcnQge0NhbGx9IGZyb20gXCIuL2NhbGxzXCI7XG5cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dSZWxvYWQoKSB7XG4gICAgd2luZG93LmxvY2F0aW9uLnJlbG9hZCgpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gV2luZG93UmVsb2FkQXBwKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV1InKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFN5c3RlbURlZmF1bHRUaGVtZSgpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dBU0RUJyk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dTZXRMaWdodFRoZW1lKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0FMVCcpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2V0RGFya1RoZW1lKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV0FEVCcpO1xufVxuXG4vKipcbiAqIFBsYWNlIHRoZSB3aW5kb3cgaW4gdGhlIGNlbnRlciBvZiB0aGUgc2NyZWVuXG4gKlxuICogQGV4cG9ydFxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93Q2VudGVyKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV2MnKTtcbn1cblxuLyoqXG4gKiBTZXRzIHRoZSB3aW5kb3cgdGl0bGVcbiAqXG4gKiBAcGFyYW0ge3N0cmluZ30gdGl0bGVcbiAqIEBleHBvcnRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFRpdGxlKHRpdGxlKSB7XG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXVCcgKyB0aXRsZSk7XG59XG5cbi8qKlxuICogTWFrZXMgdGhlIHdpbmRvdyBnbyBmdWxsc2NyZWVuXG4gKlxuICogQGV4cG9ydFxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93RnVsbHNjcmVlbigpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dGJyk7XG59XG5cbi8qKlxuICogUmV2ZXJ0cyB0aGUgd2luZG93IGZyb20gZnVsbHNjcmVlblxuICpcbiAqIEBleHBvcnRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1VuZnVsbHNjcmVlbigpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dmJyk7XG59XG5cbi8qKlxuICogUmV0dXJucyB0aGUgc3RhdGUgb2YgdGhlIHdpbmRvdywgaS5lLiB3aGV0aGVyIHRoZSB3aW5kb3cgaXMgaW4gZnVsbCBzY3JlZW4gbW9kZSBvciBub3QuXG4gKlxuICogQGV4cG9ydFxuICogQHJldHVybiB7UHJvbWlzZTxib29sZWFuPn0gVGhlIHN0YXRlIG9mIHRoZSB3aW5kb3dcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd0lzRnVsbHNjcmVlbigpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dJc0Z1bGxzY3JlZW5cIik7XG59XG5cbi8qKlxuICogU2V0IHRoZSBTaXplIG9mIHRoZSB3aW5kb3dcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge251bWJlcn0gd2lkdGhcbiAqIEBwYXJhbSB7bnVtYmVyfSBoZWlnaHRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldFNpemUod2lkdGgsIGhlaWdodCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV3M6JyArIHdpZHRoICsgJzonICsgaGVpZ2h0KTtcbn1cblxuLyoqXG4gKiBHZXQgdGhlIFNpemUgb2YgdGhlIHdpbmRvd1xuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8e3c6IG51bWJlciwgaDogbnVtYmVyfT59IFRoZSBzaXplIG9mIHRoZSB3aW5kb3dcblxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93R2V0U2l6ZSgpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dHZXRTaXplXCIpO1xufVxuXG4vKipcbiAqIFNldCB0aGUgbWF4aW11bSBzaXplIG9mIHRoZSB3aW5kb3dcbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge251bWJlcn0gd2lkdGhcbiAqIEBwYXJhbSB7bnVtYmVyfSBoZWlnaHRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldE1heFNpemUod2lkdGgsIGhlaWdodCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV1o6JyArIHdpZHRoICsgJzonICsgaGVpZ2h0KTtcbn1cblxuLyoqXG4gKiBTZXQgdGhlIG1pbmltdW0gc2l6ZSBvZiB0aGUgd2luZG93XG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtudW1iZXJ9IHdpZHRoXG4gKiBAcGFyYW0ge251bWJlcn0gaGVpZ2h0XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dTZXRNaW5TaXplKHdpZHRoLCBoZWlnaHQpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1d6OicgKyB3aWR0aCArICc6JyArIGhlaWdodCk7XG59XG5cblxuXG4vKipcbiAqIFNldCB0aGUgd2luZG93IEFsd2F5c09uVG9wIG9yIG5vdCBvbiB0b3BcbiAqXG4gKiBAZXhwb3J0XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dTZXRBbHdheXNPblRvcChiKSB7XG5cbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dBVFA6JyArIChiID8gJzEnIDogJzAnKSk7XG59XG5cblxuXG5cbi8qKlxuICogU2V0IHRoZSBQb3NpdGlvbiBvZiB0aGUgd2luZG93XG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtudW1iZXJ9IHhcbiAqIEBwYXJhbSB7bnVtYmVyfSB5XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dTZXRQb3NpdGlvbih4LCB5KSB7XG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXcDonICsgeCArICc6JyArIHkpO1xufVxuXG4vKipcbiAqIEdldCB0aGUgUG9zaXRpb24gb2YgdGhlIHdpbmRvd1xuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8e3g6IG51bWJlciwgeTogbnVtYmVyfT59IFRoZSBwb3NpdGlvbiBvZiB0aGUgd2luZG93XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dHZXRQb3NpdGlvbigpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dHZXRQb3NcIik7XG59XG5cbi8qKlxuICogSGlkZSB0aGUgV2luZG93XG4gKlxuICogQGV4cG9ydFxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93SGlkZSgpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dIJyk7XG59XG5cbi8qKlxuICogU2hvdyB0aGUgV2luZG93XG4gKlxuICogQGV4cG9ydFxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93U2hvdygpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dTJyk7XG59XG5cbi8qKlxuICogTWF4aW1pc2UgdGhlIFdpbmRvd1xuICpcbiAqIEBleHBvcnRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd01heGltaXNlKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV00nKTtcbn1cblxuLyoqXG4gKiBUb2dnbGUgdGhlIE1heGltaXNlIG9mIHRoZSBXaW5kb3dcbiAqXG4gKiBAZXhwb3J0XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dUb2dnbGVNYXhpbWlzZSgpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1d0Jyk7XG59XG5cbi8qKlxuICogVW5tYXhpbWlzZSB0aGUgV2luZG93XG4gKlxuICogQGV4cG9ydFxuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93VW5tYXhpbWlzZSgpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1dVJyk7XG59XG5cbi8qKlxuICogUmV0dXJucyB0aGUgc3RhdGUgb2YgdGhlIHdpbmRvdywgaS5lLiB3aGV0aGVyIHRoZSB3aW5kb3cgaXMgbWF4aW1pc2VkIG9yIG5vdC5cbiAqXG4gKiBAZXhwb3J0XG4gKiBAcmV0dXJuIHtQcm9taXNlPGJvb2xlYW4+fSBUaGUgc3RhdGUgb2YgdGhlIHdpbmRvd1xuICovXG5leHBvcnQgZnVuY3Rpb24gV2luZG93SXNNYXhpbWlzZWQoKSB7XG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6V2luZG93SXNNYXhpbWlzZWRcIik7XG59XG5cbi8qKlxuICogTWluaW1pc2UgdGhlIFdpbmRvd1xuICpcbiAqIEBleHBvcnRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd01pbmltaXNlKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV20nKTtcbn1cblxuLyoqXG4gKiBVbm1pbmltaXNlIHRoZSBXaW5kb3dcbiAqXG4gKiBAZXhwb3J0XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dVbm1pbmltaXNlKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnV3UnKTtcbn1cblxuLyoqXG4gKiBSZXR1cm5zIHRoZSBzdGF0ZSBvZiB0aGUgd2luZG93LCBpLmUuIHdoZXRoZXIgdGhlIHdpbmRvdyBpcyBtaW5pbWlzZWQgb3Igbm90LlxuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8Ym9vbGVhbj59IFRoZSBzdGF0ZSBvZiB0aGUgd2luZG93XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dJc01pbmltaXNlZCgpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dJc01pbmltaXNlZFwiKTtcbn1cblxuLyoqXG4gKiBSZXR1cm5zIHRoZSBzdGF0ZSBvZiB0aGUgd2luZG93LCBpLmUuIHdoZXRoZXIgdGhlIHdpbmRvdyBpcyBub3JtYWwgb3Igbm90LlxuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8Ym9vbGVhbj59IFRoZSBzdGF0ZSBvZiB0aGUgd2luZG93XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBXaW5kb3dJc05vcm1hbCgpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpXaW5kb3dJc05vcm1hbFwiKTtcbn1cblxuLyoqXG4gKiBTZXRzIHRoZSBiYWNrZ3JvdW5kIGNvbG91ciBvZiB0aGUgd2luZG93XG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtudW1iZXJ9IFIgUmVkXG4gKiBAcGFyYW0ge251bWJlcn0gRyBHcmVlblxuICogQHBhcmFtIHtudW1iZXJ9IEIgQmx1ZVxuICogQHBhcmFtIHtudW1iZXJ9IEEgQWxwaGFcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFdpbmRvd1NldEJhY2tncm91bmRDb2xvdXIoUiwgRywgQiwgQSkge1xuICAgIGxldCByZ2JhID0gSlNPTi5zdHJpbmdpZnkoe3I6IFIgfHwgMCwgZzogRyB8fCAwLCBiOiBCIHx8IDAsIGE6IEEgfHwgMjU1fSk7XG4gICAgd2luZG93LldhaWxzSW52b2tlKCdXcjonICsgcmdiYSk7XG59XG5cbiIsICIvKlxuIF9cdCAgIF9fXHQgIF8gX19cbnwgfFx0IC8gL19fXyBfKF8pIC9fX19fXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXG58IHwvIHwvIC8gL18vIC8gLyAoX18gIClcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xuKGMpIExlYSBBbnRob255IDIwMTktcHJlc2VudFxuKi9cblxuLyoganNoaW50IGVzdmVyc2lvbjogOSAqL1xuXG5cbmltcG9ydCB7Q2FsbH0gZnJvbSBcIi4vY2FsbHNcIjtcblxuXG4vKipcbiAqIEdldHMgdGhlIGFsbCBzY3JlZW5zLiBDYWxsIHRoaXMgYW5ldyBlYWNoIHRpbWUgeW91IHdhbnQgdG8gcmVmcmVzaCBkYXRhIGZyb20gdGhlIHVuZGVybHlpbmcgd2luZG93aW5nIHN5c3RlbS5cbiAqIEBleHBvcnRcbiAqIEB0eXBlZGVmIHtpbXBvcnQoJy4uL3dyYXBwZXIvcnVudGltZScpLlNjcmVlbn0gU2NyZWVuXG4gKiBAcmV0dXJuIHtQcm9taXNlPHtTY3JlZW5bXX0+fSBUaGUgc2NyZWVuc1xuICovXG5leHBvcnQgZnVuY3Rpb24gU2NyZWVuR2V0QWxsKCkge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOlNjcmVlbkdldEFsbFwiKTtcbn1cbiIsICIvKipcbiAqIEBkZXNjcmlwdGlvbjogVXNlIHRoZSBzeXN0ZW0gZGVmYXVsdCBicm93c2VyIHRvIG9wZW4gdGhlIHVybFxuICogQHBhcmFtIHtzdHJpbmd9IHVybCBcbiAqIEByZXR1cm4ge3ZvaWR9XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBCcm93c2VyT3BlblVSTCh1cmwpIHtcbiAgd2luZG93LldhaWxzSW52b2tlKCdCTzonICsgdXJsKTtcbn0iLCAiLypcbiBfXHQgICBfX1x0ICBfIF9fXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xufCB8IC98IC8gLyBfXyBgLyAvIC8gX19fL1xufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cblRoZSBlbGVjdHJvbiBhbHRlcm5hdGl2ZSBmb3IgR29cbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcbiovXG5cbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cblxuaW1wb3J0IHtDYWxsfSBmcm9tIFwiLi9jYWxsc1wiO1xuXG4vKipcbiAqIFNldCB0aGUgU2l6ZSBvZiB0aGUgd2luZG93XG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IHRleHRcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIENsaXBib2FyZFNldFRleHQodGV4dCkge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOkNsaXBib2FyZFNldFRleHRcIiwgW3RleHRdKTtcbn1cblxuLyoqXG4gKiBHZXQgdGhlIHRleHQgY29udGVudCBvZiB0aGUgY2xpcGJvYXJkXG4gKlxuICogQGV4cG9ydFxuICogQHJldHVybiB7UHJvbWlzZTx7c3RyaW5nfT59IFRleHQgY29udGVudCBvZiB0aGUgY2xpcGJvYXJkXG5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIENsaXBib2FyZEdldFRleHQoKSB7XG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6Q2xpcGJvYXJkR2V0VGV4dFwiKTtcbn0iLCAiLypcbiBfXHQgICBfX1x0ICBfIF9fXG58IHxcdCAvIC9fX18gXyhfKSAvX19fX1xufCB8IC98IC8gLyBfXyBgLyAvIC8gX19fL1xufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cblRoZSBlbGVjdHJvbiBhbHRlcm5hdGl2ZSBmb3IgR29cbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcbiovXG5cbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cblxuaW1wb3J0IHtFdmVudHNPbiwgRXZlbnRzT2ZmfSBmcm9tIFwiLi9ldmVudHNcIjtcblxuY29uc3QgZmxhZ3MgPSB7XG4gICAgcmVnaXN0ZXJlZDogZmFsc2UsXG4gICAgZGVmYXVsdFVzZURyb3BUYXJnZXQ6IHRydWUsXG4gICAgdXNlRHJvcFRhcmdldDogdHJ1ZSxcbiAgICBuZXh0RGVhY3RpdmF0ZTogbnVsbCxcbiAgICBuZXh0RGVhY3RpdmF0ZVRpbWVvdXQ6IG51bGwsXG59O1xuXG5jb25zdCBEUk9QX1RBUkdFVF9BQ1RJVkUgPSBcIndhaWxzLWRyb3AtdGFyZ2V0LWFjdGl2ZVwiO1xuXG4vKipcbiAqIGNoZWNrU3R5bGVEcm9wVGFyZ2V0IGNoZWNrcyBpZiB0aGUgc3R5bGUgaGFzIHRoZSBkcm9wIHRhcmdldCBhdHRyaWJ1dGVcbiAqIFxuICogQHBhcmFtIHtDU1NTdHlsZURlY2xhcmF0aW9ufSBzdHlsZSBcbiAqIEByZXR1cm5zIFxuICovXG5mdW5jdGlvbiBjaGVja1N0eWxlRHJvcFRhcmdldChzdHlsZSkge1xuICAgIGNvbnN0IGNzc0Ryb3BWYWx1ZSA9IHN0eWxlLmdldFByb3BlcnR5VmFsdWUod2luZG93LndhaWxzLmZsYWdzLmNzc0Ryb3BQcm9wZXJ0eSkudHJpbSgpO1xuICAgIGlmIChjc3NEcm9wVmFsdWUpIHtcbiAgICAgICAgaWYgKGNzc0Ryb3BWYWx1ZSA9PT0gd2luZG93LndhaWxzLmZsYWdzLmNzc0Ryb3BWYWx1ZSkge1xuICAgICAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgICAgIH1cbiAgICAgICAgLy8gaWYgdGhlIGVsZW1lbnQgaGFzIHRoZSBkcm9wIHRhcmdldCBhdHRyaWJ1dGUsIGJ1dCBcbiAgICAgICAgLy8gdGhlIHZhbHVlIGlzIG5vdCBjb3JyZWN0LCB0ZXJtaW5hdGUgZmluZGluZyBwcm9jZXNzLlxuICAgICAgICAvLyBUaGlzIGNhbiBiZSB1c2VmdWwgdG8gYmxvY2sgc29tZSBjaGlsZCBlbGVtZW50cyBmcm9tIGJlaW5nIGRyb3AgdGFyZ2V0cy5cbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gZmFsc2U7XG59XG5cbi8qKlxuICogb25EcmFnT3ZlciBpcyBjYWxsZWQgd2hlbiB0aGUgZHJhZ292ZXIgZXZlbnQgaXMgZW1pdHRlZC5cbiAqIEBwYXJhbSB7RHJhZ0V2ZW50fSBlXG4gKiBAcmV0dXJuc1xuICovXG5mdW5jdGlvbiBvbkRyYWdPdmVyKGUpIHtcbiAgICAvLyBDaGVjayBpZiB0aGlzIGlzIGFuIGV4dGVybmFsIGZpbGUgZHJvcCBvciBpbnRlcm5hbCBIVE1MIGRyYWdcbiAgICAvLyBFeHRlcm5hbCBmaWxlIGRyb3BzIHdpbGwgaGF2ZSBcIkZpbGVzXCIgaW4gdGhlIHR5cGVzIGFycmF5XG4gICAgLy8gSW50ZXJuYWwgSFRNTCBkcmFncyB0eXBpY2FsbHkgaGF2ZSBcInRleHQvcGxhaW5cIiwgXCJ0ZXh0L2h0bWxcIiBvciBjdXN0b20gdHlwZXNcbiAgICBjb25zdCBpc0ZpbGVEcm9wID0gZS5kYXRhVHJhbnNmZXIudHlwZXMuaW5jbHVkZXMoXCJGaWxlc1wiKTtcblxuICAgIC8vIE9ubHkgaGFuZGxlIGV4dGVybmFsIGZpbGUgZHJvcHMsIGxldCBpbnRlcm5hbCBIVE1MNSBkcmFnLWFuZC1kcm9wIHdvcmsgbm9ybWFsbHlcbiAgICBpZiAoIWlzRmlsZURyb3ApIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIC8vIEFMV0FZUyBwcmV2ZW50IGRlZmF1bHQgZm9yIGZpbGUgZHJvcHMgdG8gc3RvcCBicm93c2VyIG5hdmlnYXRpb25cbiAgICBlLnByZXZlbnREZWZhdWx0KCk7XG4gICAgZS5kYXRhVHJhbnNmZXIuZHJvcEVmZmVjdCA9ICdjb3B5JztcblxuICAgIGlmICghd2luZG93LndhaWxzLmZsYWdzLmVuYWJsZVdhaWxzRHJhZ0FuZERyb3ApIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGlmICghZmxhZ3MudXNlRHJvcFRhcmdldCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgY29uc3QgZWxlbWVudCA9IGUudGFyZ2V0O1xuXG4gICAgLy8gVHJpZ2dlciBkZWJvdW5jZSBmdW5jdGlvbiB0byBkZWFjdGl2YXRlIGRyb3AgdGFyZ2V0c1xuICAgIGlmKGZsYWdzLm5leHREZWFjdGl2YXRlKSBmbGFncy5uZXh0RGVhY3RpdmF0ZSgpO1xuXG4gICAgLy8gaWYgdGhlIGVsZW1lbnQgaXMgbnVsbCBvciBlbGVtZW50IGlzIG5vdCBjaGlsZCBvZiBkcm9wIHRhcmdldCBlbGVtZW50XG4gICAgaWYgKCFlbGVtZW50IHx8ICFjaGVja1N0eWxlRHJvcFRhcmdldChnZXRDb21wdXRlZFN0eWxlKGVsZW1lbnQpKSkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgbGV0IGN1cnJlbnRFbGVtZW50ID0gZWxlbWVudDtcbiAgICB3aGlsZSAoY3VycmVudEVsZW1lbnQpIHtcbiAgICAgICAgLy8gY2hlY2sgaWYgY3VycmVudEVsZW1lbnQgaXMgZHJvcCB0YXJnZXQgZWxlbWVudFxuICAgICAgICBpZiAoY2hlY2tTdHlsZURyb3BUYXJnZXQoZ2V0Q29tcHV0ZWRTdHlsZShjdXJyZW50RWxlbWVudCkpKSB7XG4gICAgICAgICAgICBjdXJyZW50RWxlbWVudC5jbGFzc0xpc3QuYWRkKERST1BfVEFSR0VUX0FDVElWRSk7XG4gICAgICAgIH1cbiAgICAgICAgY3VycmVudEVsZW1lbnQgPSBjdXJyZW50RWxlbWVudC5wYXJlbnRFbGVtZW50O1xuICAgIH1cbn1cblxuLyoqXG4gKiBvbkRyYWdMZWF2ZSBpcyBjYWxsZWQgd2hlbiB0aGUgZHJhZ2xlYXZlIGV2ZW50IGlzIGVtaXR0ZWQuXG4gKiBAcGFyYW0ge0RyYWdFdmVudH0gZVxuICogQHJldHVybnNcbiAqL1xuZnVuY3Rpb24gb25EcmFnTGVhdmUoZSkge1xuICAgIC8vIENoZWNrIGlmIHRoaXMgaXMgYW4gZXh0ZXJuYWwgZmlsZSBkcm9wIG9yIGludGVybmFsIEhUTUwgZHJhZ1xuICAgIGNvbnN0IGlzRmlsZURyb3AgPSBlLmRhdGFUcmFuc2Zlci50eXBlcy5pbmNsdWRlcyhcIkZpbGVzXCIpO1xuXG4gICAgLy8gT25seSBoYW5kbGUgZXh0ZXJuYWwgZmlsZSBkcm9wcywgbGV0IGludGVybmFsIEhUTUw1IGRyYWctYW5kLWRyb3Agd29yayBub3JtYWxseVxuICAgIGlmICghaXNGaWxlRHJvcCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgLy8gQUxXQVlTIHByZXZlbnQgZGVmYXVsdCBmb3IgZmlsZSBkcm9wcyB0byBzdG9wIGJyb3dzZXIgbmF2aWdhdGlvblxuICAgIGUucHJldmVudERlZmF1bHQoKTtcblxuICAgIGlmICghd2luZG93LndhaWxzLmZsYWdzLmVuYWJsZVdhaWxzRHJhZ0FuZERyb3ApIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGlmICghZmxhZ3MudXNlRHJvcFRhcmdldCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgLy8gRmluZCB0aGUgY2xvc2UgZHJvcCB0YXJnZXQgZWxlbWVudFxuICAgIGlmICghZS50YXJnZXQgfHwgIWNoZWNrU3R5bGVEcm9wVGFyZ2V0KGdldENvbXB1dGVkU3R5bGUoZS50YXJnZXQpKSkge1xuICAgICAgICByZXR1cm4gbnVsbDtcbiAgICB9XG5cbiAgICAvLyBUcmlnZ2VyIGRlYm91bmNlIGZ1bmN0aW9uIHRvIGRlYWN0aXZhdGUgZHJvcCB0YXJnZXRzXG4gICAgaWYoZmxhZ3MubmV4dERlYWN0aXZhdGUpIGZsYWdzLm5leHREZWFjdGl2YXRlKCk7XG4gICAgXG4gICAgLy8gVXNlIGRlYm91bmNlIHRlY2huaXF1ZSB0byB0YWNsZSBkcmFnbGVhdmUgZXZlbnRzIG9uIG92ZXJsYXBwaW5nIGVsZW1lbnRzIGFuZCBkcm9wIHRhcmdldCBlbGVtZW50c1xuICAgIGZsYWdzLm5leHREZWFjdGl2YXRlID0gKCkgPT4ge1xuICAgICAgICAvLyBEZWFjdGl2YXRlIGFsbCBkcm9wIHRhcmdldHMsIG5ldyBkcm9wIHRhcmdldCB3aWxsIGJlIGFjdGl2YXRlZCBvbiBuZXh0IGRyYWdvdmVyIGV2ZW50XG4gICAgICAgIEFycmF5LmZyb20oZG9jdW1lbnQuZ2V0RWxlbWVudHNCeUNsYXNzTmFtZShEUk9QX1RBUkdFVF9BQ1RJVkUpKS5mb3JFYWNoKGVsID0+IGVsLmNsYXNzTGlzdC5yZW1vdmUoRFJPUF9UQVJHRVRfQUNUSVZFKSk7XG4gICAgICAgIC8vIFJlc2V0IG5leHREZWFjdGl2YXRlXG4gICAgICAgIGZsYWdzLm5leHREZWFjdGl2YXRlID0gbnVsbDtcbiAgICAgICAgLy8gQ2xlYXIgdGltZW91dFxuICAgICAgICBpZiAoZmxhZ3MubmV4dERlYWN0aXZhdGVUaW1lb3V0KSB7XG4gICAgICAgICAgICBjbGVhclRpbWVvdXQoZmxhZ3MubmV4dERlYWN0aXZhdGVUaW1lb3V0KTtcbiAgICAgICAgICAgIGZsYWdzLm5leHREZWFjdGl2YXRlVGltZW91dCA9IG51bGw7XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICAvLyBTZXQgdGltZW91dCB0byBkZWFjdGl2YXRlIGRyb3AgdGFyZ2V0cyBpZiBub3QgdHJpZ2dlcmVkIGJ5IG5leHQgZHJhZyBldmVudFxuICAgIGZsYWdzLm5leHREZWFjdGl2YXRlVGltZW91dCA9IHNldFRpbWVvdXQoKCkgPT4ge1xuICAgICAgICBpZihmbGFncy5uZXh0RGVhY3RpdmF0ZSkgZmxhZ3MubmV4dERlYWN0aXZhdGUoKTtcbiAgICB9LCA1MCk7XG59XG5cbi8qKlxuICogb25Ecm9wIGlzIGNhbGxlZCB3aGVuIHRoZSBkcm9wIGV2ZW50IGlzIGVtaXR0ZWQuXG4gKiBAcGFyYW0ge0RyYWdFdmVudH0gZVxuICogQHJldHVybnNcbiAqL1xuZnVuY3Rpb24gb25Ecm9wKGUpIHtcbiAgICAvLyBDaGVjayBpZiB0aGlzIGlzIGFuIGV4dGVybmFsIGZpbGUgZHJvcCBvciBpbnRlcm5hbCBIVE1MIGRyYWdcbiAgICBjb25zdCBpc0ZpbGVEcm9wID0gZS5kYXRhVHJhbnNmZXIudHlwZXMuaW5jbHVkZXMoXCJGaWxlc1wiKTtcblxuICAgIC8vIE9ubHkgaGFuZGxlIGV4dGVybmFsIGZpbGUgZHJvcHMsIGxldCBpbnRlcm5hbCBIVE1MNSBkcmFnLWFuZC1kcm9wIHdvcmsgbm9ybWFsbHlcbiAgICBpZiAoIWlzRmlsZURyb3ApIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIC8vIEFMV0FZUyBwcmV2ZW50IGRlZmF1bHQgZm9yIGZpbGUgZHJvcHMgdG8gc3RvcCBicm93c2VyIG5hdmlnYXRpb25cbiAgICBlLnByZXZlbnREZWZhdWx0KCk7XG5cbiAgICBpZiAoIXdpbmRvdy53YWlscy5mbGFncy5lbmFibGVXYWlsc0RyYWdBbmREcm9wKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICBpZiAoQ2FuUmVzb2x2ZUZpbGVQYXRocygpKSB7XG4gICAgICAgIC8vIHByb2Nlc3MgZmlsZXNcbiAgICAgICAgbGV0IGZpbGVzID0gW107XG4gICAgICAgIGlmIChlLmRhdGFUcmFuc2Zlci5pdGVtcykge1xuICAgICAgICAgICAgZmlsZXMgPSBbLi4uZS5kYXRhVHJhbnNmZXIuaXRlbXNdLm1hcCgoaXRlbSwgaSkgPT4ge1xuICAgICAgICAgICAgICAgIGlmIChpdGVtLmtpbmQgPT09ICdmaWxlJykge1xuICAgICAgICAgICAgICAgICAgICByZXR1cm4gaXRlbS5nZXRBc0ZpbGUoKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9KTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGZpbGVzID0gWy4uLmUuZGF0YVRyYW5zZmVyLmZpbGVzXTtcbiAgICAgICAgfVxuICAgICAgICB3aW5kb3cucnVudGltZS5SZXNvbHZlRmlsZVBhdGhzKGUueCwgZS55LCBmaWxlcyk7XG4gICAgfVxuXG4gICAgaWYgKCFmbGFncy51c2VEcm9wVGFyZ2V0KSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICAvLyBUcmlnZ2VyIGRlYm91bmNlIGZ1bmN0aW9uIHRvIGRlYWN0aXZhdGUgZHJvcCB0YXJnZXRzXG4gICAgaWYoZmxhZ3MubmV4dERlYWN0aXZhdGUpIGZsYWdzLm5leHREZWFjdGl2YXRlKCk7XG5cbiAgICAvLyBEZWFjdGl2YXRlIGFsbCBkcm9wIHRhcmdldHNcbiAgICBBcnJheS5mcm9tKGRvY3VtZW50LmdldEVsZW1lbnRzQnlDbGFzc05hbWUoRFJPUF9UQVJHRVRfQUNUSVZFKSkuZm9yRWFjaChlbCA9PiBlbC5jbGFzc0xpc3QucmVtb3ZlKERST1BfVEFSR0VUX0FDVElWRSkpO1xufVxuXG4vKipcbiAqIHBvc3RNZXNzYWdlV2l0aEFkZGl0aW9uYWxPYmplY3RzIGNoZWNrcyB0aGUgYnJvd3NlcidzIGNhcGFiaWxpdHkgb2Ygc2VuZGluZyBwb3N0TWVzc2FnZVdpdGhBZGRpdGlvbmFsT2JqZWN0c1xuICpcbiAqIEByZXR1cm5zIHtib29sZWFufVxuICogQGNvbnN0cnVjdG9yXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBDYW5SZXNvbHZlRmlsZVBhdGhzKCkge1xuICAgIHJldHVybiB3aW5kb3cuY2hyb21lPy53ZWJ2aWV3Py5wb3N0TWVzc2FnZVdpdGhBZGRpdGlvbmFsT2JqZWN0cyAhPSBudWxsO1xufVxuXG4vKipcbiAqIFJlc29sdmVGaWxlUGF0aHMgc2VuZHMgZHJvcCBldmVudHMgdG8gdGhlIEdPIHNpZGUgdG8gcmVzb2x2ZSBmaWxlIHBhdGhzIG9uIHdpbmRvd3MuXG4gKlxuICogQHBhcmFtIHtudW1iZXJ9IHhcbiAqIEBwYXJhbSB7bnVtYmVyfSB5XG4gKiBAcGFyYW0ge2FueVtdfSBmaWxlc1xuICogQGNvbnN0cnVjdG9yXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBSZXNvbHZlRmlsZVBhdGhzKHgsIHksIGZpbGVzKSB7XG4gICAgLy8gT25seSBmb3Igd2luZG93cyB3ZWJ2aWV3MiA+PSAxLjAuMTc3NC4zMFxuICAgIC8vIGh0dHBzOi8vbGVhcm4ubWljcm9zb2Z0LmNvbS9lbi11cy9taWNyb3NvZnQtZWRnZS93ZWJ2aWV3Mi9yZWZlcmVuY2Uvd2luMzIvaWNvcmV3ZWJ2aWV3MndlYm1lc3NhZ2VyZWNlaXZlZGV2ZW50YXJnczI/dmlldz13ZWJ2aWV3Mi0xLjAuMTgyMy4zMiNhcHBsaWVzLXRvXG4gICAgaWYgKHdpbmRvdy5jaHJvbWU/LndlYnZpZXc/LnBvc3RNZXNzYWdlV2l0aEFkZGl0aW9uYWxPYmplY3RzKSB7XG4gICAgICAgIGNocm9tZS53ZWJ2aWV3LnBvc3RNZXNzYWdlV2l0aEFkZGl0aW9uYWxPYmplY3RzKGBmaWxlOmRyb3A6JHt4fToke3l9YCwgZmlsZXMpO1xuICAgIH1cbn1cblxuLyoqXG4gKiBDYWxsYmFjayBmb3IgT25GaWxlRHJvcCByZXR1cm5zIGEgc2xpY2Ugb2YgZmlsZSBwYXRoIHN0cmluZ3Mgd2hlbiBhIGRyb3AgaXMgZmluaXNoZWQuXG4gKlxuICogQGV4cG9ydFxuICogQGNhbGxiYWNrIE9uRmlsZURyb3BDYWxsYmFja1xuICogQHBhcmFtIHtudW1iZXJ9IHggLSB4IGNvb3JkaW5hdGUgb2YgdGhlIGRyb3BcbiAqIEBwYXJhbSB7bnVtYmVyfSB5IC0geSBjb29yZGluYXRlIG9mIHRoZSBkcm9wXG4gKiBAcGFyYW0ge3N0cmluZ1tdfSBwYXRocyAtIEEgbGlzdCBvZiBmaWxlIHBhdGhzLlxuICovXG5cbi8qKlxuICogT25GaWxlRHJvcCBsaXN0ZW5zIHRvIGRyYWcgYW5kIGRyb3AgZXZlbnRzIGFuZCBjYWxscyB0aGUgY2FsbGJhY2sgd2l0aCB0aGUgY29vcmRpbmF0ZXMgb2YgdGhlIGRyb3AgYW5kIGFuIGFycmF5IG9mIHBhdGggc3RyaW5ncy5cbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge09uRmlsZURyb3BDYWxsYmFja30gY2FsbGJhY2sgLSBDYWxsYmFjayBmb3IgT25GaWxlRHJvcCByZXR1cm5zIGEgc2xpY2Ugb2YgZmlsZSBwYXRoIHN0cmluZ3Mgd2hlbiBhIGRyb3AgaXMgZmluaXNoZWQuXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFt1c2VEcm9wVGFyZ2V0PXRydWVdIC0gT25seSBjYWxsIHRoZSBjYWxsYmFjayB3aGVuIHRoZSBkcm9wIGZpbmlzaGVkIG9uIGFuIGVsZW1lbnQgdGhhdCBoYXMgdGhlIGRyb3AgdGFyZ2V0IHN0eWxlLiAoLS13YWlscy1kcm9wLXRhcmdldClcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIE9uRmlsZURyb3AoY2FsbGJhY2ssIHVzZURyb3BUYXJnZXQpIHtcbiAgICBpZiAodHlwZW9mIGNhbGxiYWNrICE9PSBcImZ1bmN0aW9uXCIpIHtcbiAgICAgICAgY29uc29sZS5lcnJvcihcIkRyYWdBbmREcm9wQ2FsbGJhY2sgaXMgbm90IGEgZnVuY3Rpb25cIik7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICBpZiAoZmxhZ3MucmVnaXN0ZXJlZCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGZsYWdzLnJlZ2lzdGVyZWQgPSB0cnVlO1xuXG4gICAgY29uc3QgdURUUFQgPSB0eXBlb2YgdXNlRHJvcFRhcmdldDtcbiAgICBmbGFncy51c2VEcm9wVGFyZ2V0ID0gdURUUFQgPT09IFwidW5kZWZpbmVkXCIgfHwgdURUUFQgIT09IFwiYm9vbGVhblwiID8gZmxhZ3MuZGVmYXVsdFVzZURyb3BUYXJnZXQgOiB1c2VEcm9wVGFyZ2V0O1xuICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdkcmFnb3ZlcicsIG9uRHJhZ092ZXIpO1xuICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdkcmFnbGVhdmUnLCBvbkRyYWdMZWF2ZSk7XG4gICAgd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ2Ryb3AnLCBvbkRyb3ApO1xuXG4gICAgbGV0IGNiID0gY2FsbGJhY2s7XG4gICAgaWYgKGZsYWdzLnVzZURyb3BUYXJnZXQpIHtcbiAgICAgICAgY2IgPSBmdW5jdGlvbiAoeCwgeSwgcGF0aHMpIHtcbiAgICAgICAgICAgIGNvbnN0IGVsZW1lbnQgPSBkb2N1bWVudC5lbGVtZW50RnJvbVBvaW50KHgsIHkpXG4gICAgICAgICAgICAvLyBpZiB0aGUgZWxlbWVudCBpcyBudWxsIG9yIGVsZW1lbnQgaXMgbm90IGNoaWxkIG9mIGRyb3AgdGFyZ2V0IGVsZW1lbnQsIHJldHVybiBudWxsXG4gICAgICAgICAgICBpZiAoIWVsZW1lbnQgfHwgIWNoZWNrU3R5bGVEcm9wVGFyZ2V0KGdldENvbXB1dGVkU3R5bGUoZWxlbWVudCkpKSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuIG51bGw7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBjYWxsYmFjayh4LCB5LCBwYXRocyk7XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICBFdmVudHNPbihcIndhaWxzOmZpbGUtZHJvcFwiLCBjYik7XG59XG5cbi8qKlxuICogT25GaWxlRHJvcE9mZiByZW1vdmVzIHRoZSBkcmFnIGFuZCBkcm9wIGxpc3RlbmVycyBhbmQgaGFuZGxlcnMuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBPbkZpbGVEcm9wT2ZmKCkge1xuICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKCdkcmFnb3ZlcicsIG9uRHJhZ092ZXIpO1xuICAgIHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKCdkcmFnbGVhdmUnLCBvbkRyYWdMZWF2ZSk7XG4gICAgd2luZG93LnJlbW92ZUV2ZW50TGlzdGVuZXIoJ2Ryb3AnLCBvbkRyb3ApO1xuICAgIEV2ZW50c09mZihcIndhaWxzOmZpbGUtZHJvcFwiKTtcbiAgICBmbGFncy5yZWdpc3RlcmVkID0gZmFsc2U7XG59XG4iLCAiLypcbi0tZGVmYXVsdC1jb250ZXh0bWVudTogYXV0bzsgKGRlZmF1bHQpIHdpbGwgc2hvdyB0aGUgZGVmYXVsdCBjb250ZXh0IG1lbnUgaWYgY29udGVudEVkaXRhYmxlIGlzIHRydWUgT1IgdGV4dCBoYXMgYmVlbiBzZWxlY3RlZCBPUiBlbGVtZW50IGlzIGlucHV0IG9yIHRleHRhcmVhXG4tLWRlZmF1bHQtY29udGV4dG1lbnU6IHNob3c7IHdpbGwgYWx3YXlzIHNob3cgdGhlIGRlZmF1bHQgY29udGV4dCBtZW51XG4tLWRlZmF1bHQtY29udGV4dG1lbnU6IGhpZGU7IHdpbGwgYWx3YXlzIGhpZGUgdGhlIGRlZmF1bHQgY29udGV4dCBtZW51XG5cblRoaXMgcnVsZSBpcyBpbmhlcml0ZWQgbGlrZSBub3JtYWwgQ1NTIHJ1bGVzLCBzbyBuZXN0aW5nIHdvcmtzIGFzIGV4cGVjdGVkXG4qL1xuZXhwb3J0IGZ1bmN0aW9uIHByb2Nlc3NEZWZhdWx0Q29udGV4dE1lbnUoZXZlbnQpIHtcbiAgICAvLyBQcm9jZXNzIGRlZmF1bHQgY29udGV4dCBtZW51XG4gICAgY29uc3QgZWxlbWVudCA9IGV2ZW50LnRhcmdldDtcbiAgICBjb25zdCBjb21wdXRlZFN0eWxlID0gd2luZG93LmdldENvbXB1dGVkU3R5bGUoZWxlbWVudCk7XG4gICAgY29uc3QgZGVmYXVsdENvbnRleHRNZW51QWN0aW9uID0gY29tcHV0ZWRTdHlsZS5nZXRQcm9wZXJ0eVZhbHVlKFwiLS1kZWZhdWx0LWNvbnRleHRtZW51XCIpLnRyaW0oKTtcbiAgICBzd2l0Y2ggKGRlZmF1bHRDb250ZXh0TWVudUFjdGlvbikge1xuICAgICAgICBjYXNlIFwic2hvd1wiOlxuICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICBjYXNlIFwiaGlkZVwiOlxuICAgICAgICAgICAgZXZlbnQucHJldmVudERlZmF1bHQoKTtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgZGVmYXVsdDpcbiAgICAgICAgICAgIC8vIENoZWNrIGlmIGNvbnRlbnRFZGl0YWJsZSBpcyB0cnVlXG4gICAgICAgICAgICBpZiAoZWxlbWVudC5pc0NvbnRlbnRFZGl0YWJsZSkge1xuICAgICAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLy8gQ2hlY2sgaWYgdGV4dCBoYXMgYmVlbiBzZWxlY3RlZCBhbmQgYWN0aW9uIGlzIG9uIHRoZSBzZWxlY3RlZCBlbGVtZW50c1xuICAgICAgICAgICAgY29uc3Qgc2VsZWN0aW9uID0gd2luZG93LmdldFNlbGVjdGlvbigpO1xuICAgICAgICAgICAgY29uc3QgaGFzU2VsZWN0aW9uID0gKHNlbGVjdGlvbi50b1N0cmluZygpLmxlbmd0aCA+IDApXG4gICAgICAgICAgICBpZiAoaGFzU2VsZWN0aW9uKSB7XG4gICAgICAgICAgICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBzZWxlY3Rpb24ucmFuZ2VDb3VudDsgaSsrKSB7XG4gICAgICAgICAgICAgICAgICAgIGNvbnN0IHJhbmdlID0gc2VsZWN0aW9uLmdldFJhbmdlQXQoaSk7XG4gICAgICAgICAgICAgICAgICAgIGNvbnN0IHJlY3RzID0gcmFuZ2UuZ2V0Q2xpZW50UmVjdHMoKTtcbiAgICAgICAgICAgICAgICAgICAgZm9yIChsZXQgaiA9IDA7IGogPCByZWN0cy5sZW5ndGg7IGorKykge1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc3QgcmVjdCA9IHJlY3RzW2pdO1xuICAgICAgICAgICAgICAgICAgICAgICAgaWYgKGRvY3VtZW50LmVsZW1lbnRGcm9tUG9pbnQocmVjdC5sZWZ0LCByZWN0LnRvcCkgPT09IGVsZW1lbnQpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICAvLyBDaGVjayBpZiB0YWduYW1lIGlzIGlucHV0IG9yIHRleHRhcmVhXG4gICAgICAgICAgICBpZiAoZWxlbWVudC50YWdOYW1lID09PSBcIklOUFVUXCIgfHwgZWxlbWVudC50YWdOYW1lID09PSBcIlRFWFRBUkVBXCIpIHtcbiAgICAgICAgICAgICAgICBpZiAoaGFzU2VsZWN0aW9uIHx8ICghZWxlbWVudC5yZWFkT25seSAmJiAhZWxlbWVudC5kaXNhYmxlZCkpIHtcbiAgICAgICAgICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLy8gaGlkZSBkZWZhdWx0IGNvbnRleHQgbWVudVxuICAgICAgICAgICAgZXZlbnQucHJldmVudERlZmF1bHQoKTtcbiAgICB9XG59XG4iLCAiLypcbiBfICAgICAgIF9fICAgICAgXyBfX1xufCB8ICAgICAvIC9fX18gXyhfKSAvX19fX1xufCB8IC98IC8gLyBfXyBgLyAvIC8gX19fL1xufCB8LyB8LyAvIC9fLyAvIC8gKF9fICApXG58X18vfF9fL1xcX18sXy9fL18vX19fXy9cblRoZSBlbGVjdHJvbiBhbHRlcm5hdGl2ZSBmb3IgR29cbihjKSBMZWEgQW50aG9ueSAyMDE5LXByZXNlbnRcbiovXG4vKiBqc2hpbnQgZXN2ZXJzaW9uOiA5ICovXG5cbmltcG9ydCB7Q2FsbH0gZnJvbSBcIi4vY2FsbHNcIjtcblxuLyoqXG4gKiBJbml0aWFsaXplIHRoZSBub3RpZmljYXRpb24gc2VydmljZSBmb3IgdGhlIGFwcGxpY2F0aW9uLlxuICogVGhpcyBtdXN0IGJlIGNhbGxlZCBiZWZvcmUgc2VuZGluZyBhbnkgbm90aWZpY2F0aW9ucy5cbiAqIE9uIG1hY09TLCB0aGlzIGFsc28gZW5zdXJlcyB0aGUgbm90aWZpY2F0aW9uIGRlbGVnYXRlIGlzIHByb3Blcmx5IGluaXRpYWxpemVkLlxuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8dm9pZD59XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBJbml0aWFsaXplTm90aWZpY2F0aW9ucygpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpJbml0aWFsaXplTm90aWZpY2F0aW9uc1wiKTtcbn1cblxuLyoqXG4gKiBDbGVhbiB1cCBub3RpZmljYXRpb24gcmVzb3VyY2VzIGFuZCByZWxlYXNlIGFueSBoZWxkIGNvbm5lY3Rpb25zLlxuICogVGhpcyBzaG91bGQgYmUgY2FsbGVkIHdoZW4gc2h1dHRpbmcgZG93biB0aGUgYXBwbGljYXRpb24gdG8gcHJvcGVybHkgcmVsZWFzZSByZXNvdXJjZXNcbiAqIChwcmltYXJpbHkgbmVlZGVkIG9uIExpbnV4IHRvIGNsb3NlIEQtQnVzIGNvbm5lY3Rpb25zKS5cbiAqXG4gKiBAZXhwb3J0XG4gKiBAcmV0dXJuIHtQcm9taXNlPHZvaWQ+fVxuICovXG5leHBvcnQgZnVuY3Rpb24gQ2xlYW51cE5vdGlmaWNhdGlvbnMoKSB7XG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6Q2xlYW51cE5vdGlmaWNhdGlvbnNcIik7XG59XG5cbi8qKlxuICogQ2hlY2sgaWYgbm90aWZpY2F0aW9ucyBhcmUgYXZhaWxhYmxlIG9uIHRoZSBjdXJyZW50IHBsYXRmb3JtLlxuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8Ym9vbGVhbj59IFRydWUgaWYgbm90aWZpY2F0aW9ucyBhcmUgYXZhaWxhYmxlLCBmYWxzZSBvdGhlcndpc2VcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIElzTm90aWZpY2F0aW9uQXZhaWxhYmxlKCkge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOklzTm90aWZpY2F0aW9uQXZhaWxhYmxlXCIpO1xufVxuXG4vKipcbiAqIFJlcXVlc3Qgbm90aWZpY2F0aW9uIGF1dGhvcml6YXRpb24gZnJvbSB0aGUgdXNlci5cbiAqIE9uIG1hY09TLCB0aGlzIHByb21wdHMgdGhlIHVzZXIgdG8gYWxsb3cgbm90aWZpY2F0aW9ucy5cbiAqIE9uIG90aGVyIHBsYXRmb3JtcywgdGhpcyBhbHdheXMgcmV0dXJucyB0cnVlLlxuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8Ym9vbGVhbj59IFRydWUgaWYgYXV0aG9yaXphdGlvbiB3YXMgZ3JhbnRlZCwgZmFsc2Ugb3RoZXJ3aXNlXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBSZXF1ZXN0Tm90aWZpY2F0aW9uQXV0aG9yaXphdGlvbigpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpSZXF1ZXN0Tm90aWZpY2F0aW9uQXV0aG9yaXphdGlvblwiKTtcbn1cblxuLyoqXG4gKiBDaGVjayB0aGUgY3VycmVudCBub3RpZmljYXRpb24gYXV0aG9yaXphdGlvbiBzdGF0dXMuXG4gKiBPbiBtYWNPUywgdGhpcyBjaGVja3MgaWYgdGhlIGFwcCBoYXMgbm90aWZpY2F0aW9uIHBlcm1pc3Npb25zLlxuICogT24gb3RoZXIgcGxhdGZvcm1zLCB0aGlzIGFsd2F5cyByZXR1cm5zIHRydWUuXG4gKlxuICogQGV4cG9ydFxuICogQHJldHVybiB7UHJvbWlzZTxib29sZWFuPn0gVHJ1ZSBpZiBhdXRob3JpemVkLCBmYWxzZSBvdGhlcndpc2VcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIENoZWNrTm90aWZpY2F0aW9uQXV0aG9yaXphdGlvbigpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpDaGVja05vdGlmaWNhdGlvbkF1dGhvcml6YXRpb25cIik7XG59XG5cbi8qKlxuICogU2VuZCBhIGJhc2ljIG5vdGlmaWNhdGlvbiB3aXRoIHRoZSBnaXZlbiBvcHRpb25zLlxuICogVGhlIG5vdGlmaWNhdGlvbiB3aWxsIGRpc3BsYXkgd2l0aCB0aGUgcHJvdmlkZWQgdGl0bGUsIHN1YnRpdGxlIChpZiBzdXBwb3J0ZWQpLCBhbmQgYm9keSB0ZXh0LlxuICpcbiAqIEBleHBvcnRcbiAqIEBwYXJhbSB7T2JqZWN0fSBvcHRpb25zIC0gTm90aWZpY2F0aW9uIG9wdGlvbnNcbiAqIEBwYXJhbSB7c3RyaW5nfSBvcHRpb25zLmlkIC0gVW5pcXVlIGlkZW50aWZpZXIgZm9yIHRoZSBub3RpZmljYXRpb25cbiAqIEBwYXJhbSB7c3RyaW5nfSBvcHRpb25zLnRpdGxlIC0gTm90aWZpY2F0aW9uIHRpdGxlXG4gKiBAcGFyYW0ge3N0cmluZ30gW29wdGlvbnMuc3VidGl0bGVdIC0gTm90aWZpY2F0aW9uIHN1YnRpdGxlIChtYWNPUyBhbmQgTGludXggb25seSlcbiAqIEBwYXJhbSB7c3RyaW5nfSBbb3B0aW9ucy5ib2R5XSAtIE5vdGlmaWNhdGlvbiBib2R5IHRleHRcbiAqIEBwYXJhbSB7c3RyaW5nfSBbb3B0aW9ucy5jYXRlZ29yeUlkXSAtIENhdGVnb3J5IElEIGZvciBhY3Rpb24gYnV0dG9ucyAocmVxdWlyZXMgU2VuZE5vdGlmaWNhdGlvbldpdGhBY3Rpb25zKVxuICogQHBhcmFtIHtPYmplY3Q8c3RyaW5nLCBhbnk+fSBbb3B0aW9ucy5kYXRhXSAtIEFkZGl0aW9uYWwgdXNlciBkYXRhIHRvIGF0dGFjaCB0byB0aGUgbm90aWZpY2F0aW9uXG4gKiBAcmV0dXJuIHtQcm9taXNlPHZvaWQ+fVxuICovXG5leHBvcnQgZnVuY3Rpb24gU2VuZE5vdGlmaWNhdGlvbihvcHRpb25zKSB7XG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6U2VuZE5vdGlmaWNhdGlvblwiLCBbb3B0aW9uc10pO1xufVxuXG4vKipcbiAqIFNlbmQgYSBub3RpZmljYXRpb24gd2l0aCBhY3Rpb24gYnV0dG9ucy5cbiAqIEEgTm90aWZpY2F0aW9uQ2F0ZWdvcnkgbXVzdCBiZSByZWdpc3RlcmVkIGZpcnN0IHVzaW5nIFJlZ2lzdGVyTm90aWZpY2F0aW9uQ2F0ZWdvcnkuXG4gKiBUaGUgb3B0aW9ucy5jYXRlZ29yeUlkIG11c3QgbWF0Y2ggYSBwcmV2aW91c2x5IHJlZ2lzdGVyZWQgY2F0ZWdvcnkgSUQuXG4gKiBJZiB0aGUgY2F0ZWdvcnkgaXMgbm90IGZvdW5kLCBhIGJhc2ljIG5vdGlmaWNhdGlvbiB3aWxsIGJlIHNlbnQgaW5zdGVhZC5cbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge09iamVjdH0gb3B0aW9ucyAtIE5vdGlmaWNhdGlvbiBvcHRpb25zXG4gKiBAcGFyYW0ge3N0cmluZ30gb3B0aW9ucy5pZCAtIFVuaXF1ZSBpZGVudGlmaWVyIGZvciB0aGUgbm90aWZpY2F0aW9uXG4gKiBAcGFyYW0ge3N0cmluZ30gb3B0aW9ucy50aXRsZSAtIE5vdGlmaWNhdGlvbiB0aXRsZVxuICogQHBhcmFtIHtzdHJpbmd9IFtvcHRpb25zLnN1YnRpdGxlXSAtIE5vdGlmaWNhdGlvbiBzdWJ0aXRsZSAobWFjT1MgYW5kIExpbnV4IG9ubHkpXG4gKiBAcGFyYW0ge3N0cmluZ30gW29wdGlvbnMuYm9keV0gLSBOb3RpZmljYXRpb24gYm9keSB0ZXh0XG4gKiBAcGFyYW0ge3N0cmluZ30gb3B0aW9ucy5jYXRlZ29yeUlkIC0gQ2F0ZWdvcnkgSUQgdGhhdCBtYXRjaGVzIGEgcmVnaXN0ZXJlZCBjYXRlZ29yeVxuICogQHBhcmFtIHtPYmplY3Q8c3RyaW5nLCBhbnk+fSBbb3B0aW9ucy5kYXRhXSAtIEFkZGl0aW9uYWwgdXNlciBkYXRhIHRvIGF0dGFjaCB0byB0aGUgbm90aWZpY2F0aW9uXG4gKiBAcmV0dXJuIHtQcm9taXNlPHZvaWQ+fVxuICovXG5leHBvcnQgZnVuY3Rpb24gU2VuZE5vdGlmaWNhdGlvbldpdGhBY3Rpb25zKG9wdGlvbnMpIHtcbiAgICByZXR1cm4gQ2FsbChcIjp3YWlsczpTZW5kTm90aWZpY2F0aW9uV2l0aEFjdGlvbnNcIiwgW29wdGlvbnNdKTtcbn1cblxuLyoqXG4gKiBSZWdpc3RlciBhIG5vdGlmaWNhdGlvbiBjYXRlZ29yeSB0aGF0IGNhbiBiZSB1c2VkIHdpdGggU2VuZE5vdGlmaWNhdGlvbldpdGhBY3Rpb25zLlxuICogQ2F0ZWdvcmllcyBkZWZpbmUgdGhlIGFjdGlvbiBidXR0b25zIGFuZCBvcHRpb25hbCByZXBseSBmaWVsZHMgdGhhdCB3aWxsIGFwcGVhciBvbiBub3RpZmljYXRpb25zLlxuICogUmVnaXN0ZXJpbmcgYSBjYXRlZ29yeSB3aXRoIHRoZSBzYW1lIElEIGFzIGEgcHJldmlvdXNseSByZWdpc3RlcmVkIGNhdGVnb3J5IHdpbGwgb3ZlcnJpZGUgaXQuXG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtPYmplY3R9IGNhdGVnb3J5IC0gTm90aWZpY2F0aW9uIGNhdGVnb3J5IGRlZmluaXRpb25cbiAqIEBwYXJhbSB7c3RyaW5nfSBjYXRlZ29yeS5pZCAtIFVuaXF1ZSBpZGVudGlmaWVyIGZvciB0aGUgY2F0ZWdvcnlcbiAqIEBwYXJhbSB7QXJyYXk8T2JqZWN0Pn0gW2NhdGVnb3J5LmFjdGlvbnNdIC0gQXJyYXkgb2YgYWN0aW9uIGJ1dHRvbnNcbiAqIEBwYXJhbSB7c3RyaW5nfSBjYXRlZ29yeS5hY3Rpb25zW10uaWQgLSBVbmlxdWUgaWRlbnRpZmllciBmb3IgdGhlIGFjdGlvblxuICogQHBhcmFtIHtzdHJpbmd9IGNhdGVnb3J5LmFjdGlvbnNbXS50aXRsZSAtIERpc3BsYXkgdGl0bGUgZm9yIHRoZSBhY3Rpb24gYnV0dG9uXG4gKiBAcGFyYW0ge2Jvb2xlYW59IFtjYXRlZ29yeS5hY3Rpb25zW10uZGVzdHJ1Y3RpdmVdIC0gV2hldGhlciB0aGUgYWN0aW9uIGlzIGRlc3RydWN0aXZlIChtYWNPUy1zcGVjaWZpYylcbiAqIEBwYXJhbSB7Ym9vbGVhbn0gW2NhdGVnb3J5Lmhhc1JlcGx5RmllbGRdIC0gV2hldGhlciB0byBpbmNsdWRlIGEgdGV4dCBpbnB1dCBmaWVsZCBmb3IgcmVwbGllc1xuICogQHBhcmFtIHtzdHJpbmd9IFtjYXRlZ29yeS5yZXBseVBsYWNlaG9sZGVyXSAtIFBsYWNlaG9sZGVyIHRleHQgZm9yIHRoZSByZXBseSBmaWVsZCAocmVxdWlyZWQgaWYgaGFzUmVwbHlGaWVsZCBpcyB0cnVlKVxuICogQHBhcmFtIHtzdHJpbmd9IFtjYXRlZ29yeS5yZXBseUJ1dHRvblRpdGxlXSAtIFRpdGxlIGZvciB0aGUgcmVwbHkgYnV0dG9uIChyZXF1aXJlZCBpZiBoYXNSZXBseUZpZWxkIGlzIHRydWUpXG4gKiBAcmV0dXJuIHtQcm9taXNlPHZvaWQ+fVxuICovXG5leHBvcnQgZnVuY3Rpb24gUmVnaXN0ZXJOb3RpZmljYXRpb25DYXRlZ29yeShjYXRlZ29yeSkge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOlJlZ2lzdGVyTm90aWZpY2F0aW9uQ2F0ZWdvcnlcIiwgW2NhdGVnb3J5XSk7XG59XG5cbi8qKlxuICogUmVtb3ZlIGEgcHJldmlvdXNseSByZWdpc3RlcmVkIG5vdGlmaWNhdGlvbiBjYXRlZ29yeS5cbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gY2F0ZWdvcnlJZCAtIFRoZSBJRCBvZiB0aGUgY2F0ZWdvcnkgdG8gcmVtb3ZlXG4gKiBAcmV0dXJuIHtQcm9taXNlPHZvaWQ+fVxuICovXG5leHBvcnQgZnVuY3Rpb24gUmVtb3ZlTm90aWZpY2F0aW9uQ2F0ZWdvcnkoY2F0ZWdvcnlJZCkge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOlJlbW92ZU5vdGlmaWNhdGlvbkNhdGVnb3J5XCIsIFtjYXRlZ29yeUlkXSk7XG59XG5cbi8qKlxuICogUmVtb3ZlIGFsbCBwZW5kaW5nIG5vdGlmaWNhdGlvbnMgZnJvbSB0aGUgbm90aWZpY2F0aW9uIGNlbnRlci5cbiAqIE9uIFdpbmRvd3MsIHRoaXMgaXMgYSBuby1vcCBhcyB0aGUgcGxhdGZvcm0gbWFuYWdlcyBub3RpZmljYXRpb24gbGlmZWN5Y2xlIGF1dG9tYXRpY2FsbHkuXG4gKlxuICogQGV4cG9ydFxuICogQHJldHVybiB7UHJvbWlzZTx2b2lkPn1cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFJlbW92ZUFsbFBlbmRpbmdOb3RpZmljYXRpb25zKCkge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOlJlbW92ZUFsbFBlbmRpbmdOb3RpZmljYXRpb25zXCIpO1xufVxuXG4vKipcbiAqIFJlbW92ZSBhIHNwZWNpZmljIHBlbmRpbmcgbm90aWZpY2F0aW9uIGJ5IGl0cyBpZGVudGlmaWVyLlxuICogT24gV2luZG93cywgdGhpcyBpcyBhIG5vLW9wIGFzIHRoZSBwbGF0Zm9ybSBtYW5hZ2VzIG5vdGlmaWNhdGlvbiBsaWZlY3ljbGUgYXV0b21hdGljYWxseS5cbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gaWRlbnRpZmllciAtIFRoZSBJRCBvZiB0aGUgbm90aWZpY2F0aW9uIHRvIHJlbW92ZVxuICogQHJldHVybiB7UHJvbWlzZTx2b2lkPn1cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFJlbW92ZVBlbmRpbmdOb3RpZmljYXRpb24oaWRlbnRpZmllcikge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOlJlbW92ZVBlbmRpbmdOb3RpZmljYXRpb25cIiwgW2lkZW50aWZpZXJdKTtcbn1cblxuLyoqXG4gKiBSZW1vdmUgYWxsIGRlbGl2ZXJlZCBub3RpZmljYXRpb25zIGZyb20gdGhlIG5vdGlmaWNhdGlvbiBjZW50ZXIuXG4gKiBPbiBXaW5kb3dzLCB0aGlzIGlzIGEgbm8tb3AgYXMgdGhlIHBsYXRmb3JtIG1hbmFnZXMgbm90aWZpY2F0aW9uIGxpZmVjeWNsZSBhdXRvbWF0aWNhbGx5LlxuICpcbiAqIEBleHBvcnRcbiAqIEByZXR1cm4ge1Byb21pc2U8dm9pZD59XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBSZW1vdmVBbGxEZWxpdmVyZWROb3RpZmljYXRpb25zKCkge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOlJlbW92ZUFsbERlbGl2ZXJlZE5vdGlmaWNhdGlvbnNcIik7XG59XG5cbi8qKlxuICogUmVtb3ZlIGEgc3BlY2lmaWMgZGVsaXZlcmVkIG5vdGlmaWNhdGlvbiBieSBpdHMgaWRlbnRpZmllci5cbiAqIE9uIFdpbmRvd3MsIHRoaXMgaXMgYSBuby1vcCBhcyB0aGUgcGxhdGZvcm0gbWFuYWdlcyBub3RpZmljYXRpb24gbGlmZWN5Y2xlIGF1dG9tYXRpY2FsbHkuXG4gKlxuICogQGV4cG9ydFxuICogQHBhcmFtIHtzdHJpbmd9IGlkZW50aWZpZXIgLSBUaGUgSUQgb2YgdGhlIG5vdGlmaWNhdGlvbiB0byByZW1vdmVcbiAqIEByZXR1cm4ge1Byb21pc2U8dm9pZD59XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBSZW1vdmVEZWxpdmVyZWROb3RpZmljYXRpb24oaWRlbnRpZmllcikge1xuICAgIHJldHVybiBDYWxsKFwiOndhaWxzOlJlbW92ZURlbGl2ZXJlZE5vdGlmaWNhdGlvblwiLCBbaWRlbnRpZmllcl0pO1xufVxuXG4vKipcbiAqIFJlbW92ZSBhIG5vdGlmaWNhdGlvbiBieSBpdHMgaWRlbnRpZmllci5cbiAqIFRoaXMgaXMgYSBjb252ZW5pZW5jZSBmdW5jdGlvbiB0aGF0IHdvcmtzIGFjcm9zcyBwbGF0Zm9ybXMuXG4gKiBPbiBtYWNPUywgdXNlIHRoZSBtb3JlIHNwZWNpZmljIFJlbW92ZVBlbmRpbmdOb3RpZmljYXRpb24gb3IgUmVtb3ZlRGVsaXZlcmVkTm90aWZpY2F0aW9uIGZ1bmN0aW9ucy5cbiAqXG4gKiBAZXhwb3J0XG4gKiBAcGFyYW0ge3N0cmluZ30gaWRlbnRpZmllciAtIFRoZSBJRCBvZiB0aGUgbm90aWZpY2F0aW9uIHRvIHJlbW92ZVxuICogQHJldHVybiB7UHJvbWlzZTx2b2lkPn1cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFJlbW92ZU5vdGlmaWNhdGlvbihpZGVudGlmaWVyKSB7XG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6UmVtb3ZlTm90aWZpY2F0aW9uXCIsIFtpZGVudGlmaWVyXSk7XG59XG5cbiIsICIvKlxuIF9cdCAgIF9fXHQgIF8gX19cbnwgfFx0IC8gL19fXyBfKF8pIC9fX19fXG58IHwgL3wgLyAvIF9fIGAvIC8gLyBfX18vXG58IHwvIHwvIC8gL18vIC8gLyAoX18gIClcbnxfXy98X18vXFxfXyxfL18vXy9fX19fL1xuVGhlIGVsZWN0cm9uIGFsdGVybmF0aXZlIGZvciBHb1xuKGMpIExlYSBBbnRob255IDIwMTktcHJlc2VudFxuKi9cbi8qIGpzaGludCBlc3ZlcnNpb246IDkgKi9cbmltcG9ydCAqIGFzIExvZyBmcm9tICcuL2xvZyc7XG5pbXBvcnQge1xuICBldmVudExpc3RlbmVycyxcbiAgRXZlbnRzRW1pdCxcbiAgRXZlbnRzTm90aWZ5LFxuICBFdmVudHNPZmYsXG4gIEV2ZW50c09mZkFsbCxcbiAgRXZlbnRzT24sXG4gIEV2ZW50c09uY2UsXG4gIEV2ZW50c09uTXVsdGlwbGUsXG59IGZyb20gXCIuL2V2ZW50c1wiO1xuaW1wb3J0IHsgQ2FsbCwgQ2FsbGJhY2ssIGNhbGxiYWNrcyB9IGZyb20gJy4vY2FsbHMnO1xuaW1wb3J0IHsgU2V0QmluZGluZ3MgfSBmcm9tIFwiLi9iaW5kaW5nc1wiO1xuaW1wb3J0ICogYXMgV2luZG93IGZyb20gXCIuL3dpbmRvd1wiO1xuaW1wb3J0ICogYXMgU2NyZWVuIGZyb20gXCIuL3NjcmVlblwiO1xuaW1wb3J0ICogYXMgQnJvd3NlciBmcm9tIFwiLi9icm93c2VyXCI7XG5pbXBvcnQgKiBhcyBDbGlwYm9hcmQgZnJvbSBcIi4vY2xpcGJvYXJkXCI7XG5pbXBvcnQgKiBhcyBEcmFnQW5kRHJvcCBmcm9tIFwiLi9kcmFnYW5kZHJvcFwiO1xuaW1wb3J0ICogYXMgQ29udGV4dE1lbnUgZnJvbSBcIi4vY29udGV4dG1lbnVcIjtcbmltcG9ydCAqIGFzIE5vdGlmaWNhdGlvbnMgZnJvbSBcIi4vbm90aWZpY2F0aW9uc1wiO1xuXG5leHBvcnQgZnVuY3Rpb24gUXVpdCgpIHtcbiAgICB3aW5kb3cuV2FpbHNJbnZva2UoJ1EnKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFNob3coKSB7XG4gICAgd2luZG93LldhaWxzSW52b2tlKCdTJyk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBIaWRlKCkge1xuICAgIHdpbmRvdy5XYWlsc0ludm9rZSgnSCcpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gRW52aXJvbm1lbnQoKSB7XG4gICAgcmV0dXJuIENhbGwoXCI6d2FpbHM6RW52aXJvbm1lbnRcIik7XG59XG5cbi8vIFRoZSBKUyBydW50aW1lXG53aW5kb3cucnVudGltZSA9IHtcbiAgICAuLi5Mb2csXG4gICAgLi4uV2luZG93LFxuICAgIC4uLkJyb3dzZXIsXG4gICAgLi4uU2NyZWVuLFxuICAgIC4uLkNsaXBib2FyZCxcbiAgICAuLi5EcmFnQW5kRHJvcCxcbiAgICAuLi5Ob3RpZmljYXRpb25zLFxuICAgIEV2ZW50c09uLFxuICAgIEV2ZW50c09uY2UsXG4gICAgRXZlbnRzT25NdWx0aXBsZSxcbiAgICBFdmVudHNFbWl0LFxuICAgIEV2ZW50c09mZixcbiAgICBFdmVudHNPZmZBbGwsXG4gICAgRW52aXJvbm1lbnQsXG4gICAgU2hvdyxcbiAgICBIaWRlLFxuICAgIFF1aXRcbn07XG5cbi8vIEludGVybmFsIHdhaWxzIGVuZHBvaW50c1xud2luZG93LndhaWxzID0ge1xuICAgIENhbGxiYWNrLFxuICAgIEV2ZW50c05vdGlmeSxcbiAgICBTZXRCaW5kaW5ncyxcbiAgICBldmVudExpc3RlbmVycyxcbiAgICBjYWxsYmFja3MsXG4gICAgZmxhZ3M6IHtcbiAgICAgICAgZGlzYWJsZVNjcm9sbGJhckRyYWc6IGZhbHNlLFxuICAgICAgICBkaXNhYmxlRGVmYXVsdENvbnRleHRNZW51OiBmYWxzZSxcbiAgICAgICAgZW5hYmxlUmVzaXplOiBmYWxzZSxcbiAgICAgICAgZGVmYXVsdEN1cnNvcjogbnVsbCxcbiAgICAgICAgYm9yZGVyVGhpY2tuZXNzOiA2LFxuICAgICAgICBzaG91bGREcmFnOiBmYWxzZSxcbiAgICAgICAgZGVmZXJEcmFnVG9Nb3VzZU1vdmU6IHRydWUsXG4gICAgICAgIGNzc0RyYWdQcm9wZXJ0eTogXCItLXdhaWxzLWRyYWdnYWJsZVwiLFxuICAgICAgICBjc3NEcmFnVmFsdWU6IFwiZHJhZ1wiLFxuICAgICAgICBjc3NEcm9wUHJvcGVydHk6IFwiLS13YWlscy1kcm9wLXRhcmdldFwiLFxuICAgICAgICBjc3NEcm9wVmFsdWU6IFwiZHJvcFwiLFxuICAgICAgICBlbmFibGVXYWlsc0RyYWdBbmREcm9wOiBmYWxzZSxcbiAgICB9XG59O1xuXG4vLyBTZXQgdGhlIGJpbmRpbmdzXG5pZiAod2luZG93LndhaWxzYmluZGluZ3MpIHtcbiAgICB3aW5kb3cud2FpbHMuU2V0QmluZGluZ3Mod2luZG93LndhaWxzYmluZGluZ3MpO1xuICAgIGRlbGV0ZSB3aW5kb3cud2FpbHMuU2V0QmluZGluZ3M7XG59XG5cbi8vIChib29sKSBUaGlzIGlzIGV2YWx1YXRlZCBhdCBidWlsZCB0aW1lIGluIHBhY2thZ2UuanNvblxuaWYgKCFERUJVRykge1xuICAgIGRlbGV0ZSB3aW5kb3cud2FpbHNiaW5kaW5ncztcbn1cblxubGV0IGRyYWdUZXN0ID0gZnVuY3Rpb24oZSkge1xuICAgIHZhciB2YWwgPSB3aW5kb3cuZ2V0Q29tcHV0ZWRTdHlsZShlLnRhcmdldCkuZ2V0UHJvcGVydHlWYWx1ZSh3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1Byb3BlcnR5KTtcbiAgICBpZiAodmFsKSB7XG4gICAgICAgIHZhbCA9IHZhbC50cmltKCk7XG4gICAgfVxuXG4gICAgaWYgKHZhbCAhPT0gd2luZG93LndhaWxzLmZsYWdzLmNzc0RyYWdWYWx1ZSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuXG4gICAgaWYgKGUuYnV0dG9ucyAhPT0gMSkge1xuICAgICAgICAvLyBEbyBub3Qgc3RhcnQgZHJhZ2dpbmcgaWYgbm90IHRoZSBwcmltYXJ5IGJ1dHRvbiBoYXMgYmVlbiBjbGlja2VkLlxuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuXG4gICAgaWYgKGUuZGV0YWlsICE9PSAxKSB7XG4gICAgICAgIC8vIERvIG5vdCBzdGFydCBkcmFnZ2luZyBpZiBtb3JlIHRoYW4gb25jZSBoYXMgYmVlbiBjbGlja2VkLCBlLmcuIHdoZW4gZG91YmxlIGNsaWNraW5nXG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG5cbiAgICByZXR1cm4gdHJ1ZTtcbn07XG5cbndpbmRvdy53YWlscy5zZXRDU1NEcmFnUHJvcGVydGllcyA9IGZ1bmN0aW9uKHByb3BlcnR5LCB2YWx1ZSkge1xuICAgIHdpbmRvdy53YWlscy5mbGFncy5jc3NEcmFnUHJvcGVydHkgPSBwcm9wZXJ0eTtcbiAgICB3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJhZ1ZhbHVlID0gdmFsdWU7XG59XG5cbndpbmRvdy53YWlscy5zZXRDU1NEcm9wUHJvcGVydGllcyA9IGZ1bmN0aW9uKHByb3BlcnR5LCB2YWx1ZSkge1xuICAgIHdpbmRvdy53YWlscy5mbGFncy5jc3NEcm9wUHJvcGVydHkgPSBwcm9wZXJ0eTtcbiAgICB3aW5kb3cud2FpbHMuZmxhZ3MuY3NzRHJvcFZhbHVlID0gdmFsdWU7XG59XG5cbndpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdtb3VzZWRvd24nLCAoZSkgPT4ge1xuICAgIC8vIENoZWNrIGZvciByZXNpemluZ1xuICAgIGlmICh3aW5kb3cud2FpbHMuZmxhZ3MucmVzaXplRWRnZSkge1xuICAgICAgICB3aW5kb3cuV2FpbHNJbnZva2UoXCJyZXNpemU6XCIgKyB3aW5kb3cud2FpbHMuZmxhZ3MucmVzaXplRWRnZSk7XG4gICAgICAgIGUucHJldmVudERlZmF1bHQoKTtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGlmIChkcmFnVGVzdChlKSkge1xuICAgICAgICBpZiAod2luZG93LndhaWxzLmZsYWdzLmRpc2FibGVTY3JvbGxiYXJEcmFnKSB7XG4gICAgICAgICAgICAvLyBUaGlzIGNoZWNrcyBmb3IgY2xpY2tzIG9uIHRoZSBzY3JvbGwgYmFyXG4gICAgICAgICAgICBpZiAoZS5vZmZzZXRYID4gZS50YXJnZXQuY2xpZW50V2lkdGggfHwgZS5vZmZzZXRZID4gZS50YXJnZXQuY2xpZW50SGVpZ2h0KSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIGlmICh3aW5kb3cud2FpbHMuZmxhZ3MuZGVmZXJEcmFnVG9Nb3VzZU1vdmUpIHtcbiAgICAgICAgICAgIHdpbmRvdy53YWlscy5mbGFncy5zaG91bGREcmFnID0gdHJ1ZTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGUucHJldmVudERlZmF1bHQoKVxuICAgICAgICAgICAgd2luZG93LldhaWxzSW52b2tlKFwiZHJhZ1wiKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm47XG4gICAgfSBlbHNlIHtcbiAgICAgICAgd2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcgPSBmYWxzZTtcbiAgICB9XG59KTtcblxud2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ21vdXNldXAnLCAoKSA9PiB7XG4gICAgd2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcgPSBmYWxzZTtcbn0pO1xuXG5mdW5jdGlvbiBzZXRSZXNpemUoY3Vyc29yKSB7XG4gICAgZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LnN0eWxlLmN1cnNvciA9IGN1cnNvciB8fCB3aW5kb3cud2FpbHMuZmxhZ3MuZGVmYXVsdEN1cnNvcjtcbiAgICB3aW5kb3cud2FpbHMuZmxhZ3MucmVzaXplRWRnZSA9IGN1cnNvcjtcbn1cblxud2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ21vdXNlbW92ZScsIGZ1bmN0aW9uKGUpIHtcbiAgICBpZiAod2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcpIHtcbiAgICAgICAgd2luZG93LndhaWxzLmZsYWdzLnNob3VsZERyYWcgPSBmYWxzZTtcbiAgICAgICAgbGV0IG1vdXNlUHJlc3NlZCA9IGUuYnV0dG9ucyAhPT0gdW5kZWZpbmVkID8gZS5idXR0b25zIDogZS53aGljaDtcbiAgICAgICAgaWYgKG1vdXNlUHJlc3NlZCA+IDApIHtcbiAgICAgICAgICAgIHdpbmRvdy5XYWlsc0ludm9rZShcImRyYWdcIik7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICB9XG4gICAgaWYgKCF3aW5kb3cud2FpbHMuZmxhZ3MuZW5hYmxlUmVzaXplKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKHdpbmRvdy53YWlscy5mbGFncy5kZWZhdWx0Q3Vyc29yID09IG51bGwpIHtcbiAgICAgICAgd2luZG93LndhaWxzLmZsYWdzLmRlZmF1bHRDdXJzb3IgPSBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuc3R5bGUuY3Vyc29yO1xuICAgIH1cbiAgICBpZiAod2luZG93Lm91dGVyV2lkdGggLSBlLmNsaWVudFggPCB3aW5kb3cud2FpbHMuZmxhZ3MuYm9yZGVyVGhpY2tuZXNzICYmIHdpbmRvdy5vdXRlckhlaWdodCAtIGUuY2xpZW50WSA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3MpIHtcbiAgICAgICAgZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LnN0eWxlLmN1cnNvciA9IFwic2UtcmVzaXplXCI7XG4gICAgfVxuICAgIGxldCByaWdodEJvcmRlciA9IHdpbmRvdy5vdXRlcldpZHRoIC0gZS5jbGllbnRYIDwgd2luZG93LndhaWxzLmZsYWdzLmJvcmRlclRoaWNrbmVzcztcbiAgICBsZXQgbGVmdEJvcmRlciA9IGUuY2xpZW50WCA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3M7XG4gICAgbGV0IHRvcEJvcmRlciA9IGUuY2xpZW50WSA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3M7XG4gICAgbGV0IGJvdHRvbUJvcmRlciA9IHdpbmRvdy5vdXRlckhlaWdodCAtIGUuY2xpZW50WSA8IHdpbmRvdy53YWlscy5mbGFncy5ib3JkZXJUaGlja25lc3M7XG5cbiAgICAvLyBJZiB3ZSBhcmVuJ3Qgb24gYW4gZWRnZSwgYnV0IHdlcmUsIHJlc2V0IHRoZSBjdXJzb3IgdG8gZGVmYXVsdFxuICAgIGlmICghbGVmdEJvcmRlciAmJiAhcmlnaHRCb3JkZXIgJiYgIXRvcEJvcmRlciAmJiAhYm90dG9tQm9yZGVyICYmIHdpbmRvdy53YWlscy5mbGFncy5yZXNpemVFZGdlICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgc2V0UmVzaXplKCk7XG4gICAgfSBlbHNlIGlmIChyaWdodEJvcmRlciAmJiBib3R0b21Cb3JkZXIpIHNldFJlc2l6ZShcInNlLXJlc2l6ZVwiKTtcbiAgICBlbHNlIGlmIChsZWZ0Qm9yZGVyICYmIGJvdHRvbUJvcmRlcikgc2V0UmVzaXplKFwic3ctcmVzaXplXCIpO1xuICAgIGVsc2UgaWYgKGxlZnRCb3JkZXIgJiYgdG9wQm9yZGVyKSBzZXRSZXNpemUoXCJudy1yZXNpemVcIik7XG4gICAgZWxzZSBpZiAodG9wQm9yZGVyICYmIHJpZ2h0Qm9yZGVyKSBzZXRSZXNpemUoXCJuZS1yZXNpemVcIik7XG4gICAgZWxzZSBpZiAobGVmdEJvcmRlcikgc2V0UmVzaXplKFwidy1yZXNpemVcIik7XG4gICAgZWxzZSBpZiAodG9wQm9yZGVyKSBzZXRSZXNpemUoXCJuLXJlc2l6ZVwiKTtcbiAgICBlbHNlIGlmIChib3R0b21Cb3JkZXIpIHNldFJlc2l6ZShcInMtcmVzaXplXCIpO1xuICAgIGVsc2UgaWYgKHJpZ2h0Qm9yZGVyKSBzZXRSZXNpemUoXCJlLXJlc2l6ZVwiKTtcblxufSk7XG5cbi8vIFNldHVwIGNvbnRleHQgbWVudSBob29rXG53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcignY29udGV4dG1lbnUnLCBmdW5jdGlvbihlKSB7XG4gICAgLy8gYWx3YXlzIHNob3cgdGhlIGNvbnRleHRtZW51IGluIGRlYnVnICYgZGV2XG4gICAgaWYgKERFQlVHKSByZXR1cm47XG5cbiAgICBpZiAod2luZG93LndhaWxzLmZsYWdzLmRpc2FibGVEZWZhdWx0Q29udGV4dE1lbnUpIHtcbiAgICAgICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xuICAgIH0gZWxzZSB7XG4gICAgICAgIENvbnRleHRNZW51LnByb2Nlc3NEZWZhdWx0Q29udGV4dE1lbnUoZSk7XG4gICAgfVxufSk7XG5cbndpbmRvdy5XYWlsc0ludm9rZShcInJ1bnRpbWU6cmVhZHlcIik7Il0sCiAgIm1hcHBpbmdzIjogIjs7Ozs7Ozs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFrQkEsV0FBUyxlQUFlLE9BQU8sU0FBUztBQUl2QyxXQUFPLFlBQVksTUFBTSxRQUFRLE9BQU87QUFBQSxFQUN6QztBQVFPLFdBQVMsU0FBUyxTQUFTO0FBQ2pDLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxTQUFTLFNBQVM7QUFDakMsbUJBQWUsS0FBSyxPQUFPO0FBQUEsRUFDNUI7QUFRTyxXQUFTLFNBQVMsU0FBUztBQUNqQyxtQkFBZSxLQUFLLE9BQU87QUFBQSxFQUM1QjtBQVFPLFdBQVMsUUFBUSxTQUFTO0FBQ2hDLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxXQUFXLFNBQVM7QUFDbkMsbUJBQWUsS0FBSyxPQUFPO0FBQUEsRUFDNUI7QUFRTyxXQUFTLFNBQVMsU0FBUztBQUNqQyxtQkFBZSxLQUFLLE9BQU87QUFBQSxFQUM1QjtBQVFPLFdBQVMsU0FBUyxTQUFTO0FBQ2pDLG1CQUFlLEtBQUssT0FBTztBQUFBLEVBQzVCO0FBUU8sV0FBUyxZQUFZLFVBQVU7QUFDckMsbUJBQWUsS0FBSyxRQUFRO0FBQUEsRUFDN0I7QUFHTyxNQUFNLFdBQVc7QUFBQSxJQUN2QixPQUFPO0FBQUEsSUFDUCxPQUFPO0FBQUEsSUFDUCxNQUFNO0FBQUEsSUFDTixTQUFTO0FBQUEsSUFDVCxPQUFPO0FBQUEsRUFDUjs7O0FDOUZBLE1BQU0sV0FBTixNQUFlO0FBQUEsSUFRWCxZQUFZLFdBQVcsVUFBVSxjQUFjO0FBQzNDLFdBQUssWUFBWTtBQUVqQixXQUFLLGVBQWUsZ0JBQWdCO0FBR3BDLFdBQUssV0FBVyxDQUFDLFNBQVM7QUFDdEIsaUJBQVMsTUFBTSxNQUFNLElBQUk7QUFFekIsWUFBSSxLQUFLLGlCQUFpQixJQUFJO0FBQzFCLGlCQUFPO0FBQUEsUUFDWDtBQUVBLGFBQUssZ0JBQWdCO0FBQ3JCLGVBQU8sS0FBSyxpQkFBaUI7QUFBQSxNQUNqQztBQUFBLElBQ0o7QUFBQSxFQUNKO0FBRU8sTUFBTSxpQkFBaUIsQ0FBQztBQVd4QixXQUFTLGlCQUFpQixXQUFXLFVBQVUsY0FBYztBQUNoRSxtQkFBZSxhQUFhLGVBQWUsY0FBYyxDQUFDO0FBQzFELFVBQU0sZUFBZSxJQUFJLFNBQVMsV0FBVyxVQUFVLFlBQVk7QUFDbkUsbUJBQWUsV0FBVyxLQUFLLFlBQVk7QUFDM0MsV0FBTyxNQUFNLFlBQVksWUFBWTtBQUFBLEVBQ3pDO0FBVU8sV0FBUyxTQUFTLFdBQVcsVUFBVTtBQUMxQyxXQUFPLGlCQUFpQixXQUFXLFVBQVUsRUFBRTtBQUFBLEVBQ25EO0FBVU8sV0FBUyxXQUFXLFdBQVcsVUFBVTtBQUM1QyxXQUFPLGlCQUFpQixXQUFXLFVBQVUsQ0FBQztBQUFBLEVBQ2xEO0FBRUEsV0FBUyxnQkFBZ0IsV0FBVztBQUdoQyxRQUFJLFlBQVksVUFBVTtBQUcxQixVQUFNLHVCQUF1QixlQUFlLFlBQVksTUFBTSxLQUFLLENBQUM7QUFHcEUsUUFBSSxxQkFBcUIsUUFBUTtBQUc3QixlQUFTLFFBQVEscUJBQXFCLFNBQVMsR0FBRyxTQUFTLEdBQUcsU0FBUyxHQUFHO0FBR3RFLGNBQU0sV0FBVyxxQkFBcUI7QUFFdEMsWUFBSSxPQUFPLFVBQVU7QUFHckIsY0FBTSxVQUFVLFNBQVMsU0FBUyxJQUFJO0FBQ3RDLFlBQUksU0FBUztBQUVULCtCQUFxQixPQUFPLE9BQU8sQ0FBQztBQUFBLFFBQ3hDO0FBQUEsTUFDSjtBQUdBLFVBQUkscUJBQXFCLFdBQVcsR0FBRztBQUNuQyx1QkFBZSxTQUFTO0FBQUEsTUFDNUIsT0FBTztBQUNILHVCQUFlLGFBQWE7QUFBQSxNQUNoQztBQUFBLElBQ0o7QUFBQSxFQUNKO0FBU08sV0FBUyxhQUFhLGVBQWU7QUFFeEMsUUFBSTtBQUNKLFFBQUk7QUFDQSxnQkFBVSxLQUFLLE1BQU0sYUFBYTtBQUFBLElBQ3RDLFNBQVMsR0FBUDtBQUNFLFlBQU0sUUFBUSxvQ0FBb0M7QUFDbEQsWUFBTSxJQUFJLE1BQU0sS0FBSztBQUFBLElBQ3pCO0FBQ0Esb0JBQWdCLE9BQU87QUFBQSxFQUMzQjtBQVFPLFdBQVMsV0FBVyxXQUFXO0FBRWxDLFVBQU0sVUFBVTtBQUFBLE1BQ1osTUFBTTtBQUFBLE1BQ04sTUFBTSxDQUFDLEVBQUUsTUFBTSxNQUFNLFNBQVMsRUFBRSxNQUFNLENBQUM7QUFBQSxJQUMzQztBQUdBLG9CQUFnQixPQUFPO0FBR3ZCLFdBQU8sWUFBWSxPQUFPLEtBQUssVUFBVSxPQUFPLENBQUM7QUFBQSxFQUNyRDtBQUVBLFdBQVMsZUFBZSxXQUFXO0FBRS9CLFdBQU8sZUFBZTtBQUd0QixXQUFPLFlBQVksT0FBTyxTQUFTO0FBQUEsRUFDdkM7QUFTTyxXQUFTLFVBQVUsY0FBYyxzQkFBc0I7QUFDMUQsbUJBQWUsU0FBUztBQUV4QixRQUFJLHFCQUFxQixTQUFTLEdBQUc7QUFDakMsMkJBQXFCLFFBQVEsQ0FBQUEsZUFBYTtBQUN0Qyx1QkFBZUEsVUFBUztBQUFBLE1BQzVCLENBQUM7QUFBQSxJQUNMO0FBQUEsRUFDSjtBQUtRLFdBQVMsZUFBZTtBQUM1QixVQUFNLGFBQWEsT0FBTyxLQUFLLGNBQWM7QUFDN0MsZUFBVyxRQUFRLGVBQWE7QUFDNUIscUJBQWUsU0FBUztBQUFBLElBQzVCLENBQUM7QUFBQSxFQUNMO0FBT0MsV0FBUyxZQUFZLFVBQVU7QUFDNUIsVUFBTSxZQUFZLFNBQVM7QUFDM0IsUUFBSSxlQUFlLGVBQWU7QUFBVztBQUc3QyxtQkFBZSxhQUFhLGVBQWUsV0FBVyxPQUFPLE9BQUssTUFBTSxRQUFRO0FBR2hGLFFBQUksZUFBZSxXQUFXLFdBQVcsR0FBRztBQUN4QyxxQkFBZSxTQUFTO0FBQUEsSUFDNUI7QUFBQSxFQUNKOzs7QUMxTU8sTUFBTSxZQUFZLENBQUM7QUFPMUIsV0FBUyxlQUFlO0FBQ3ZCLFFBQUksUUFBUSxJQUFJLFlBQVksQ0FBQztBQUM3QixXQUFPLE9BQU8sT0FBTyxnQkFBZ0IsS0FBSyxFQUFFO0FBQUEsRUFDN0M7QUFRQSxXQUFTLGNBQWM7QUFDdEIsV0FBTyxLQUFLLE9BQU8sSUFBSTtBQUFBLEVBQ3hCO0FBR0EsTUFBSTtBQUNKLE1BQUksT0FBTyxRQUFRO0FBQ2xCLGlCQUFhO0FBQUEsRUFDZCxPQUFPO0FBQ04saUJBQWE7QUFBQSxFQUNkO0FBaUJPLFdBQVMsS0FBSyxNQUFNLE1BQU0sU0FBUztBQUd6QyxRQUFJLFdBQVcsTUFBTTtBQUNwQixnQkFBVTtBQUFBLElBQ1g7QUFHQSxXQUFPLElBQUksUUFBUSxTQUFVLFNBQVMsUUFBUTtBQUc3QyxVQUFJO0FBQ0osU0FBRztBQUNGLHFCQUFhLE9BQU8sTUFBTSxXQUFXO0FBQUEsTUFDdEMsU0FBUyxVQUFVO0FBRW5CLFVBQUk7QUFFSixVQUFJLFVBQVUsR0FBRztBQUNoQix3QkFBZ0IsV0FBVyxXQUFZO0FBQ3RDLGlCQUFPLE1BQU0sYUFBYSxPQUFPLDZCQUE2QixVQUFVLENBQUM7QUFBQSxRQUMxRSxHQUFHLE9BQU87QUFBQSxNQUNYO0FBR0EsZ0JBQVUsY0FBYztBQUFBLFFBQ3ZCO0FBQUEsUUFDQTtBQUFBLFFBQ0E7QUFBQSxNQUNEO0FBRUEsVUFBSTtBQUNILGNBQU0sVUFBVTtBQUFBLFVBQ2Y7QUFBQSxVQUNBO0FBQUEsVUFDQTtBQUFBLFFBQ0Q7QUFHUyxlQUFPLFlBQVksTUFBTSxLQUFLLFVBQVUsT0FBTyxDQUFDO0FBQUEsTUFDcEQsU0FBUyxHQUFQO0FBRUUsZ0JBQVEsTUFBTSxDQUFDO0FBQUEsTUFDbkI7QUFBQSxJQUNKLENBQUM7QUFBQSxFQUNMO0FBRUEsU0FBTyxpQkFBaUIsQ0FBQyxJQUFJLE1BQU0sWUFBWTtBQUczQyxRQUFJLFdBQVcsTUFBTTtBQUNqQixnQkFBVTtBQUFBLElBQ2Q7QUFHQSxXQUFPLElBQUksUUFBUSxTQUFVLFNBQVMsUUFBUTtBQUcxQyxVQUFJO0FBQ0osU0FBRztBQUNDLHFCQUFhLEtBQUssTUFBTSxXQUFXO0FBQUEsTUFDdkMsU0FBUyxVQUFVO0FBRW5CLFVBQUk7QUFFSixVQUFJLFVBQVUsR0FBRztBQUNiLHdCQUFnQixXQUFXLFdBQVk7QUFDbkMsaUJBQU8sTUFBTSxvQkFBb0IsS0FBSyw2QkFBNkIsVUFBVSxDQUFDO0FBQUEsUUFDbEYsR0FBRyxPQUFPO0FBQUEsTUFDZDtBQUdBLGdCQUFVLGNBQWM7QUFBQSxRQUNwQjtBQUFBLFFBQ0E7QUFBQSxRQUNBO0FBQUEsTUFDSjtBQUVBLFVBQUk7QUFDQSxjQUFNLFVBQVU7QUFBQSxVQUN4QjtBQUFBLFVBQ0E7QUFBQSxVQUNBO0FBQUEsUUFDRDtBQUdTLGVBQU8sWUFBWSxNQUFNLEtBQUssVUFBVSxPQUFPLENBQUM7QUFBQSxNQUNwRCxTQUFTLEdBQVA7QUFFRSxnQkFBUSxNQUFNLENBQUM7QUFBQSxNQUNuQjtBQUFBLElBQ0osQ0FBQztBQUFBLEVBQ0w7QUFVTyxXQUFTLFNBQVMsaUJBQWlCO0FBRXpDLFFBQUk7QUFDSixRQUFJO0FBQ0gsZ0JBQVUsS0FBSyxNQUFNLGVBQWU7QUFBQSxJQUNyQyxTQUFTLEdBQVA7QUFDRCxZQUFNLFFBQVEsb0NBQW9DLEVBQUUscUJBQXFCO0FBQ3pFLGNBQVEsU0FBUyxLQUFLO0FBQ3RCLFlBQU0sSUFBSSxNQUFNLEtBQUs7QUFBQSxJQUN0QjtBQUNBLFFBQUksYUFBYSxRQUFRO0FBQ3pCLFFBQUksZUFBZSxVQUFVO0FBQzdCLFFBQUksQ0FBQyxjQUFjO0FBQ2xCLFlBQU0sUUFBUSxhQUFhO0FBQzNCLGNBQVEsTUFBTSxLQUFLO0FBQ25CLFlBQU0sSUFBSSxNQUFNLEtBQUs7QUFBQSxJQUN0QjtBQUNBLGlCQUFhLGFBQWEsYUFBYTtBQUV2QyxXQUFPLFVBQVU7QUFFakIsUUFBSSxRQUFRLE9BQU87QUFDbEIsbUJBQWEsT0FBTyxRQUFRLEtBQUs7QUFBQSxJQUNsQyxPQUFPO0FBQ04sbUJBQWEsUUFBUSxRQUFRLE1BQU07QUFBQSxJQUNwQztBQUFBLEVBQ0Q7OztBQzFLQSxTQUFPLEtBQUssQ0FBQztBQUVOLFdBQVMsWUFBWSxhQUFhO0FBQ3hDLFFBQUk7QUFDSCxvQkFBYyxLQUFLLE1BQU0sV0FBVztBQUFBLElBQ3JDLFNBQVMsR0FBUDtBQUNELGNBQVEsTUFBTSxDQUFDO0FBQUEsSUFDaEI7QUFHQSxXQUFPLEtBQUssT0FBTyxNQUFNLENBQUM7QUFHMUIsV0FBTyxLQUFLLFdBQVcsRUFBRSxRQUFRLENBQUMsZ0JBQWdCO0FBR2pELGFBQU8sR0FBRyxlQUFlLE9BQU8sR0FBRyxnQkFBZ0IsQ0FBQztBQUdwRCxhQUFPLEtBQUssWUFBWSxZQUFZLEVBQUUsUUFBUSxDQUFDLGVBQWU7QUFHN0QsZUFBTyxHQUFHLGFBQWEsY0FBYyxPQUFPLEdBQUcsYUFBYSxlQUFlLENBQUM7QUFFNUUsZUFBTyxLQUFLLFlBQVksYUFBYSxXQUFXLEVBQUUsUUFBUSxDQUFDLGVBQWU7QUFFekUsaUJBQU8sR0FBRyxhQUFhLFlBQVksY0FBYyxXQUFZO0FBRzVELGdCQUFJLFVBQVU7QUFHZCxxQkFBUyxVQUFVO0FBQ2xCLG9CQUFNLE9BQU8sQ0FBQyxFQUFFLE1BQU0sS0FBSyxTQUFTO0FBQ3BDLHFCQUFPLEtBQUssQ0FBQyxhQUFhLFlBQVksVUFBVSxFQUFFLEtBQUssR0FBRyxHQUFHLE1BQU0sT0FBTztBQUFBLFlBQzNFO0FBR0Esb0JBQVEsYUFBYSxTQUFVLFlBQVk7QUFDMUMsd0JBQVU7QUFBQSxZQUNYO0FBR0Esb0JBQVEsYUFBYSxXQUFZO0FBQ2hDLHFCQUFPO0FBQUEsWUFDUjtBQUVBLG1CQUFPO0FBQUEsVUFDUixFQUFFO0FBQUEsUUFDSCxDQUFDO0FBQUEsTUFDRixDQUFDO0FBQUEsSUFDRixDQUFDO0FBQUEsRUFDRjs7O0FDbEVBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBZU8sV0FBUyxlQUFlO0FBQzNCLFdBQU8sU0FBUyxPQUFPO0FBQUEsRUFDM0I7QUFFTyxXQUFTLGtCQUFrQjtBQUM5QixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBRU8sV0FBUyw4QkFBOEI7QUFDMUMsV0FBTyxZQUFZLE9BQU87QUFBQSxFQUM5QjtBQUVPLFdBQVMsc0JBQXNCO0FBQ2xDLFdBQU8sWUFBWSxNQUFNO0FBQUEsRUFDN0I7QUFFTyxXQUFTLHFCQUFxQjtBQUNqQyxXQUFPLFlBQVksTUFBTTtBQUFBLEVBQzdCO0FBT08sV0FBUyxlQUFlO0FBQzNCLFdBQU8sWUFBWSxJQUFJO0FBQUEsRUFDM0I7QUFRTyxXQUFTLGVBQWUsT0FBTztBQUNsQyxXQUFPLFlBQVksT0FBTyxLQUFLO0FBQUEsRUFDbkM7QUFPTyxXQUFTLG1CQUFtQjtBQUMvQixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBT08sV0FBUyxxQkFBcUI7QUFDakMsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQVFPLFdBQVMscUJBQXFCO0FBQ2pDLFdBQU8sS0FBSywyQkFBMkI7QUFBQSxFQUMzQztBQVNPLFdBQVMsY0FBYyxPQUFPLFFBQVE7QUFDekMsV0FBTyxZQUFZLFFBQVEsUUFBUSxNQUFNLE1BQU07QUFBQSxFQUNuRDtBQVNPLFdBQVMsZ0JBQWdCO0FBQzVCLFdBQU8sS0FBSyxzQkFBc0I7QUFBQSxFQUN0QztBQVNPLFdBQVMsaUJBQWlCLE9BQU8sUUFBUTtBQUM1QyxXQUFPLFlBQVksUUFBUSxRQUFRLE1BQU0sTUFBTTtBQUFBLEVBQ25EO0FBU08sV0FBUyxpQkFBaUIsT0FBTyxRQUFRO0FBQzVDLFdBQU8sWUFBWSxRQUFRLFFBQVEsTUFBTSxNQUFNO0FBQUEsRUFDbkQ7QUFTTyxXQUFTLHFCQUFxQixHQUFHO0FBRXBDLFdBQU8sWUFBWSxXQUFXLElBQUksTUFBTSxJQUFJO0FBQUEsRUFDaEQ7QUFZTyxXQUFTLGtCQUFrQixHQUFHLEdBQUc7QUFDcEMsV0FBTyxZQUFZLFFBQVEsSUFBSSxNQUFNLENBQUM7QUFBQSxFQUMxQztBQVFPLFdBQVMsb0JBQW9CO0FBQ2hDLFdBQU8sS0FBSyxxQkFBcUI7QUFBQSxFQUNyQztBQU9PLFdBQVMsYUFBYTtBQUN6QixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBT08sV0FBUyxhQUFhO0FBQ3pCLFdBQU8sWUFBWSxJQUFJO0FBQUEsRUFDM0I7QUFPTyxXQUFTLGlCQUFpQjtBQUM3QixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBT08sV0FBUyx1QkFBdUI7QUFDbkMsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQU9PLFdBQVMsbUJBQW1CO0FBQy9CLFdBQU8sWUFBWSxJQUFJO0FBQUEsRUFDM0I7QUFRTyxXQUFTLG9CQUFvQjtBQUNoQyxXQUFPLEtBQUssMEJBQTBCO0FBQUEsRUFDMUM7QUFPTyxXQUFTLGlCQUFpQjtBQUM3QixXQUFPLFlBQVksSUFBSTtBQUFBLEVBQzNCO0FBT08sV0FBUyxtQkFBbUI7QUFDL0IsV0FBTyxZQUFZLElBQUk7QUFBQSxFQUMzQjtBQVFPLFdBQVMsb0JBQW9CO0FBQ2hDLFdBQU8sS0FBSywwQkFBMEI7QUFBQSxFQUMxQztBQVFPLFdBQVMsaUJBQWlCO0FBQzdCLFdBQU8sS0FBSyx1QkFBdUI7QUFBQSxFQUN2QztBQVdPLFdBQVMsMEJBQTBCLEdBQUcsR0FBRyxHQUFHLEdBQUc7QUFDbEQsUUFBSSxPQUFPLEtBQUssVUFBVSxFQUFDLEdBQUcsS0FBSyxHQUFHLEdBQUcsS0FBSyxHQUFHLEdBQUcsS0FBSyxHQUFHLEdBQUcsS0FBSyxJQUFHLENBQUM7QUFDeEUsV0FBTyxZQUFZLFFBQVEsSUFBSTtBQUFBLEVBQ25DOzs7QUMzUUE7QUFBQTtBQUFBO0FBQUE7QUFzQk8sV0FBUyxlQUFlO0FBQzNCLFdBQU8sS0FBSyxxQkFBcUI7QUFBQSxFQUNyQzs7O0FDeEJBO0FBQUE7QUFBQTtBQUFBO0FBS08sV0FBUyxlQUFlLEtBQUs7QUFDbEMsV0FBTyxZQUFZLFFBQVEsR0FBRztBQUFBLEVBQ2hDOzs7QUNQQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBb0JPLFdBQVMsaUJBQWlCLE1BQU07QUFDbkMsV0FBTyxLQUFLLDJCQUEyQixDQUFDLElBQUksQ0FBQztBQUFBLEVBQ2pEO0FBU08sV0FBUyxtQkFBbUI7QUFDL0IsV0FBTyxLQUFLLHlCQUF5QjtBQUFBLEVBQ3pDOzs7QUNqQ0E7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFjQSxNQUFNLFFBQVE7QUFBQSxJQUNWLFlBQVk7QUFBQSxJQUNaLHNCQUFzQjtBQUFBLElBQ3RCLGVBQWU7QUFBQSxJQUNmLGdCQUFnQjtBQUFBLElBQ2hCLHVCQUF1QjtBQUFBLEVBQzNCO0FBRUEsTUFBTSxxQkFBcUI7QUFRM0IsV0FBUyxxQkFBcUIsT0FBTztBQUNqQyxVQUFNLGVBQWUsTUFBTSxpQkFBaUIsT0FBTyxNQUFNLE1BQU0sZUFBZSxFQUFFLEtBQUs7QUFDckYsUUFBSSxjQUFjO0FBQ2QsVUFBSSxpQkFBaUIsT0FBTyxNQUFNLE1BQU0sY0FBYztBQUNsRCxlQUFPO0FBQUEsTUFDWDtBQUlBLGFBQU87QUFBQSxJQUNYO0FBQ0EsV0FBTztBQUFBLEVBQ1g7QUFPQSxXQUFTLFdBQVcsR0FBRztBQUluQixVQUFNLGFBQWEsRUFBRSxhQUFhLE1BQU0sU0FBUyxPQUFPO0FBR3hELFFBQUksQ0FBQyxZQUFZO0FBQ2I7QUFBQSxJQUNKO0FBR0EsTUFBRSxlQUFlO0FBQ2pCLE1BQUUsYUFBYSxhQUFhO0FBRTVCLFFBQUksQ0FBQyxPQUFPLE1BQU0sTUFBTSx3QkFBd0I7QUFDNUM7QUFBQSxJQUNKO0FBRUEsUUFBSSxDQUFDLE1BQU0sZUFBZTtBQUN0QjtBQUFBLElBQ0o7QUFFQSxVQUFNLFVBQVUsRUFBRTtBQUdsQixRQUFHLE1BQU07QUFBZ0IsWUFBTSxlQUFlO0FBRzlDLFFBQUksQ0FBQyxXQUFXLENBQUMscUJBQXFCLGlCQUFpQixPQUFPLENBQUMsR0FBRztBQUM5RDtBQUFBLElBQ0o7QUFFQSxRQUFJLGlCQUFpQjtBQUNyQixXQUFPLGdCQUFnQjtBQUVuQixVQUFJLHFCQUFxQixpQkFBaUIsY0FBYyxDQUFDLEdBQUc7QUFDeEQsdUJBQWUsVUFBVSxJQUFJLGtCQUFrQjtBQUFBLE1BQ25EO0FBQ0EsdUJBQWlCLGVBQWU7QUFBQSxJQUNwQztBQUFBLEVBQ0o7QUFPQSxXQUFTLFlBQVksR0FBRztBQUVwQixVQUFNLGFBQWEsRUFBRSxhQUFhLE1BQU0sU0FBUyxPQUFPO0FBR3hELFFBQUksQ0FBQyxZQUFZO0FBQ2I7QUFBQSxJQUNKO0FBR0EsTUFBRSxlQUFlO0FBRWpCLFFBQUksQ0FBQyxPQUFPLE1BQU0sTUFBTSx3QkFBd0I7QUFDNUM7QUFBQSxJQUNKO0FBRUEsUUFBSSxDQUFDLE1BQU0sZUFBZTtBQUN0QjtBQUFBLElBQ0o7QUFHQSxRQUFJLENBQUMsRUFBRSxVQUFVLENBQUMscUJBQXFCLGlCQUFpQixFQUFFLE1BQU0sQ0FBQyxHQUFHO0FBQ2hFLGFBQU87QUFBQSxJQUNYO0FBR0EsUUFBRyxNQUFNO0FBQWdCLFlBQU0sZUFBZTtBQUc5QyxVQUFNLGlCQUFpQixNQUFNO0FBRXpCLFlBQU0sS0FBSyxTQUFTLHVCQUF1QixrQkFBa0IsQ0FBQyxFQUFFLFFBQVEsUUFBTSxHQUFHLFVBQVUsT0FBTyxrQkFBa0IsQ0FBQztBQUVySCxZQUFNLGlCQUFpQjtBQUV2QixVQUFJLE1BQU0sdUJBQXVCO0FBQzdCLHFCQUFhLE1BQU0scUJBQXFCO0FBQ3hDLGNBQU0sd0JBQXdCO0FBQUEsTUFDbEM7QUFBQSxJQUNKO0FBR0EsVUFBTSx3QkFBd0IsV0FBVyxNQUFNO0FBQzNDLFVBQUcsTUFBTTtBQUFnQixjQUFNLGVBQWU7QUFBQSxJQUNsRCxHQUFHLEVBQUU7QUFBQSxFQUNUO0FBT0EsV0FBUyxPQUFPLEdBQUc7QUFFZixVQUFNLGFBQWEsRUFBRSxhQUFhLE1BQU0sU0FBUyxPQUFPO0FBR3hELFFBQUksQ0FBQyxZQUFZO0FBQ2I7QUFBQSxJQUNKO0FBR0EsTUFBRSxlQUFlO0FBRWpCLFFBQUksQ0FBQyxPQUFPLE1BQU0sTUFBTSx3QkFBd0I7QUFDNUM7QUFBQSxJQUNKO0FBRUEsUUFBSSxvQkFBb0IsR0FBRztBQUV2QixVQUFJLFFBQVEsQ0FBQztBQUNiLFVBQUksRUFBRSxhQUFhLE9BQU87QUFDdEIsZ0JBQVEsQ0FBQyxHQUFHLEVBQUUsYUFBYSxLQUFLLEVBQUUsSUFBSSxDQUFDLE1BQU0sTUFBTTtBQUMvQyxjQUFJLEtBQUssU0FBUyxRQUFRO0FBQ3RCLG1CQUFPLEtBQUssVUFBVTtBQUFBLFVBQzFCO0FBQUEsUUFDSixDQUFDO0FBQUEsTUFDTCxPQUFPO0FBQ0gsZ0JBQVEsQ0FBQyxHQUFHLEVBQUUsYUFBYSxLQUFLO0FBQUEsTUFDcEM7QUFDQSxhQUFPLFFBQVEsaUJBQWlCLEVBQUUsR0FBRyxFQUFFLEdBQUcsS0FBSztBQUFBLElBQ25EO0FBRUEsUUFBSSxDQUFDLE1BQU0sZUFBZTtBQUN0QjtBQUFBLElBQ0o7QUFHQSxRQUFHLE1BQU07QUFBZ0IsWUFBTSxlQUFlO0FBRzlDLFVBQU0sS0FBSyxTQUFTLHVCQUF1QixrQkFBa0IsQ0FBQyxFQUFFLFFBQVEsUUFBTSxHQUFHLFVBQVUsT0FBTyxrQkFBa0IsQ0FBQztBQUFBLEVBQ3pIO0FBUU8sV0FBUyxzQkFBc0I7QUFDbEMsV0FBTyxPQUFPLFFBQVEsU0FBUyxvQ0FBb0M7QUFBQSxFQUN2RTtBQVVPLFdBQVMsaUJBQWlCLEdBQUcsR0FBRyxPQUFPO0FBRzFDLFFBQUksT0FBTyxRQUFRLFNBQVMsa0NBQWtDO0FBQzFELGFBQU8sUUFBUSxpQ0FBaUMsYUFBYSxLQUFLLEtBQUssS0FBSztBQUFBLElBQ2hGO0FBQUEsRUFDSjtBQW1CTyxXQUFTLFdBQVcsVUFBVSxlQUFlO0FBQ2hELFFBQUksT0FBTyxhQUFhLFlBQVk7QUFDaEMsY0FBUSxNQUFNLHVDQUF1QztBQUNyRDtBQUFBLElBQ0o7QUFFQSxRQUFJLE1BQU0sWUFBWTtBQUNsQjtBQUFBLElBQ0o7QUFDQSxVQUFNLGFBQWE7QUFFbkIsVUFBTSxRQUFRLE9BQU87QUFDckIsVUFBTSxnQkFBZ0IsVUFBVSxlQUFlLFVBQVUsWUFBWSxNQUFNLHVCQUF1QjtBQUNsRyxXQUFPLGlCQUFpQixZQUFZLFVBQVU7QUFDOUMsV0FBTyxpQkFBaUIsYUFBYSxXQUFXO0FBQ2hELFdBQU8saUJBQWlCLFFBQVEsTUFBTTtBQUV0QyxRQUFJLEtBQUs7QUFDVCxRQUFJLE1BQU0sZUFBZTtBQUNyQixXQUFLLFNBQVUsR0FBRyxHQUFHLE9BQU87QUFDeEIsY0FBTSxVQUFVLFNBQVMsaUJBQWlCLEdBQUcsQ0FBQztBQUU5QyxZQUFJLENBQUMsV0FBVyxDQUFDLHFCQUFxQixpQkFBaUIsT0FBTyxDQUFDLEdBQUc7QUFDOUQsaUJBQU87QUFBQSxRQUNYO0FBQ0EsaUJBQVMsR0FBRyxHQUFHLEtBQUs7QUFBQSxNQUN4QjtBQUFBLElBQ0o7QUFFQSxhQUFTLG1CQUFtQixFQUFFO0FBQUEsRUFDbEM7QUFLTyxXQUFTLGdCQUFnQjtBQUM1QixXQUFPLG9CQUFvQixZQUFZLFVBQVU7QUFDakQsV0FBTyxvQkFBb0IsYUFBYSxXQUFXO0FBQ25ELFdBQU8sb0JBQW9CLFFBQVEsTUFBTTtBQUN6QyxjQUFVLGlCQUFpQjtBQUMzQixVQUFNLGFBQWE7QUFBQSxFQUN2Qjs7O0FDNVFPLFdBQVMsMEJBQTBCLE9BQU87QUFFN0MsVUFBTSxVQUFVLE1BQU07QUFDdEIsVUFBTSxnQkFBZ0IsT0FBTyxpQkFBaUIsT0FBTztBQUNyRCxVQUFNLDJCQUEyQixjQUFjLGlCQUFpQix1QkFBdUIsRUFBRSxLQUFLO0FBQzlGLFlBQVEsMEJBQTBCO0FBQUEsTUFDOUIsS0FBSztBQUNEO0FBQUEsTUFDSixLQUFLO0FBQ0QsY0FBTSxlQUFlO0FBQ3JCO0FBQUEsTUFDSjtBQUVJLFlBQUksUUFBUSxtQkFBbUI7QUFDM0I7QUFBQSxRQUNKO0FBR0EsY0FBTSxZQUFZLE9BQU8sYUFBYTtBQUN0QyxjQUFNLGVBQWdCLFVBQVUsU0FBUyxFQUFFLFNBQVM7QUFDcEQsWUFBSSxjQUFjO0FBQ2QsbUJBQVMsSUFBSSxHQUFHLElBQUksVUFBVSxZQUFZLEtBQUs7QUFDM0Msa0JBQU0sUUFBUSxVQUFVLFdBQVcsQ0FBQztBQUNwQyxrQkFBTSxRQUFRLE1BQU0sZUFBZTtBQUNuQyxxQkFBUyxJQUFJLEdBQUcsSUFBSSxNQUFNLFFBQVEsS0FBSztBQUNuQyxvQkFBTSxPQUFPLE1BQU07QUFDbkIsa0JBQUksU0FBUyxpQkFBaUIsS0FBSyxNQUFNLEtBQUssR0FBRyxNQUFNLFNBQVM7QUFDNUQ7QUFBQSxjQUNKO0FBQUEsWUFDSjtBQUFBLFVBQ0o7QUFBQSxRQUNKO0FBRUEsWUFBSSxRQUFRLFlBQVksV0FBVyxRQUFRLFlBQVksWUFBWTtBQUMvRCxjQUFJLGdCQUFpQixDQUFDLFFBQVEsWUFBWSxDQUFDLFFBQVEsVUFBVztBQUMxRDtBQUFBLFVBQ0o7QUFBQSxRQUNKO0FBR0EsY0FBTSxlQUFlO0FBQUEsSUFDN0I7QUFBQSxFQUNKOzs7QUNqREE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQXFCTyxXQUFTLDBCQUEwQjtBQUN0QyxXQUFPLEtBQUssZ0NBQWdDO0FBQUEsRUFDaEQ7QUFVTyxXQUFTLHVCQUF1QjtBQUNuQyxXQUFPLEtBQUssNkJBQTZCO0FBQUEsRUFDN0M7QUFRTyxXQUFTLDBCQUEwQjtBQUN0QyxXQUFPLEtBQUssZ0NBQWdDO0FBQUEsRUFDaEQ7QUFVTyxXQUFTLG1DQUFtQztBQUMvQyxXQUFPLEtBQUsseUNBQXlDO0FBQUEsRUFDekQ7QUFVTyxXQUFTLGlDQUFpQztBQUM3QyxXQUFPLEtBQUssdUNBQXVDO0FBQUEsRUFDdkQ7QUFnQk8sV0FBUyxpQkFBaUIsU0FBUztBQUN0QyxXQUFPLEtBQUssMkJBQTJCLENBQUMsT0FBTyxDQUFDO0FBQUEsRUFDcEQ7QUFrQk8sV0FBUyw0QkFBNEIsU0FBUztBQUNqRCxXQUFPLEtBQUssc0NBQXNDLENBQUMsT0FBTyxDQUFDO0FBQUEsRUFDL0Q7QUFtQk8sV0FBUyw2QkFBNkIsVUFBVTtBQUNuRCxXQUFPLEtBQUssdUNBQXVDLENBQUMsUUFBUSxDQUFDO0FBQUEsRUFDakU7QUFTTyxXQUFTLDJCQUEyQixZQUFZO0FBQ25ELFdBQU8sS0FBSyxxQ0FBcUMsQ0FBQyxVQUFVLENBQUM7QUFBQSxFQUNqRTtBQVNPLFdBQVMsZ0NBQWdDO0FBQzVDLFdBQU8sS0FBSyxzQ0FBc0M7QUFBQSxFQUN0RDtBQVVPLFdBQVMsMEJBQTBCLFlBQVk7QUFDbEQsV0FBTyxLQUFLLG9DQUFvQyxDQUFDLFVBQVUsQ0FBQztBQUFBLEVBQ2hFO0FBU08sV0FBUyxrQ0FBa0M7QUFDOUMsV0FBTyxLQUFLLHdDQUF3QztBQUFBLEVBQ3hEO0FBVU8sV0FBUyw0QkFBNEIsWUFBWTtBQUNwRCxXQUFPLEtBQUssc0NBQXNDLENBQUMsVUFBVSxDQUFDO0FBQUEsRUFDbEU7QUFXTyxXQUFTLG1CQUFtQixZQUFZO0FBQzNDLFdBQU8sS0FBSyw2QkFBNkIsQ0FBQyxVQUFVLENBQUM7QUFBQSxFQUN6RDs7O0FDdktPLFdBQVMsT0FBTztBQUNuQixXQUFPLFlBQVksR0FBRztBQUFBLEVBQzFCO0FBRU8sV0FBUyxPQUFPO0FBQ25CLFdBQU8sWUFBWSxHQUFHO0FBQUEsRUFDMUI7QUFFTyxXQUFTLE9BQU87QUFDbkIsV0FBTyxZQUFZLEdBQUc7QUFBQSxFQUMxQjtBQUVPLFdBQVMsY0FBYztBQUMxQixXQUFPLEtBQUssb0JBQW9CO0FBQUEsRUFDcEM7QUFHQSxTQUFPLFVBQVU7QUFBQSxJQUNiLEdBQUc7QUFBQSxJQUNILEdBQUc7QUFBQSxJQUNILEdBQUc7QUFBQSxJQUNILEdBQUc7QUFBQSxJQUNILEdBQUc7QUFBQSxJQUNILEdBQUc7QUFBQSxJQUNILEdBQUc7QUFBQSxJQUNIO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsRUFDSjtBQUdBLFNBQU8sUUFBUTtBQUFBLElBQ1g7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQSxPQUFPO0FBQUEsTUFDSCxzQkFBc0I7QUFBQSxNQUN0QiwyQkFBMkI7QUFBQSxNQUMzQixjQUFjO0FBQUEsTUFDZCxlQUFlO0FBQUEsTUFDZixpQkFBaUI7QUFBQSxNQUNqQixZQUFZO0FBQUEsTUFDWixzQkFBc0I7QUFBQSxNQUN0QixpQkFBaUI7QUFBQSxNQUNqQixjQUFjO0FBQUEsTUFDZCxpQkFBaUI7QUFBQSxNQUNqQixjQUFjO0FBQUEsTUFDZCx3QkFBd0I7QUFBQSxJQUM1QjtBQUFBLEVBQ0o7QUFHQSxNQUFJLE9BQU8sZUFBZTtBQUN0QixXQUFPLE1BQU0sWUFBWSxPQUFPLGFBQWE7QUFDN0MsV0FBTyxPQUFPLE1BQU07QUFBQSxFQUN4QjtBQUdBLE1BQUksT0FBUTtBQUNSLFdBQU8sT0FBTztBQUFBLEVBQ2xCO0FBRUEsTUFBSSxXQUFXLFNBQVMsR0FBRztBQUN2QixRQUFJLE1BQU0sT0FBTyxpQkFBaUIsRUFBRSxNQUFNLEVBQUUsaUJBQWlCLE9BQU8sTUFBTSxNQUFNLGVBQWU7QUFDL0YsUUFBSSxLQUFLO0FBQ0wsWUFBTSxJQUFJLEtBQUs7QUFBQSxJQUNuQjtBQUVBLFFBQUksUUFBUSxPQUFPLE1BQU0sTUFBTSxjQUFjO0FBQ3pDLGFBQU87QUFBQSxJQUNYO0FBRUEsUUFBSSxFQUFFLFlBQVksR0FBRztBQUVqQixhQUFPO0FBQUEsSUFDWDtBQUVBLFFBQUksRUFBRSxXQUFXLEdBQUc7QUFFaEIsYUFBTztBQUFBLElBQ1g7QUFFQSxXQUFPO0FBQUEsRUFDWDtBQUVBLFNBQU8sTUFBTSx1QkFBdUIsU0FBUyxVQUFVLE9BQU87QUFDMUQsV0FBTyxNQUFNLE1BQU0sa0JBQWtCO0FBQ3JDLFdBQU8sTUFBTSxNQUFNLGVBQWU7QUFBQSxFQUN0QztBQUVBLFNBQU8sTUFBTSx1QkFBdUIsU0FBUyxVQUFVLE9BQU87QUFDMUQsV0FBTyxNQUFNLE1BQU0sa0JBQWtCO0FBQ3JDLFdBQU8sTUFBTSxNQUFNLGVBQWU7QUFBQSxFQUN0QztBQUVBLFNBQU8saUJBQWlCLGFBQWEsQ0FBQyxNQUFNO0FBRXhDLFFBQUksT0FBTyxNQUFNLE1BQU0sWUFBWTtBQUMvQixhQUFPLFlBQVksWUFBWSxPQUFPLE1BQU0sTUFBTSxVQUFVO0FBQzVELFFBQUUsZUFBZTtBQUNqQjtBQUFBLElBQ0o7QUFFQSxRQUFJLFNBQVMsQ0FBQyxHQUFHO0FBQ2IsVUFBSSxPQUFPLE1BQU0sTUFBTSxzQkFBc0I7QUFFekMsWUFBSSxFQUFFLFVBQVUsRUFBRSxPQUFPLGVBQWUsRUFBRSxVQUFVLEVBQUUsT0FBTyxjQUFjO0FBQ3ZFO0FBQUEsUUFDSjtBQUFBLE1BQ0o7QUFDQSxVQUFJLE9BQU8sTUFBTSxNQUFNLHNCQUFzQjtBQUN6QyxlQUFPLE1BQU0sTUFBTSxhQUFhO0FBQUEsTUFDcEMsT0FBTztBQUNILFVBQUUsZUFBZTtBQUNqQixlQUFPLFlBQVksTUFBTTtBQUFBLE1BQzdCO0FBQ0E7QUFBQSxJQUNKLE9BQU87QUFDSCxhQUFPLE1BQU0sTUFBTSxhQUFhO0FBQUEsSUFDcEM7QUFBQSxFQUNKLENBQUM7QUFFRCxTQUFPLGlCQUFpQixXQUFXLE1BQU07QUFDckMsV0FBTyxNQUFNLE1BQU0sYUFBYTtBQUFBLEVBQ3BDLENBQUM7QUFFRCxXQUFTLFVBQVUsUUFBUTtBQUN2QixhQUFTLGdCQUFnQixNQUFNLFNBQVMsVUFBVSxPQUFPLE1BQU0sTUFBTTtBQUNyRSxXQUFPLE1BQU0sTUFBTSxhQUFhO0FBQUEsRUFDcEM7QUFFQSxTQUFPLGlCQUFpQixhQUFhLFNBQVMsR0FBRztBQUM3QyxRQUFJLE9BQU8sTUFBTSxNQUFNLFlBQVk7QUFDL0IsYUFBTyxNQUFNLE1BQU0sYUFBYTtBQUNoQyxVQUFJLGVBQWUsRUFBRSxZQUFZLFNBQVksRUFBRSxVQUFVLEVBQUU7QUFDM0QsVUFBSSxlQUFlLEdBQUc7QUFDbEIsZUFBTyxZQUFZLE1BQU07QUFDekI7QUFBQSxNQUNKO0FBQUEsSUFDSjtBQUNBLFFBQUksQ0FBQyxPQUFPLE1BQU0sTUFBTSxjQUFjO0FBQ2xDO0FBQUEsSUFDSjtBQUNBLFFBQUksT0FBTyxNQUFNLE1BQU0saUJBQWlCLE1BQU07QUFDMUMsYUFBTyxNQUFNLE1BQU0sZ0JBQWdCLFNBQVMsZ0JBQWdCLE1BQU07QUFBQSxJQUN0RTtBQUNBLFFBQUksT0FBTyxhQUFhLEVBQUUsVUFBVSxPQUFPLE1BQU0sTUFBTSxtQkFBbUIsT0FBTyxjQUFjLEVBQUUsVUFBVSxPQUFPLE1BQU0sTUFBTSxpQkFBaUI7QUFDM0ksZUFBUyxnQkFBZ0IsTUFBTSxTQUFTO0FBQUEsSUFDNUM7QUFDQSxRQUFJLGNBQWMsT0FBTyxhQUFhLEVBQUUsVUFBVSxPQUFPLE1BQU0sTUFBTTtBQUNyRSxRQUFJLGFBQWEsRUFBRSxVQUFVLE9BQU8sTUFBTSxNQUFNO0FBQ2hELFFBQUksWUFBWSxFQUFFLFVBQVUsT0FBTyxNQUFNLE1BQU07QUFDL0MsUUFBSSxlQUFlLE9BQU8sY0FBYyxFQUFFLFVBQVUsT0FBTyxNQUFNLE1BQU07QUFHdkUsUUFBSSxDQUFDLGNBQWMsQ0FBQyxlQUFlLENBQUMsYUFBYSxDQUFDLGdCQUFnQixPQUFPLE1BQU0sTUFBTSxlQUFlLFFBQVc7QUFDM0csZ0JBQVU7QUFBQSxJQUNkLFdBQVcsZUFBZTtBQUFjLGdCQUFVLFdBQVc7QUFBQSxhQUNwRCxjQUFjO0FBQWMsZ0JBQVUsV0FBVztBQUFBLGFBQ2pELGNBQWM7QUFBVyxnQkFBVSxXQUFXO0FBQUEsYUFDOUMsYUFBYTtBQUFhLGdCQUFVLFdBQVc7QUFBQSxhQUMvQztBQUFZLGdCQUFVLFVBQVU7QUFBQSxhQUNoQztBQUFXLGdCQUFVLFVBQVU7QUFBQSxhQUMvQjtBQUFjLGdCQUFVLFVBQVU7QUFBQSxhQUNsQztBQUFhLGdCQUFVLFVBQVU7QUFBQSxFQUU5QyxDQUFDO0FBR0QsU0FBTyxpQkFBaUIsZUFBZSxTQUFTLEdBQUc7QUFFL0MsUUFBSTtBQUFPO0FBRVgsUUFBSSxPQUFPLE1BQU0sTUFBTSwyQkFBMkI7QUFDOUMsUUFBRSxlQUFlO0FBQUEsSUFDckIsT0FBTztBQUNILE1BQVksMEJBQTBCLENBQUM7QUFBQSxJQUMzQztBQUFBLEVBQ0osQ0FBQztBQUVELFNBQU8sWUFBWSxlQUFlOyIsCiAgIm5hbWVzIjogWyJldmVudE5hbWUiXQp9Cg== diff --git a/v2/internal/frontend/runtime/runtime_prod_desktop.js b/v2/internal/frontend/runtime/runtime_prod_desktop.js index 9b2f39ff0..c285fa642 100644 --- a/v2/internal/frontend/runtime/runtime_prod_desktop.js +++ b/v2/internal/frontend/runtime/runtime_prod_desktop.js @@ -1 +1 @@ -(()=>{var P=Object.defineProperty;var c=(e,n)=>{for(var o in n)P(e,o,{get:n[o],enumerable:!0})};var x={};c(x,{LogDebug:()=>G,LogError:()=>F,LogFatal:()=>J,LogInfo:()=>H,LogLevel:()=>j,LogPrint:()=>B,LogTrace:()=>A,LogWarning:()=>U,SetLogLevel:()=>N});function f(e,n){window.WailsInvoke("L"+e+n)}function A(e){f("T",e)}function B(e){f("P",e)}function G(e){f("D",e)}function H(e){f("I",e)}function U(e){f("W",e)}function F(e){f("E",e)}function J(e){f("F",e)}function N(e){f("S",e)}var j={TRACE:1,DEBUG:2,INFO:3,WARNING:4,ERROR:5};var v=class{constructor(n,o,t){this.eventName=n,this.maxCallbacks=t||-1,this.Callback=i=>(o.apply(null,i),this.maxCallbacks===-1?!1:(this.maxCallbacks-=1,this.maxCallbacks===0))}},a={};function p(e,n,o){a[e]=a[e]||[];let t=new v(e,n,o);return a[e].push(t),()=>V(t)}function y(e,n){return p(e,n,-1)}function C(e,n){return p(e,n,1)}function D(e){let n=e.name;if(a[n]){let o=a[n].slice();for(let t=a[n].length-1;t>=0;t-=1){let i=a[n][t],r=e.data;i.Callback(r)&&o.splice(t,1)}o.length===0?g(n):a[n]=o}}function T(e){let n;try{n=JSON.parse(e)}catch{let t="Invalid JSON passed to Notify: "+e;throw new Error(t)}D(n)}function O(e){let n={name:e,data:[].slice.apply(arguments).slice(1)};D(n),window.WailsInvoke("EE"+JSON.stringify(n))}function g(e){delete a[e],window.WailsInvoke("EX"+e)}function L(e,...n){g(e),n.length>0&&n.forEach(o=>{g(o)})}function V(e){let n=e.eventName;a[n]=a[n].filter(o=>o!==e),a[n].length===0&&g(n)}var u={};function X(){var e=new Uint32Array(1);return window.crypto.getRandomValues(e)[0]}function Y(){return Math.random()*9007199254740991}var W;window.crypto?W=X:W=Y;function s(e,n,o){return o==null&&(o=0),new Promise(function(t,i){var r;do r=e+"-"+W();while(u[r]);var l;o>0&&(l=setTimeout(function(){i(Error("Call to "+e+" timed out. Request ID: "+r))},o)),u[r]={timeoutHandle:l,reject:i,resolve:t};try{let d={name:e,args:n,callbackID:r};window.WailsInvoke("C"+JSON.stringify(d))}catch(d){console.error(d)}})}window.ObfuscatedCall=(e,n,o)=>(o==null&&(o=0),new Promise(function(t,i){var r;do r=e+"-"+W();while(u[r]);var l;o>0&&(l=setTimeout(function(){i(Error("Call to method "+e+" timed out. Request ID: "+r))},o)),u[r]={timeoutHandle:l,reject:i,resolve:t};try{let d={id:e,args:n,callbackID:r};window.WailsInvoke("c"+JSON.stringify(d))}catch(d){console.error(d)}}));function z(e){let n;try{n=JSON.parse(e)}catch(i){let r=`Invalid JSON passed to callback: ${i.message}. Message: ${e}`;throw runtime.LogDebug(r),new Error(r)}let o=n.callbackid,t=u[o];if(!t){let i=`Callback '${o}' not registered!!!`;throw console.error(i),new Error(i)}clearTimeout(t.timeoutHandle),delete u[o],n.error?t.reject(n.error):t.resolve(n.result)}window.go={};function M(e){try{e=JSON.parse(e)}catch(n){console.error(n)}window.go=window.go||{},Object.keys(e).forEach(n=>{window.go[n]=window.go[n]||{},Object.keys(e[n]).forEach(o=>{window.go[n][o]=window.go[n][o]||{},Object.keys(e[n][o]).forEach(t=>{window.go[n][o][t]=function(){let i=0;function r(){let l=[].slice.call(arguments);return s([n,o,t].join("."),l,i)}return r.setTimeout=function(l){i=l},r.getTimeout=function(){return i},r}()})})})}var h={};c(h,{WindowCenter:()=>_,WindowFullscreen:()=>ne,WindowGetPosition:()=>de,WindowGetSize:()=>re,WindowHide:()=>fe,WindowIsFullscreen:()=>te,WindowIsMaximised:()=>We,WindowIsMinimised:()=>ve,WindowIsNormal:()=>he,WindowMaximise:()=>ce,WindowMinimise:()=>me,WindowReload:()=>$,WindowReloadApp:()=>q,WindowSetAlwaysOnTop:()=>ae,WindowSetBackgroundColour:()=>ke,WindowSetDarkTheme:()=>K,WindowSetLightTheme:()=>Z,WindowSetMaxSize:()=>se,WindowSetMinSize:()=>le,WindowSetPosition:()=>we,WindowSetSize:()=>ie,WindowSetSystemDefaultTheme:()=>Q,WindowSetTitle:()=>ee,WindowShow:()=>ue,WindowToggleMaximise:()=>ge,WindowUnfullscreen:()=>oe,WindowUnmaximise:()=>pe,WindowUnminimise:()=>xe});function $(){window.location.reload()}function q(){window.WailsInvoke("WR")}function Q(){window.WailsInvoke("WASDT")}function Z(){window.WailsInvoke("WALT")}function K(){window.WailsInvoke("WADT")}function _(){window.WailsInvoke("Wc")}function ee(e){window.WailsInvoke("WT"+e)}function ne(){window.WailsInvoke("WF")}function oe(){window.WailsInvoke("Wf")}function te(){return s(":wails:WindowIsFullscreen")}function ie(e,n){window.WailsInvoke("Ws:"+e+":"+n)}function re(){return s(":wails:WindowGetSize")}function se(e,n){window.WailsInvoke("WZ:"+e+":"+n)}function le(e,n){window.WailsInvoke("Wz:"+e+":"+n)}function ae(e){window.WailsInvoke("WATP:"+(e?"1":"0"))}function we(e,n){window.WailsInvoke("Wp:"+e+":"+n)}function de(){return s(":wails:WindowGetPos")}function fe(){window.WailsInvoke("WH")}function ue(){window.WailsInvoke("WS")}function ce(){window.WailsInvoke("WM")}function ge(){window.WailsInvoke("Wt")}function pe(){window.WailsInvoke("WU")}function We(){return s(":wails:WindowIsMaximised")}function me(){window.WailsInvoke("Wm")}function xe(){window.WailsInvoke("Wu")}function ve(){return s(":wails:WindowIsMinimised")}function he(){return s(":wails:WindowIsNormal")}function ke(e,n,o,t){let i=JSON.stringify({r:e||0,g:n||0,b:o||0,a:t||255});window.WailsInvoke("Wr:"+i)}var k={};c(k,{ScreenGetAll:()=>Ie});function Ie(){return s(":wails:ScreenGetAll")}var I={};c(I,{BrowserOpenURL:()=>be});function be(e){window.WailsInvoke("BO:"+e)}var b={};c(b,{ClipboardGetText:()=>Ee,ClipboardSetText:()=>Se});function Se(e){return s(":wails:ClipboardSetText",[e])}function Ee(){return s(":wails:ClipboardGetText")}function R(e){let n=e.target;switch(window.getComputedStyle(n).getPropertyValue("--default-contextmenu").trim()){case"show":return;case"hide":e.preventDefault();return;default:if(n.isContentEditable)return;let i=window.getSelection(),r=i.toString().length>0;if(r)for(let l=0;l{if(window.wails.flags.resizeEdge){window.WailsInvoke("resize:"+window.wails.flags.resizeEdge),e.preventDefault();return}if(Le(e)){if(window.wails.flags.disableScrollbarDrag&&(e.offsetX>e.target.clientWidth||e.offsetY>e.target.clientHeight))return;window.wails.flags.deferDragToMouseMove?window.wails.flags.shouldDrag=!0:(e.preventDefault(),window.WailsInvoke("drag"));return}else window.wails.flags.shouldDrag=!1});window.addEventListener("mouseup",()=>{window.wails.flags.shouldDrag=!1});function w(e){document.documentElement.style.cursor=e||window.wails.flags.defaultCursor,window.wails.flags.resizeEdge=e}window.addEventListener("mousemove",function(e){if(window.wails.flags.shouldDrag&&(window.wails.flags.shouldDrag=!1,(e.buttons!==void 0?e.buttons:e.which)>0)){window.WailsInvoke("drag");return}if(!window.wails.flags.enableResize)return;window.wails.flags.defaultCursor==null&&(window.wails.flags.defaultCursor=document.documentElement.style.cursor),window.outerWidth-e.clientX{var J=Object.defineProperty;var p=(e,t)=>{for(var n in t)J(e,n,{get:t[n],enumerable:!0})};var y={};p(y,{LogDebug:()=>q,LogError:()=>_,LogFatal:()=>Z,LogInfo:()=>Y,LogLevel:()=>ee,LogPrint:()=>$,LogTrace:()=>X,LogWarning:()=>Q,SetLogLevel:()=>K});function u(e,t){window.WailsInvoke("L"+e+t)}function X(e){u("T",e)}function $(e){u("P",e)}function q(e){u("D",e)}function Y(e){u("I",e)}function Q(e){u("W",e)}function _(e){u("E",e)}function Z(e){u("F",e)}function K(e){u("S",e)}var ee={TRACE:1,DEBUG:2,INFO:3,WARNING:4,ERROR:5};var b=class{constructor(t,n,i){this.eventName=t,this.maxCallbacks=i||-1,this.Callback=o=>(n.apply(null,o),this.maxCallbacks===-1?!1:(this.maxCallbacks-=1,this.maxCallbacks===0))}},f={};function v(e,t,n){f[e]=f[e]||[];let i=new b(e,t,n);return f[e].push(i),()=>te(i)}function x(e,t){return v(e,t,-1)}function N(e,t){return v(e,t,1)}function L(e){let t=e.name,n=f[t]?.slice()||[];if(n.length){for(let i=n.length-1;i>=0;i-=1){let o=n[i],s=e.data;o.Callback(s)&&n.splice(i,1)}n.length===0?g(t):f[t]=n}}function P(e){let t;try{t=JSON.parse(e)}catch{let i="Invalid JSON passed to Notify: "+e;throw new Error(i)}L(t)}function z(e){let t={name:e,data:[].slice.apply(arguments).slice(1)};L(t),window.WailsInvoke("EE"+JSON.stringify(t))}function g(e){delete f[e],window.WailsInvoke("EX"+e)}function D(e,...t){g(e),t.length>0&&t.forEach(n=>{g(n)})}function F(){Object.keys(f).forEach(t=>{g(t)})}function te(e){let t=e.eventName;f[t]!==void 0&&(f[t]=f[t].filter(n=>n!==e),f[t].length===0&&g(t))}var c={};function ne(){var e=new Uint32Array(1);return window.crypto.getRandomValues(e)[0]}function ie(){return Math.random()*9007199254740991}var W;window.crypto?W=ne:W=ie;function r(e,t,n){return n==null&&(n=0),new Promise(function(i,o){var s;do s=e+"-"+W();while(c[s]);var l;n>0&&(l=setTimeout(function(){o(Error("Call to "+e+" timed out. Request ID: "+s))},n)),c[s]={timeoutHandle:l,reject:o,resolve:i};try{let w={name:e,args:t,callbackID:s};window.WailsInvoke("C"+JSON.stringify(w))}catch(w){console.error(w)}})}window.ObfuscatedCall=(e,t,n)=>(n==null&&(n=0),new Promise(function(i,o){var s;do s=e+"-"+W();while(c[s]);var l;n>0&&(l=setTimeout(function(){o(Error("Call to method "+e+" timed out. Request ID: "+s))},n)),c[s]={timeoutHandle:l,reject:o,resolve:i};try{let w={id:e,args:t,callbackID:s};window.WailsInvoke("c"+JSON.stringify(w))}catch(w){console.error(w)}}));function M(e){let t;try{t=JSON.parse(e)}catch(o){let s=`Invalid JSON passed to callback: ${o.message}. Message: ${e}`;throw runtime.LogDebug(s),new Error(s)}let n=t.callbackid,i=c[n];if(!i){let o=`Callback '${n}' not registered!!!`;throw console.error(o),new Error(o)}clearTimeout(i.timeoutHandle),delete c[n],t.error?i.reject(t.error):i.resolve(t.result)}window.go={};function B(e){try{e=JSON.parse(e)}catch(t){console.error(t)}window.go=window.go||{},Object.keys(e).forEach(t=>{window.go[t]=window.go[t]||{},Object.keys(e[t]).forEach(n=>{window.go[t][n]=window.go[t][n]||{},Object.keys(e[t][n]).forEach(i=>{window.go[t][n][i]=function(){let o=0;function s(){let l=[].slice.call(arguments);return r([t,n,i].join("."),l,o)}return s.setTimeout=function(l){o=l},s.getTimeout=function(){return o},s}()})})})}var C={};p(C,{WindowCenter:()=>fe,WindowFullscreen:()=>de,WindowGetPosition:()=>We,WindowGetSize:()=>ge,WindowHide:()=>he,WindowIsFullscreen:()=>ce,WindowIsMaximised:()=>Se,WindowIsMinimised:()=>Ie,WindowIsNormal:()=>Ae,WindowMaximise:()=>ye,WindowMinimise:()=>Te,WindowReload:()=>oe,WindowReloadApp:()=>re,WindowSetAlwaysOnTop:()=>xe,WindowSetBackgroundColour:()=>Oe,WindowSetDarkTheme:()=>le,WindowSetLightTheme:()=>ae,WindowSetMaxSize:()=>me,WindowSetMinSize:()=>ve,WindowSetPosition:()=>De,WindowSetSize:()=>pe,WindowSetSystemDefaultTheme:()=>se,WindowSetTitle:()=>we,WindowShow:()=>Ee,WindowToggleMaximise:()=>be,WindowUnfullscreen:()=>ue,WindowUnmaximise:()=>Ce,WindowUnminimise:()=>ke});function oe(){window.location.reload()}function re(){window.WailsInvoke("WR")}function se(){window.WailsInvoke("WASDT")}function ae(){window.WailsInvoke("WALT")}function le(){window.WailsInvoke("WADT")}function fe(){window.WailsInvoke("Wc")}function we(e){window.WailsInvoke("WT"+e)}function de(){window.WailsInvoke("WF")}function ue(){window.WailsInvoke("Wf")}function ce(){return r(":wails:WindowIsFullscreen")}function pe(e,t){window.WailsInvoke("Ws:"+e+":"+t)}function ge(){return r(":wails:WindowGetSize")}function me(e,t){window.WailsInvoke("WZ:"+e+":"+t)}function ve(e,t){window.WailsInvoke("Wz:"+e+":"+t)}function xe(e){window.WailsInvoke("WATP:"+(e?"1":"0"))}function De(e,t){window.WailsInvoke("Wp:"+e+":"+t)}function We(){return r(":wails:WindowGetPos")}function he(){window.WailsInvoke("WH")}function Ee(){window.WailsInvoke("WS")}function ye(){window.WailsInvoke("WM")}function be(){window.WailsInvoke("Wt")}function Ce(){window.WailsInvoke("WU")}function Se(){return r(":wails:WindowIsMaximised")}function Te(){window.WailsInvoke("Wm")}function ke(){window.WailsInvoke("Wu")}function Ie(){return r(":wails:WindowIsMinimised")}function Ae(){return r(":wails:WindowIsNormal")}function Oe(e,t,n,i){let o=JSON.stringify({r:e||0,g:t||0,b:n||0,a:i||255});window.WailsInvoke("Wr:"+o)}var S={};p(S,{ScreenGetAll:()=>Re});function Re(){return r(":wails:ScreenGetAll")}var T={};p(T,{BrowserOpenURL:()=>Ne});function Ne(e){window.WailsInvoke("BO:"+e)}var k={};p(k,{ClipboardGetText:()=>Pe,ClipboardSetText:()=>Le});function Le(e){return r(":wails:ClipboardSetText",[e])}function Pe(){return r(":wails:ClipboardGetText")}var I={};p(I,{CanResolveFilePaths:()=>V,OnFileDrop:()=>Fe,OnFileDropOff:()=>Me,ResolveFilePaths:()=>ze});var a={registered:!1,defaultUseDropTarget:!0,useDropTarget:!0,nextDeactivate:null,nextDeactivateTimeout:null},m="wails-drop-target-active";function h(e){let t=e.getPropertyValue(window.wails.flags.cssDropProperty).trim();return t?t===window.wails.flags.cssDropValue:!1}function G(e){if(!e.dataTransfer.types.includes("Files")||(e.preventDefault(),e.dataTransfer.dropEffect="copy",!window.wails.flags.enableWailsDragAndDrop)||!a.useDropTarget)return;let n=e.target;if(a.nextDeactivate&&a.nextDeactivate(),!n||!h(getComputedStyle(n)))return;let i=n;for(;i;)h(getComputedStyle(i))&&i.classList.add(m),i=i.parentElement}function H(e){if(!!e.dataTransfer.types.includes("Files")&&(e.preventDefault(),!!window.wails.flags.enableWailsDragAndDrop&&!!a.useDropTarget)){if(!e.target||!h(getComputedStyle(e.target)))return null;a.nextDeactivate&&a.nextDeactivate(),a.nextDeactivate=()=>{Array.from(document.getElementsByClassName(m)).forEach(n=>n.classList.remove(m)),a.nextDeactivate=null,a.nextDeactivateTimeout&&(clearTimeout(a.nextDeactivateTimeout),a.nextDeactivateTimeout=null)},a.nextDeactivateTimeout=setTimeout(()=>{a.nextDeactivate&&a.nextDeactivate()},50)}}function U(e){if(!!e.dataTransfer.types.includes("Files")&&(e.preventDefault(),!!window.wails.flags.enableWailsDragAndDrop)){if(V()){let n=[];e.dataTransfer.items?n=[...e.dataTransfer.items].map((i,o)=>{if(i.kind==="file")return i.getAsFile()}):n=[...e.dataTransfer.files],window.runtime.ResolveFilePaths(e.x,e.y,n)}!a.useDropTarget||(a.nextDeactivate&&a.nextDeactivate(),Array.from(document.getElementsByClassName(m)).forEach(n=>n.classList.remove(m)))}}function V(){return window.chrome?.webview?.postMessageWithAdditionalObjects!=null}function ze(e,t,n){window.chrome?.webview?.postMessageWithAdditionalObjects&&chrome.webview.postMessageWithAdditionalObjects(`file:drop:${e}:${t}`,n)}function Fe(e,t){if(typeof e!="function"){console.error("DragAndDropCallback is not a function");return}if(a.registered)return;a.registered=!0;let n=typeof t;a.useDropTarget=n==="undefined"||n!=="boolean"?a.defaultUseDropTarget:t,window.addEventListener("dragover",G),window.addEventListener("dragleave",H),window.addEventListener("drop",U);let i=e;a.useDropTarget&&(i=function(o,s,l){let w=document.elementFromPoint(o,s);if(!w||!h(getComputedStyle(w)))return null;e(o,s,l)}),x("wails:file-drop",i)}function Me(){window.removeEventListener("dragover",G),window.removeEventListener("dragleave",H),window.removeEventListener("drop",U),D("wails:file-drop"),a.registered=!1}function j(e){let t=e.target;switch(window.getComputedStyle(t).getPropertyValue("--default-contextmenu").trim()){case"show":return;case"hide":e.preventDefault();return;default:if(t.isContentEditable)return;let o=window.getSelection(),s=o.toString().length>0;if(s)for(let l=0;lje,CleanupNotifications:()=>He,InitializeNotifications:()=>Ge,IsNotificationAvailable:()=>Ue,RegisterNotificationCategory:()=>$e,RemoveAllDeliveredNotifications:()=>_e,RemoveAllPendingNotifications:()=>Ye,RemoveDeliveredNotification:()=>Ze,RemoveNotification:()=>Ke,RemoveNotificationCategory:()=>qe,RemovePendingNotification:()=>Qe,RequestNotificationAuthorization:()=>Ve,SendNotification:()=>Je,SendNotificationWithActions:()=>Xe});function Ge(){return r(":wails:InitializeNotifications")}function He(){return r(":wails:CleanupNotifications")}function Ue(){return r(":wails:IsNotificationAvailable")}function Ve(){return r(":wails:RequestNotificationAuthorization")}function je(){return r(":wails:CheckNotificationAuthorization")}function Je(e){return r(":wails:SendNotification",[e])}function Xe(e){return r(":wails:SendNotificationWithActions",[e])}function $e(e){return r(":wails:RegisterNotificationCategory",[e])}function qe(e){return r(":wails:RemoveNotificationCategory",[e])}function Ye(){return r(":wails:RemoveAllPendingNotifications")}function Qe(e){return r(":wails:RemovePendingNotification",[e])}function _e(){return r(":wails:RemoveAllDeliveredNotifications")}function Ze(e){return r(":wails:RemoveDeliveredNotification",[e])}function Ke(e){return r(":wails:RemoveNotification",[e])}function et(){window.WailsInvoke("Q")}function tt(){window.WailsInvoke("S")}function nt(){window.WailsInvoke("H")}function it(){return r(":wails:Environment")}window.runtime={...y,...C,...T,...S,...k,...I,...A,EventsOn:x,EventsOnce:N,EventsOnMultiple:v,EventsEmit:z,EventsOff:D,EventsOffAll:F,Environment:it,Show:tt,Hide:nt,Quit:et};window.wails={Callback:M,EventsNotify:P,SetBindings:B,eventListeners:f,callbacks:c,flags:{disableScrollbarDrag:!1,disableDefaultContextMenu:!1,enableResize:!1,defaultCursor:null,borderThickness:6,shouldDrag:!1,deferDragToMouseMove:!0,cssDragProperty:"--wails-draggable",cssDragValue:"drag",cssDropProperty:"--wails-drop-target",cssDropValue:"drop",enableWailsDragAndDrop:!1}};window.wailsbindings&&(window.wails.SetBindings(window.wailsbindings),delete window.wails.SetBindings);delete window.wailsbindings;var ot=function(e){var t=window.getComputedStyle(e.target).getPropertyValue(window.wails.flags.cssDragProperty);return t&&(t=t.trim()),!(t!==window.wails.flags.cssDragValue||e.buttons!==1||e.detail!==1)};window.wails.setCSSDragProperties=function(e,t){window.wails.flags.cssDragProperty=e,window.wails.flags.cssDragValue=t};window.wails.setCSSDropProperties=function(e,t){window.wails.flags.cssDropProperty=e,window.wails.flags.cssDropValue=t};window.addEventListener("mousedown",e=>{if(window.wails.flags.resizeEdge){window.WailsInvoke("resize:"+window.wails.flags.resizeEdge),e.preventDefault();return}if(ot(e)){if(window.wails.flags.disableScrollbarDrag&&(e.offsetX>e.target.clientWidth||e.offsetY>e.target.clientHeight))return;window.wails.flags.deferDragToMouseMove?window.wails.flags.shouldDrag=!0:(e.preventDefault(),window.WailsInvoke("drag"));return}else window.wails.flags.shouldDrag=!1});window.addEventListener("mouseup",()=>{window.wails.flags.shouldDrag=!1});function d(e){document.documentElement.style.cursor=e||window.wails.flags.defaultCursor,window.wails.flags.resizeEdge=e}window.addEventListener("mousemove",function(e){if(window.wails.flags.shouldDrag&&(window.wails.flags.shouldDrag=!1,(e.buttons!==void 0?e.buttons:e.which)>0)){window.WailsInvoke("drag");return}if(!window.wails.flags.enableResize)return;window.wails.flags.defaultCursor==null&&(window.wails.flags.defaultCursor=document.documentElement.style.cursor),window.outerWidth-e.clientX; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. @@ -233,3 +233,98 @@ export function ClipboardGetText(): Promise; // [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) // Sets a text on the clipboard export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void + +// Notification types +export interface NotificationOptions { + id: string; + title: string; + subtitle?: string; // macOS and Linux only + body?: string; + categoryId?: string; + data?: { [key: string]: any }; +} + +export interface NotificationAction { + id?: string; + title?: string; + destructive?: boolean; // macOS-specific +} + +export interface NotificationCategory { + id?: string; + actions?: NotificationAction[]; + hasReplyField?: boolean; + replyPlaceholder?: string; + replyButtonTitle?: string; +} + +// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications) +// Initializes the notification service for the application. +// This must be called before sending any notifications. +export function InitializeNotifications(): Promise; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise; + +// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions) +// Sends a notification with action buttons. Requires a registered category. +export function SendNotificationWithActions(options: NotificationOptions): Promise; + +// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory) +// Registers a notification category that can be used with SendNotificationWithActions. +export function RegisterNotificationCategory(category: NotificationCategory): Promise; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise; + +// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification) +// Removes a notification by its identifier (cross-platform convenience function). +export function RemoveNotification(identifier: string): Promise; \ No newline at end of file diff --git a/v2/internal/frontend/runtime/wrapper/runtime.js b/v2/internal/frontend/runtime/wrapper/runtime.js index bd4f371ae..556621eeb 100644 --- a/v2/internal/frontend/runtime/wrapper/runtime.js +++ b/v2/internal/frontend/runtime/wrapper/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName, ...additionalEventNames) { return window.runtime.EventsOff(eventName, ...additionalEventNames); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { return EventsOnMultiple(eventName, callback, 1); } @@ -199,4 +203,96 @@ export function ClipboardGetText() { export function ClipboardSetText(text) { return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} + +export function InitializeNotifications() { + return window.runtime.InitializeNotifications(); +} + +export function CleanupNotifications() { + return window.runtime.CleanupNotifications(); +} + +export function IsNotificationAvailable() { + return window.runtime.IsNotificationAvailable(); +} + +export function RequestNotificationAuthorization() { + return window.runtime.RequestNotificationAuthorization(); +} + +export function CheckNotificationAuthorization() { + return window.runtime.CheckNotificationAuthorization(); +} + +export function SendNotification(options) { + return window.runtime.SendNotification(options); +} + +export function SendNotificationWithActions(options) { + return window.runtime.SendNotificationWithActions(options); +} + +export function RegisterNotificationCategory(category) { + return window.runtime.RegisterNotificationCategory(category); +} + +export function RemoveNotificationCategory(categoryId) { + return window.runtime.RemoveNotificationCategory(categoryId); +} + +export function RemoveAllPendingNotifications() { + return window.runtime.RemoveAllPendingNotifications(); +} + +export function RemovePendingNotification(identifier) { + return window.runtime.RemovePendingNotification(identifier); +} + +export function RemoveAllDeliveredNotifications() { + return window.runtime.RemoveAllDeliveredNotifications(); +} + +export function RemoveDeliveredNotification(identifier) { + return window.runtime.RemoveDeliveredNotification(identifier); +} + +export function RemoveNotification(identifier) { + return window.runtime.RemoveNotification(identifier); } \ No newline at end of file diff --git a/v2/internal/frontend/utils/urlValidator.go b/v2/internal/frontend/utils/urlValidator.go new file mode 100644 index 000000000..76ba216ce --- /dev/null +++ b/v2/internal/frontend/utils/urlValidator.go @@ -0,0 +1,58 @@ +package utils + +import ( + "errors" + "fmt" + "net/url" + "regexp" + "strings" +) + +func ValidateAndSanitizeURL(rawURL string) (string, error) { + // Check for null bytes (can cause truncation issues in some systems) + if strings.Contains(rawURL, "\x00") { + return "", errors.New("null bytes not allowed in URL") + } + + // Parse URL first - this handles most malformed URLs + parsedURL, err := url.Parse(rawURL) + if err != nil { + return "", fmt.Errorf("invalid URL format: %v", err) + } + + scheme := strings.ToLower(parsedURL.Scheme) + + if scheme == "javascript" || scheme == "data" || scheme == "file" || scheme == "ftp" || scheme == "" { + return "", errors.New("scheme not allowed") + } + + // Ensure there's actually a host for http/https URLs + if (scheme == "http" || scheme == "https") && parsedURL.Host == "" { + return "", fmt.Errorf("missing host for %s URL", scheme) + } + + sanitizedURL := parsedURL.String() + + // Check for control characters that might cause issues + // (but allow legitimate URL characters like &, ;, etc.) + for i, r := range sanitizedURL { + // Block control characters except tab, but allow other printable chars + if r < 32 && r != 9 { // 9 is tab, which might be legitimate + return "", fmt.Errorf("control character at position %d not allowed", i) + } + } + + // Shell metacharacter check + shellDangerous := `[;\|` + "`" + `$\\<>*{}\[\]()~! \t\n\r]` + if matched, _ := regexp.MatchString(shellDangerous, sanitizedURL); matched { + return "", errors.New("shell metacharacters not allowed") + } + + // Unicode danger check + unicodeDangerous := "[\u0000-\u001F\u007F\u00A0\u1680\u2000-\u200F\u2028-\u202F\u205F\u2060\u3000\uFEFF]" + if matched, _ := regexp.MatchString(unicodeDangerous, sanitizedURL); matched { + return "", errors.New("unicode dangerous characters not allowed") + } + + return sanitizedURL, nil +} diff --git a/v2/internal/frontend/utils/urlValidator_test.go b/v2/internal/frontend/utils/urlValidator_test.go new file mode 100644 index 000000000..b385ccec1 --- /dev/null +++ b/v2/internal/frontend/utils/urlValidator_test.go @@ -0,0 +1,207 @@ +package utils_test + +import ( + "strings" + "testing" + + "github.com/wailsapp/wails/v2/internal/frontend/utils" +) + +// Test cases for ValidateAndOpenURL +func TestValidateURL(t *testing.T) { + testCases := []struct { + name string + url string + shouldErr bool + errMsg string + expected string + }{ + // Valid URLs + { + name: "valid https URL", + url: "https://www.example.com", + shouldErr: false, + expected: "https://www.example.com", + }, + { + name: "valid http URL", + url: "http://example.com", + shouldErr: false, + expected: "http://example.com", + }, + { + name: "URL with query parameters", + url: "https://example.com/search?q=cats&dogs", + shouldErr: false, + expected: "https://example.com/search?q=cats&dogs", + }, + { + name: "URL with port", + url: "https://example.com:8080/path", + shouldErr: false, + expected: "https://example.com:8080/path", + }, + { + name: "URL with fragment", + url: "https://example.com/page#section", + shouldErr: false, + expected: "https://example.com/page#section", + }, + { + name: "urlencode params", + url: "http://google.com/ ----browser-subprocess-path=C:\\\\Users\\\\Public\\\\test.bat", + shouldErr: false, + expected: "http://google.com/%20----browser-subprocess-path=C:%5C%5CUsers%5C%5CPublic%5C%5Ctest.bat", + }, + + // Invalid schemes + { + name: "javascript scheme", + url: "javascript:alert('xss')", + shouldErr: true, + errMsg: "scheme not allowed", + }, + { + name: "data scheme", + url: "data:text/html,", + shouldErr: true, + errMsg: "scheme not allowed", + }, + { + name: "file scheme", + url: "file:///etc/passwd", + shouldErr: true, + errMsg: "scheme not allowed", + }, + { + name: "ftp scheme", + url: "ftp://files.example.com/file.txt", + shouldErr: true, + errMsg: "scheme not allowed", + }, + + // Malformed URLs + { + name: "not a URL", + url: "not-a-url", + shouldErr: true, + errMsg: "scheme not allowed", // will have empty scheme + }, + { + name: "missing scheme", + url: "example.com", + shouldErr: true, + errMsg: "scheme not allowed", + }, + { + name: "malformed URL", + url: "https://", + shouldErr: true, + errMsg: "missing host", + }, + { + name: "empty host", + url: "http:///path", + shouldErr: true, + errMsg: "missing host", + }, + + // Security issues + { + name: "null byte in URL", + url: "https://example.com\x00/hidden", + shouldErr: true, + errMsg: "null bytes not allowed", + }, + { + name: "control characters", + url: "https://example.com\n/path", + shouldErr: true, + errMsg: "control character", + }, + { + name: "carriage return", + url: "https://example.com\r/path", + shouldErr: true, + errMsg: "control character", + }, + { + name: "URL with tab character", + url: "https://example.com/path?q=hello\tworld", + shouldErr: true, + errMsg: "control character", + }, + { + name: "URL with path parameters", + url: "https://example.com/path;param=value", + shouldErr: true, + errMsg: "shell metacharacters not allowed", + }, + { + name: "URL with special characters in query", + url: "https://example.com/search?q=hello world&filter=price>100", + shouldErr: true, + errMsg: "shell metacharacters not allowed", + }, + { + name: "URL with special characters in query and params", + url: "https://example.com/search?q=hello ----browser-subprocess-path=C:\\\\Users\\\\Public\\\\test.bat", + shouldErr: true, + errMsg: "shell metacharacters not allowed", + }, + { + name: "URL with dollar sign in query", + url: "https://example.com/search?price=$100", + shouldErr: true, + errMsg: "shell metacharacters not allowed", + }, + { + name: "URL with parentheses", + url: "https://example.com/file(1).html", + shouldErr: true, + errMsg: "shell metacharacters not allowed", + }, + { + name: "URL with unicode", + url: "https://example.com/search?q=hello\u2001foo", + shouldErr: true, + errMsg: "unicode dangerous characters not allowed", + }, + + // Edge cases + { + name: "international domain", + url: "https://例え.テスト/path", + shouldErr: false, + expected: "https://%E4%BE%8B%E3%81%88.%E3%83%86%E3%82%B9%E3%83%88/path", + }, + { + name: "URL with pipe character", + url: "https://example.com/user/123|admin", + shouldErr: false, + expected: "https://example.com/user/123%7Cadmin", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // We'll test only the validation part to avoid actually opening URLs + sanitized, err := utils.ValidateAndSanitizeURL(tc.url) + + if tc.shouldErr { + if err == nil { + t.Errorf("expected error for URL %q, but got none", tc.url) + } else if tc.errMsg != "" && !strings.Contains(err.Error(), tc.errMsg) { + t.Errorf("expected error containing %q, got %q", tc.errMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("expected no error for URL %q, but got: %v", tc.url, err) + } + if sanitized != tc.expected { + t.Errorf("unexpected sanitized URL for %q: expected %q, got %q", tc.url, tc.expected, sanitized) + } + } + }) + } +} diff --git a/v2/internal/fs/fs.go b/v2/internal/fs/fs.go index 5bfa71b1c..5662c020c 100644 --- a/v2/internal/fs/fs.go +++ b/v2/internal/fs/fs.go @@ -2,6 +2,7 @@ package fs import ( "crypto/md5" + "encoding/hex" "fmt" "io" "io/fs" @@ -27,14 +28,14 @@ func RelativeToCwd(relativePath string) (string, error) { // Mkdir will create the given directory func Mkdir(dirname string) error { - return os.Mkdir(dirname, 0755) + return os.Mkdir(dirname, 0o755) } // MkDirs creates the given nested directories. // Returns error on failure func MkDirs(fullPath string, mode ...os.FileMode) error { var perms os.FileMode - perms = 0755 + perms = 0o755 if len(mode) == 1 { perms = mode[0] } @@ -111,7 +112,7 @@ func RelativePath(relativepath string, optionalpaths ...string) string { // I'm allowing this for 1 reason only: It's fatal if the path // supplied is wrong as it's only used internally in Wails. If we get // that path wrong, we should know about it immediately. The other reason is - // that it cuts down a ton of unnecassary error handling. + // that it cuts down a ton of unnecessary error handling. panic(err) } return result @@ -141,7 +142,7 @@ func MD5File(filename string) (string, error) { return "", err } - return fmt.Sprintf("%x", h.Sum(nil)), nil + return hex.EncodeToString(h.Sum(nil)), nil } // MustMD5File will call MD5File and abort the program on error @@ -157,7 +158,7 @@ func MustMD5File(filename string) string { // MustWriteString will attempt to write the given data to the given filename // It will abort the program in the event of a failure func MustWriteString(filename string, data string) { - err := os.WriteFile(filename, []byte(data), 0755) + err := os.WriteFile(filename, []byte(data), 0o755) if err != nil { fatal("Unable to write file", filename, ":", err.Error()) os.Exit(1) @@ -194,7 +195,6 @@ func GetSubdirectories(rootDir string) (*slicer.StringSlicer, error) { } func DirIsEmpty(dir string) (bool, error) { - // CREDIT: https://stackoverflow.com/a/30708914/8325411 f, err := os.Open(dir) if err != nil { @@ -284,7 +284,6 @@ func SetPermissions(dir string, perm os.FileMode) error { // Symlinks are ignored and skipped. // Credit: https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04 func CopyDirExtended(src string, dst string, ignore []string) (err error) { - ignoreList := slicer.String(ignore) src = filepath.Clean(src) dst = filepath.Clean(dst) @@ -382,7 +381,6 @@ func FindPathToFile(fsys fs.FS, file string) (string, error) { // FindFileInParents searches for a file in the current directory and all parent directories. // Returns the absolute path to the file if found, otherwise an empty string func FindFileInParents(path string, filename string) string { - // Check for bad paths if _, err := os.Stat(path); err != nil { return "" diff --git a/v2/internal/github/github.go b/v2/internal/github/github.go index db3b70c58..2aa5e1432 100644 --- a/v2/internal/github/github.go +++ b/v2/internal/github/github.go @@ -3,13 +3,15 @@ package github import ( "encoding/json" "fmt" - "github.com/charmbracelet/glamour" + "github.com/charmbracelet/glamour/styles" "io" "net/http" "net/url" "runtime" "sort" "strings" + + "github.com/charmbracelet/glamour" ) func GetReleaseNotes(tagVersion string, noColour bool) string { @@ -38,7 +40,7 @@ func GetReleaseNotes(tagVersion string, noColour bool) string { var termRendererOpts []glamour.TermRendererOption if runtime.GOOS == "windows" || noColour { - termRendererOpts = append(termRendererOpts, glamour.WithStyles(glamour.NoTTYStyleConfig)) + termRendererOpts = append(termRendererOpts, glamour.WithStyles(styles.NoTTYStyleConfig)) } else { termRendererOpts = append(termRendererOpts, glamour.WithAutoStyle()) } @@ -57,7 +59,6 @@ func GetReleaseNotes(tagVersion string, noColour bool) string { // GetVersionTags gets the list of tags on the Wails repo // It returns a list of sorted tags in descending order func GetVersionTags() ([]*SemanticVersion, error) { - result := []*SemanticVersion{} var err error @@ -97,7 +98,6 @@ func GetVersionTags() ([]*SemanticVersion, error) { // GetLatestStableRelease gets the latest stable release on GitHub func GetLatestStableRelease() (result *SemanticVersion, err error) { - tags, err := GetVersionTags() if err != nil { return nil, err @@ -114,7 +114,6 @@ func GetLatestStableRelease() (result *SemanticVersion, err error) { // GetLatestPreRelease gets the latest prerelease on GitHub func GetLatestPreRelease() (result *SemanticVersion, err error) { - tags, err := GetVersionTags() if err != nil { return nil, err diff --git a/v2/internal/go-common-file-dialog/cfd/DialogConfig.go b/v2/internal/go-common-file-dialog/cfd/DialogConfig.go index 221dbef27..9e06fb503 100644 --- a/v2/internal/go-common-file-dialog/cfd/DialogConfig.go +++ b/v2/internal/go-common-file-dialog/cfd/DialogConfig.go @@ -2,6 +2,12 @@ package cfd +import ( + "fmt" + "os" + "reflect" +) + type FileFilter struct { // The display name of the filter (That is shown to the user) DisplayName string @@ -9,6 +15,9 @@ type FileFilter struct { Pattern string } +// Never obfuscate the FileFilter type. +var _ = reflect.TypeOf(FileFilter{}) + type DialogConfig struct { // The title of the dialog Title string @@ -67,6 +76,10 @@ func (config *DialogConfig) apply(dialog Dialog) (err error) { } if config.Folder != "" { + _, err = os.Stat(config.Folder) + if err != nil { + return + } err = dialog.SetFolder(config.Folder) if err != nil { return @@ -74,6 +87,10 @@ func (config *DialogConfig) apply(dialog Dialog) (err error) { } if config.DefaultFolder != "" { + _, err = os.Stat(config.DefaultFolder) + if err != nil { + return + } err = dialog.SetDefaultFolder(config.DefaultFolder) if err != nil { return @@ -102,6 +119,10 @@ func (config *DialogConfig) apply(dialog Dialog) (err error) { } if config.SelectedFileFilterIndex != 0 { + if config.SelectedFileFilterIndex > uint(len(fileFilters)) { + err = fmt.Errorf("selected file filter index out of range") + return + } err = dialog.SetSelectedFileFilterIndex(config.SelectedFileFilterIndex) if err != nil { return diff --git a/v2/internal/go-common-file-dialog/cfd/errors.go b/v2/internal/go-common-file-dialog/cfd/errors.go index c097c8eb2..4ca3300b9 100644 --- a/v2/internal/go-common-file-dialog/cfd/errors.go +++ b/v2/internal/go-common-file-dialog/cfd/errors.go @@ -3,5 +3,7 @@ package cfd import "errors" var ( - ErrorCancelled = errors.New("cancelled by user") + ErrCancelled = errors.New("cancelled by user") + ErrInvalidGUID = errors.New("guid cannot be nil") + ErrEmptyFilters = errors.New("must specify at least one filter") ) diff --git a/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go b/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go index 8c82cda2c..b1be23fcf 100644 --- a/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go +++ b/v2/internal/go-common-file-dialog/cfd/iFileOpenDialog.go @@ -5,7 +5,7 @@ package cfd import ( "github.com/go-ole/go-ole" - "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/util" + "github.com/google/uuid" "syscall" "unsafe" ) @@ -106,7 +106,7 @@ func (fileOpenDialog *iFileOpenDialog) SetFileFilters(filter []FileFilter) error } func (fileOpenDialog *iFileOpenDialog) SetRole(role string) error { - return fileOpenDialog.vtbl.setClientGuid(unsafe.Pointer(fileOpenDialog), util.StringToUUID(role)) + return fileOpenDialog.vtbl.setClientGuid(unsafe.Pointer(fileOpenDialog), StringToUUID(role)) } // This should only be callable when the user asks for a multi select because @@ -164,8 +164,7 @@ func (fileOpenDialog *iFileOpenDialog) setIsMultiselect(isMultiselect bool) erro func (vtbl *iFileOpenDialogVtbl) getResults(objPtr unsafe.Pointer) (*iShellItemArray, error) { var shellItemArray *iShellItemArray - ret, _, _ := syscall.Syscall(vtbl.GetResults, - 1, + ret, _, _ := syscall.SyscallN(vtbl.GetResults, uintptr(objPtr), uintptr(unsafe.Pointer(&shellItemArray)), 0) @@ -178,7 +177,7 @@ func (vtbl *iFileOpenDialogVtbl) getResultsStrings(objPtr unsafe.Pointer) ([]str return nil, err } if shellItemArray == nil { - return nil, ErrorCancelled + return nil, ErrCancelled } defer shellItemArray.vtbl.release(unsafe.Pointer(shellItemArray)) count, err := shellItemArray.vtbl.getCount(unsafe.Pointer(shellItemArray)) @@ -195,3 +194,7 @@ func (vtbl *iFileOpenDialogVtbl) getResultsStrings(objPtr unsafe.Pointer) ([]str } return results, nil } + +func StringToUUID(str string) *ole.GUID { + return ole.NewGUID(uuid.NewSHA1(uuid.Nil, []byte(str)).String()) +} diff --git a/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go b/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go index 3effeda25..ddee7b246 100644 --- a/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go +++ b/v2/internal/go-common-file-dialog/cfd/iFileSaveDialog.go @@ -5,7 +5,6 @@ package cfd import ( "github.com/go-ole/go-ole" - "github.com/wailsapp/wails/v2/internal/go-common-file-dialog/util" "unsafe" ) @@ -77,7 +76,7 @@ func (fileSaveDialog *iFileSaveDialog) SetFileFilters(filter []FileFilter) error } func (fileSaveDialog *iFileSaveDialog) SetRole(role string) error { - return fileSaveDialog.vtbl.setClientGuid(unsafe.Pointer(fileSaveDialog), util.StringToUUID(role)) + return fileSaveDialog.vtbl.setClientGuid(unsafe.Pointer(fileSaveDialog), StringToUUID(role)) } func (fileSaveDialog *iFileSaveDialog) SetDefaultExtension(defaultExtension string) error { diff --git a/v2/internal/go-common-file-dialog/cfd/iShellItem.go b/v2/internal/go-common-file-dialog/cfd/iShellItem.go index 6a747f4d9..080115345 100644 --- a/v2/internal/go-common-file-dialog/cfd/iShellItem.go +++ b/v2/internal/go-common-file-dialog/cfd/iShellItem.go @@ -30,6 +30,10 @@ type iShellItemVtbl struct { func newIShellItem(path string) (*iShellItem, error) { var shellItem *iShellItem pathPtr := ole.SysAllocString(path) + defer func(v *int16) { + _ = ole.SysFreeString(v) + }(pathPtr) + ret, _, _ := procSHCreateItemFromParsingName.Call( uintptr(unsafe.Pointer(pathPtr)), 0, @@ -40,10 +44,9 @@ func newIShellItem(path string) (*iShellItem, error) { func (vtbl *iShellItemVtbl) getDisplayName(objPtr unsafe.Pointer) (string, error) { var ptr *uint16 - ret, _, _ := syscall.Syscall(vtbl.GetDisplayName, - 2, + ret, _, _ := syscall.SyscallN(vtbl.GetDisplayName, uintptr(objPtr), - 0x80058000, // SIGDN_FILESYSPATH + 0x80058000, // SIGDN_FILESYSPATH, uintptr(unsafe.Pointer(&ptr))) if err := hresultToError(ret); err != nil { return "", err diff --git a/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go b/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go index 84f26fa20..c548160d1 100644 --- a/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go +++ b/v2/internal/go-common-file-dialog/cfd/iShellItemArray.go @@ -38,11 +38,9 @@ type iShellItemArrayVtbl struct { func (vtbl *iShellItemArrayVtbl) getCount(objPtr unsafe.Pointer) (uintptr, error) { var count uintptr - ret, _, _ := syscall.Syscall(vtbl.GetCount, - 1, + ret, _, _ := syscall.SyscallN(vtbl.GetCount, uintptr(objPtr), - uintptr(unsafe.Pointer(&count)), - 0) + uintptr(unsafe.Pointer(&count))) if err := hresultToError(ret); err != nil { return 0, err } @@ -51,8 +49,7 @@ func (vtbl *iShellItemArrayVtbl) getCount(objPtr unsafe.Pointer) (uintptr, error func (vtbl *iShellItemArrayVtbl) getItemAt(objPtr unsafe.Pointer, index uintptr) (string, error) { var shellItem *iShellItem - ret, _, _ := syscall.Syscall(vtbl.GetItemAt, - 2, + ret, _, _ := syscall.SyscallN(vtbl.GetItemAt, uintptr(objPtr), index, uintptr(unsafe.Pointer(&shellItem))) @@ -60,7 +57,7 @@ func (vtbl *iShellItemArrayVtbl) getItemAt(objPtr unsafe.Pointer, index uintptr) return "", err } if shellItem == nil { - return "", ErrorCancelled + return "", ErrCancelled } defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem)) diff --git a/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go b/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go index a92100010..581a7b25c 100644 --- a/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go +++ b/v2/internal/go-common-file-dialog/cfd/vtblCommonFunc.go @@ -1,10 +1,8 @@ //go:build windows -// +build windows package cfd import ( - "fmt" "github.com/go-ole/go-ole" "strings" "syscall" @@ -19,27 +17,23 @@ func hresultToError(hr uintptr) error { } func (vtbl *iUnknownVtbl) release(objPtr unsafe.Pointer) error { - ret, _, _ := syscall.Syscall(vtbl.Release, - 0, + ret, _, _ := syscall.SyscallN(vtbl.Release, uintptr(objPtr), - 0, 0) return hresultToError(ret) } func (vtbl *iModalWindowVtbl) show(objPtr unsafe.Pointer, hwnd uintptr) error { - ret, _, _ := syscall.Syscall(vtbl.Show, - 1, + ret, _, _ := syscall.SyscallN(vtbl.Show, uintptr(objPtr), - hwnd, - 0) + hwnd) return hresultToError(ret) } func (vtbl *iFileDialogVtbl) setFileTypes(objPtr unsafe.Pointer, filters []FileFilter) error { cFileTypes := len(filters) if cFileTypes < 0 { - return fmt.Errorf("must specify at least one filter") + return ErrEmptyFilters } comDlgFilterSpecs := make([]comDlgFilterSpec, cFileTypes) for i := 0; i < cFileTypes; i++ { @@ -49,8 +43,16 @@ func (vtbl *iFileDialogVtbl) setFileTypes(objPtr unsafe.Pointer, filters []FileF pszSpec: ole.SysAllocString(filter.Pattern), } } - ret, _, _ := syscall.Syscall(vtbl.SetFileTypes, - 2, + + // Ensure memory is freed after use + defer func() { + for _, spec := range comDlgFilterSpecs { + ole.SysFreeString(spec.pszName) + ole.SysFreeString(spec.pszSpec) + } + }() + + ret, _, _ := syscall.SyscallN(vtbl.SetFileTypes, uintptr(objPtr), uintptr(cFileTypes), uintptr(unsafe.Pointer(&comDlgFilterSpecs[0]))) @@ -82,21 +84,17 @@ func (vtbl *iFileDialogVtbl) setFileTypes(objPtr unsafe.Pointer, filters []FileF // FOS_FORCEPREVIEWPANEON = 0x40000000, // FOS_SUPPORTSTREAMABLEITEMS = 0x80000000 func (vtbl *iFileDialogVtbl) setOptions(objPtr unsafe.Pointer, options uint32) error { - ret, _, _ := syscall.Syscall(vtbl.SetOptions, - 1, + ret, _, _ := syscall.SyscallN(vtbl.SetOptions, uintptr(objPtr), - uintptr(options), - 0) + uintptr(options)) return hresultToError(ret) } func (vtbl *iFileDialogVtbl) getOptions(objPtr unsafe.Pointer) (uint32, error) { var options uint32 - ret, _, _ := syscall.Syscall(vtbl.GetOptions, - 1, + ret, _, _ := syscall.SyscallN(vtbl.GetOptions, uintptr(objPtr), - uintptr(unsafe.Pointer(&options)), - 0) + uintptr(unsafe.Pointer(&options))) return options, hresultToError(ret) } @@ -122,11 +120,9 @@ func (vtbl *iFileDialogVtbl) setDefaultFolder(objPtr unsafe.Pointer, path string return err } defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) - ret, _, _ := syscall.Syscall(vtbl.SetDefaultFolder, - 1, + ret, _, _ := syscall.SyscallN(vtbl.SetDefaultFolder, uintptr(objPtr), - uintptr(unsafe.Pointer(shellItem)), - 0) + uintptr(unsafe.Pointer(shellItem))) return hresultToError(ret) } @@ -136,40 +132,32 @@ func (vtbl *iFileDialogVtbl) setFolder(objPtr unsafe.Pointer, path string) error return err } defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) - ret, _, _ := syscall.Syscall(vtbl.SetFolder, - 1, + ret, _, _ := syscall.SyscallN(vtbl.SetFolder, uintptr(objPtr), - uintptr(unsafe.Pointer(shellItem)), - 0) + uintptr(unsafe.Pointer(shellItem))) return hresultToError(ret) } func (vtbl *iFileDialogVtbl) setTitle(objPtr unsafe.Pointer, title string) error { titlePtr := ole.SysAllocString(title) - ret, _, _ := syscall.Syscall(vtbl.SetTitle, - 1, + defer ole.SysFreeString(titlePtr) // Ensure the string is freed + ret, _, _ := syscall.SyscallN(vtbl.SetTitle, uintptr(objPtr), - uintptr(unsafe.Pointer(titlePtr)), - 0) + uintptr(unsafe.Pointer(titlePtr))) return hresultToError(ret) } func (vtbl *iFileDialogVtbl) close(objPtr unsafe.Pointer) error { - ret, _, _ := syscall.Syscall(vtbl.Close, - 1, - uintptr(objPtr), - 0, - 0) + ret, _, _ := syscall.SyscallN(vtbl.Close, + uintptr(objPtr)) return hresultToError(ret) } func (vtbl *iFileDialogVtbl) getResult(objPtr unsafe.Pointer) (*iShellItem, error) { var shellItem *iShellItem - ret, _, _ := syscall.Syscall(vtbl.GetResult, - 1, + ret, _, _ := syscall.SyscallN(vtbl.GetResult, uintptr(objPtr), - uintptr(unsafe.Pointer(&shellItem)), - 0) + uintptr(unsafe.Pointer(&shellItem))) return shellItem, hresultToError(ret) } @@ -179,49 +167,58 @@ func (vtbl *iFileDialogVtbl) getResultString(objPtr unsafe.Pointer) (string, err return "", err } if shellItem == nil { - return "", ErrorCancelled + return "", ErrCancelled } defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem)) } func (vtbl *iFileDialogVtbl) setClientGuid(objPtr unsafe.Pointer, guid *ole.GUID) error { - ret, _, _ := syscall.Syscall(vtbl.SetClientGuid, - 1, + // Ensure the GUID is not nil + if guid == nil { + return ErrInvalidGUID + } + + // Call the SetClientGuid method + ret, _, _ := syscall.SyscallN(vtbl.SetClientGuid, uintptr(objPtr), - uintptr(unsafe.Pointer(guid)), - 0) + uintptr(unsafe.Pointer(guid))) + + // Convert the HRESULT to a Go error return hresultToError(ret) } func (vtbl *iFileDialogVtbl) setDefaultExtension(objPtr unsafe.Pointer, defaultExtension string) error { - if defaultExtension[0] == '.' { + // Ensure the string is not empty before accessing the first character + if len(defaultExtension) > 0 && defaultExtension[0] == '.' { defaultExtension = strings.TrimPrefix(defaultExtension, ".") } + + // Allocate memory for the default extension string defaultExtensionPtr := ole.SysAllocString(defaultExtension) - ret, _, _ := syscall.Syscall(vtbl.SetDefaultExtension, - 1, + defer ole.SysFreeString(defaultExtensionPtr) // Ensure the string is freed + + // Call the SetDefaultExtension method + ret, _, _ := syscall.SyscallN(vtbl.SetDefaultExtension, uintptr(objPtr), - uintptr(unsafe.Pointer(defaultExtensionPtr)), - 0) + uintptr(unsafe.Pointer(defaultExtensionPtr))) + + // Convert the HRESULT to a Go error return hresultToError(ret) } func (vtbl *iFileDialogVtbl) setFileName(objPtr unsafe.Pointer, fileName string) error { fileNamePtr := ole.SysAllocString(fileName) - ret, _, _ := syscall.Syscall(vtbl.SetFileName, - 1, + defer ole.SysFreeString(fileNamePtr) // Ensure the string is freed + ret, _, _ := syscall.SyscallN(vtbl.SetFileName, uintptr(objPtr), - uintptr(unsafe.Pointer(fileNamePtr)), - 0) + uintptr(unsafe.Pointer(fileNamePtr))) return hresultToError(ret) } func (vtbl *iFileDialogVtbl) setSelectedFileFilterIndex(objPtr unsafe.Pointer, index uint) error { - ret, _, _ := syscall.Syscall(vtbl.SetFileTypeIndex, - 1, + ret, _, _ := syscall.SyscallN(vtbl.SetFileTypeIndex, uintptr(objPtr), - uintptr(index+1), // SetFileTypeIndex counts from 1 - 0) + uintptr(index+1)) // SetFileTypeIndex counts from 1 return hresultToError(ret) } diff --git a/v2/internal/gomod/gomod.go b/v2/internal/gomod/gomod.go index 8ab7e0b66..c38e60f0b 100644 --- a/v2/internal/gomod/gomod.go +++ b/v2/internal/gomod/gomod.go @@ -2,6 +2,7 @@ package gomod import ( "fmt" + "github.com/Masterminds/semver" "golang.org/x/mod/modfile" ) diff --git a/v2/internal/gomod/gomod_data_unix.go b/v2/internal/gomod/gomod_data_unix.go index f3a5e04c3..c6004f486 100644 --- a/v2/internal/gomod/gomod_data_unix.go +++ b/v2/internal/gomod/gomod_data_unix.go @@ -10,6 +10,7 @@ require github.com/wailsapp/wails/v2 v2.0.0-beta.7 //replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2 ` + const basicUpdated string = `module changeme go 1.17 @@ -29,6 +30,7 @@ require ( //replace github.com/wailsapp/wails/v2 v2.0.0-beta.7 => /home/lea/wails/v2 ` + const multilineReplace = `module changeme go 1.17 @@ -98,6 +100,7 @@ require ( replace github.com/wailsapp/wails/v2 v2.0.0-beta.20 => /home/lea/wails/v2 ` + const multilineReplaceNoVersionUpdated = `module changeme go 1.17 @@ -108,6 +111,7 @@ require ( replace github.com/wailsapp/wails/v2 => /home/lea/wails/v2 ` + const multilineReplaceNoVersionBlockUpdated = `module changeme go 1.17 diff --git a/v2/internal/goversion/min.go b/v2/internal/goversion/min.go index 17edc0c94..8c057b3c2 100644 --- a/v2/internal/goversion/min.go +++ b/v2/internal/goversion/min.go @@ -1,3 +1,3 @@ package goversion -const MinRequirement string = "1.18" +const MinRequirement string = "1.20" diff --git a/v2/internal/logger/custom_logger.go b/v2/internal/logger/custom_logger.go index 5e24aa093..51e07c0fc 100644 --- a/v2/internal/logger/custom_logger.go +++ b/v2/internal/logger/custom_logger.go @@ -86,7 +86,6 @@ func (l *customLogger) Warning(format string, args ...interface{}) { func (l *customLogger) Error(format string, args ...interface{}) { format = fmt.Sprintf("%s | %s", l.name, format) l.logger.Error(format, args...) - } // Fatal level logging. Works like Sprintf. diff --git a/v2/internal/logger/default_logger.go b/v2/internal/logger/default_logger.go index fe5c05387..5c72ae209 100644 --- a/v2/internal/logger/default_logger.go +++ b/v2/internal/logger/default_logger.go @@ -84,7 +84,6 @@ func (l *Logger) Info(format string, args ...interface{}) { if l.logLevel <= logger.INFO { l.output.Info(fmt.Sprintf(format, args...)) } - } // Warning level logging. Works like Sprintf. @@ -99,7 +98,6 @@ func (l *Logger) Error(format string, args ...interface{}) { if l.logLevel <= logger.ERROR { l.output.Error(fmt.Sprintf(format, args...)) } - } // Fatal level logging. Works like Sprintf. diff --git a/v2/internal/menumanager/applicationmenu.go b/v2/internal/menumanager/applicationmenu.go index 424834e53..4446a00cb 100644 --- a/v2/internal/menumanager/applicationmenu.go +++ b/v2/internal/menumanager/applicationmenu.go @@ -3,7 +3,6 @@ package menumanager import "github.com/wailsapp/wails/v2/pkg/menu" func (m *Manager) SetApplicationMenu(applicationMenu *menu.Menu) error { - if applicationMenu == nil { return nil } @@ -38,7 +37,6 @@ func (m *Manager) UpdateApplicationMenu() (string, error) { } func (m *Manager) processApplicationMenu() error { - // Process the menu m.processedApplicationMenu = NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu) m.processRadioGroups(m.processedApplicationMenu, m.applicationMenuItemMap) diff --git a/v2/internal/menumanager/contextmenu.go b/v2/internal/menumanager/contextmenu.go index 77c47891c..f05bcdc49 100644 --- a/v2/internal/menumanager/contextmenu.go +++ b/v2/internal/menumanager/contextmenu.go @@ -23,7 +23,6 @@ func (t *ContextMenu) AsJSON() (string, error) { } func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu { - result := &ContextMenu{ ID: contextMenu.ID, menu: contextMenu.Menu, @@ -37,7 +36,6 @@ func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu { } func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) { - newContextMenu := NewContextMenu(contextMenu) // Save the references diff --git a/v2/internal/menumanager/menuitemmap.go b/v2/internal/menumanager/menuitemmap.go index 790d5d06d..e4e291be6 100644 --- a/v2/internal/menumanager/menuitemmap.go +++ b/v2/internal/menumanager/menuitemmap.go @@ -2,8 +2,10 @@ package menumanager import ( "fmt" - "github.com/wailsapp/wails/v2/pkg/menu" + "strconv" "sync" + + "github.com/wailsapp/wails/v2/pkg/menu" ) // MenuItemMap holds a mapping between menuIDs and menu items @@ -48,14 +50,13 @@ func (m *MenuItemMap) Dump() { // GenerateMenuID returns a unique string ID for a menu item func (m *MenuItemMap) generateMenuID() string { m.menuIDCounterMutex.Lock() - result := fmt.Sprintf("%d", m.menuIDCounter) + result := strconv.FormatInt(m.menuIDCounter, 10) m.menuIDCounter++ m.menuIDCounterMutex.Unlock() return result } func (m *MenuItemMap) processMenuItem(item *menu.MenuItem) { - if item.SubMenu != nil { for _, submenuitem := range item.SubMenu.Items { m.processMenuItem(submenuitem) diff --git a/v2/internal/menumanager/menumanager.go b/v2/internal/menumanager/menumanager.go index ea7939415..0c6be0df2 100644 --- a/v2/internal/menumanager/menumanager.go +++ b/v2/internal/menumanager/menumanager.go @@ -2,11 +2,11 @@ package menumanager import ( "fmt" + "github.com/wailsapp/wails/v2/pkg/menu" ) type Manager struct { - // The application menu. applicationMenu *menu.Menu applicationMenuJSON string @@ -43,7 +43,6 @@ func (m *Manager) getMenuItemByID(menuMap *MenuItemMap, menuId string) *menu.Men } func (m *Manager) ProcessClick(menuID string, data string, menuType string, parentID string) error { - var menuItemMap *MenuItemMap switch menuType { @@ -93,7 +92,7 @@ func (m *Manager) ProcessClick(menuID string, data string, menuType string, pare // Create new Callback struct callbackData := &menu.CallbackData{ MenuItem: menuItem, - //ContextData: data, + // ContextData: data, } // Call back! diff --git a/v2/internal/menumanager/processedMenu.go b/v2/internal/menumanager/processedMenu.go index 8d886e19e..c87646ccb 100644 --- a/v2/internal/menumanager/processedMenu.go +++ b/v2/internal/menumanager/processedMenu.go @@ -2,6 +2,7 @@ package menumanager import ( "encoding/json" + "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu/keys" ) @@ -11,7 +12,7 @@ type ProcessedMenuItem struct { // Label is what appears as the menu text Label string `json:",omitempty"` // Role is a predefined menu type - //Role menu.Role `json:",omitempty"` + // Role menu.Role `json:",omitempty"` // Accelerator holds a representation of a key binding Accelerator *keys.Accelerator `json:",omitempty"` // Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu @@ -22,8 +23,8 @@ type ProcessedMenuItem struct { Hidden bool `json:",omitempty"` // Checked indicates if the item is selected (used by Checkbox and Radio types only) Checked bool `json:",omitempty"` - // Submenu contains a list of menu items that will be shown as a submenu - //SubMenu []*MenuItem `json:"SubMenu,omitempty"` + // SubMenu contains a list of menu items that will be shown as a submenu + // SubMenu []*MenuItem `json:"SubMenu,omitempty"` SubMenu *ProcessedMenu `json:",omitempty"` /* // Colour @@ -47,7 +48,6 @@ type ProcessedMenuItem struct { } func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem { - ID := menuItemMap.menuItemToIDMap[menuItem] // Parse ANSI text @@ -63,21 +63,21 @@ func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *Pr result := &ProcessedMenuItem{ ID: ID, Label: menuItem.Label, - //Role: menuItem.Role, + // Role: menuItem.Role, Accelerator: menuItem.Accelerator, Type: menuItem.Type, Disabled: menuItem.Disabled, Hidden: menuItem.Hidden, Checked: menuItem.Checked, SubMenu: nil, - //BackgroundColour: menuItem.BackgroundColour, - //FontSize: menuItem.FontSize, - //FontName: menuItem.FontName, - //Image: menuItem.Image, - //MacTemplateImage: menuItem.MacTemplateImage, - //MacAlternate: menuItem.MacAlternate, - //Tooltip: menuItem.Tooltip, - //StyledLabel: styledLabel, + // BackgroundColour: menuItem.BackgroundColour, + // FontSize: menuItem.FontSize, + // FontName: menuItem.FontName, + // Image: menuItem.Image, + // MacTemplateImage: menuItem.MacTemplateImage, + // MacAlternate: menuItem.MacAlternate, + // Tooltip: menuItem.Tooltip, + // StyledLabel: styledLabel, } if menuItem.SubMenu != nil { @@ -92,7 +92,6 @@ type ProcessedMenu struct { } func NewProcessedMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *ProcessedMenu { - result := &ProcessedMenu{} if menu != nil { for _, item := range menu.Items { @@ -131,7 +130,6 @@ func NewWailsMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *WailsMenu { } func (w *WailsMenu) AsJSON() (string, error) { - menuAsJSON, err := json.Marshal(w) if err != nil { return "", err @@ -150,7 +148,6 @@ func (w *WailsMenu) processRadioGroups() { } func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) { - switch item.Type { // We need to recurse submenus @@ -172,7 +169,6 @@ func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) { } func (w *WailsMenu) finaliseRadioGroup() { - // If we were processing a radio group, fix up the references if len(w.currentRadioGroup) > 0 { diff --git a/v2/internal/menumanager/traymenu.go b/v2/internal/menumanager/traymenu.go index aed5b05ac..5efc4a861 100644 --- a/v2/internal/menumanager/traymenu.go +++ b/v2/internal/menumanager/traymenu.go @@ -13,8 +13,10 @@ import ( "github.com/wailsapp/wails/v2/pkg/menu" ) -var trayMenuID int -var trayMenuIDMutex sync.Mutex +var ( + trayMenuID int + trayMenuIDMutex sync.Mutex +) func generateTrayID() string { var idStr string @@ -51,7 +53,6 @@ func (t *TrayMenu) AsJSON() (string, error) { } func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu { - // Parse ANSI text var styledLabel []*ansi.StyledText tempLabel := trayMenu.Label @@ -205,7 +206,6 @@ func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) { } return string(data), nil - } func (m *Manager) GetContextMenus() ([]string, error) { diff --git a/v2/internal/platform/win32/consts.go b/v2/internal/platform/win32/consts.go index 03f42b1a6..43149b036 100644 --- a/v2/internal/platform/win32/consts.go +++ b/v2/internal/platform/win32/consts.go @@ -80,7 +80,7 @@ ShouldSystemUseDarkMode = bool () // ordinal 138 SetPreferredAppMode = PreferredAppMode (PreferredAppMode appMode) // ordinal 135, since 18334 IsDarkModeAllowedForApp = bool () // ordinal 139 */ -func init() { +func Init() { if IsWindowsVersionAtLeast(10, 0, 18334) { // AllowDarkModeForWindow is only available on Windows 10+ diff --git a/v2/internal/process/process.go b/v2/internal/process/process.go index 6d497ed8e..18c9f45da 100644 --- a/v2/internal/process/process.go +++ b/v2/internal/process/process.go @@ -25,7 +25,6 @@ func NewProcess(cmd string, args ...string) *Process { // Start the process func (p *Process) Start(exitCodeChannel chan int) error { - err := p.cmd.Start() if err != nil { return err diff --git a/v2/internal/project/project.go b/v2/internal/project/project.go index 023ca1dfe..2df99bdfa 100644 --- a/v2/internal/project/project.go +++ b/v2/internal/project/project.go @@ -12,7 +12,6 @@ import ( // Project holds the data related to a Wails project type Project struct { - /*** Application Data ***/ Name string `json:"name"` AssetDirectory string `json:"assetdir,omitempty"` @@ -43,6 +42,9 @@ type Project struct { // Build directory BuildDir string `json:"build:dir"` + // BuildTags Extra tags to process during build + BuildTags string `json:"build:tags"` + // The output filename OutputFilename string `json:"outputfilename"` @@ -81,7 +83,7 @@ type Project struct { // The address to bind the wails dev server to. Default "localhost:34115" DevServer string `json:"devServer"` - // Arguments that are forwared to the application in dev mode + // Arguments that are forward to the application in dev mode AppArgs string `json:"appargs"` // NSISType to be build @@ -94,6 +96,9 @@ type Project struct { // Frontend directory FrontendDir string `json:"frontend:dir"` + // The timeout in seconds for Vite server detection. Default 10 + ViteServerTimeout int `json:"viteServerTimeout"` + Bindings Bindings `json:"bindings"` } @@ -144,11 +149,10 @@ func (p *Project) Save() error { if err != nil { return err } - return os.WriteFile(p.filename, data, 0755) + return os.WriteFile(p.filename, data, 0o755) } func (p *Project) setDefaults() { - if p.Path == "" { p.Path = lo.Must(os.Getwd()) } @@ -177,6 +181,9 @@ func (p *Project) setDefaults() { if p.DevServer == "" { p.DevServer = "localhost:34115" } + if p.ViteServerTimeout == 0 { + p.ViteServerTimeout = 10 + } if p.NSISType == "" { p.NSISType = "multiple" } @@ -216,11 +223,27 @@ type Author struct { } type Info struct { - CompanyName string `json:"companyName"` - ProductName string `json:"productName"` - ProductVersion string `json:"productVersion"` - Copyright *string `json:"copyright"` - Comments *string `json:"comments"` + CompanyName string `json:"companyName"` + ProductName string `json:"productName"` + ProductVersion string `json:"productVersion"` + Copyright *string `json:"copyright"` + Comments *string `json:"comments"` + FileAssociations []FileAssociation `json:"fileAssociations"` + Protocols []Protocol `json:"protocols"` +} + +type FileAssociation struct { + Ext string `json:"ext"` + Name string `json:"name"` + Description string `json:"description"` + IconName string `json:"iconName"` + Role string `json:"role"` +} + +type Protocol struct { + Scheme string `json:"scheme"` + Description string `json:"description"` + Role string `json:"role"` } type Bindings struct { @@ -228,8 +251,9 @@ type Bindings struct { } type TsGeneration struct { - Prefix string `json:"prefix"` - Suffix string `json:"suffix"` + Prefix string `json:"prefix"` + Suffix string `json:"suffix"` + OutputType string `json:"outputType"` } // Parse the given JSON data into a Project struct diff --git a/v2/internal/s/s.go b/v2/internal/s/s.go index 86536e24c..adb304178 100644 --- a/v2/internal/s/s.go +++ b/v2/internal/s/s.go @@ -2,9 +2,9 @@ package s import ( "crypto/md5" + "encoding/hex" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -28,7 +28,7 @@ func checkError(err error) { func mute() { originalOutput = Output - Output = ioutil.Discard + Output = io.Discard } func unmute() { @@ -74,9 +74,10 @@ func CD(dir string) { checkError(err) log("CD %s [%s]", dir, CWD()) } + func MKDIR(path string, mode ...os.FileMode) { var perms os.FileMode - perms = 0755 + perms = 0o755 if len(mode) == 1 { perms = mode[0] } @@ -88,7 +89,7 @@ func MKDIR(path string, mode ...os.FileMode) { // ENDIR ensures that the path gets created if it doesn't exist func ENDIR(path string, mode ...os.FileMode) { var perms os.FileMode - perms = 0755 + perms = 0o755 if len(mode) == 1 { perms = mode[0] } @@ -150,6 +151,7 @@ func COPY(source string, target string) { defer closefile(src) d, err := os.Create(target) checkError(err) + defer closefile(d) _, err = io.Copy(d, src) checkError(err) } @@ -210,17 +212,13 @@ func ISDIR(path string) bool { // ISDIREMPTY returns true if the given directory is empty func ISDIREMPTY(dir string) bool { - // CREDIT: https://stackoverflow.com/a/30708914/8325411 f, err := os.Open(dir) checkError(err) defer closefile(f) _, err = f.Readdirnames(1) // Or f.Readdir(1) - if err == io.EOF { - return true - } - return false + return err == io.EOF } // ISFILE returns true if the given file exists @@ -270,7 +268,7 @@ func LOADSTRING(filename string) string { // SAVEBYTES will create a file with the given string func SAVEBYTES(filename string, data []byte) { log("SAVEBYTES %s", filename) - err := os.WriteFile(filename, data, 0755) + err := os.WriteFile(filename, data, 0o755) checkError(err) } @@ -297,7 +295,7 @@ func MD5FILE(filename string) string { _, err = io.Copy(h, f) checkError(err) - return fmt.Sprintf("%x", h.Sum(nil)) + return hex.EncodeToString(h.Sum(nil)) } // Sub is the substitution type diff --git a/v2/internal/shell/shell.go b/v2/internal/shell/shell.go index badea2b39..349e27bff 100644 --- a/v2/internal/shell/shell.go +++ b/v2/internal/shell/shell.go @@ -42,6 +42,7 @@ func (c *Command) Run() error { func (c *Command) Stdout() string { return c.stdo.String() } + func (c *Command) Stderr() string { return c.stde.String() } @@ -93,8 +94,5 @@ func RunCommandVerbose(directory string, command string, args ...string) error { // CommandExists returns true if the given command can be found on the shell func CommandExists(name string) bool { _, err := exec.LookPath(name) - if err != nil { - return false - } - return true + return err == nil } diff --git a/v2/internal/signal/signal.go b/v2/internal/signal/signal.go index 96e10bee6..fa797453f 100644 --- a/v2/internal/signal/signal.go +++ b/v2/internal/signal/signal.go @@ -9,8 +9,10 @@ import ( var signalChannel = make(chan os.Signal, 2) -var callbacks []func() -var lock sync.Mutex +var ( + callbacks []func() + lock sync.Mutex +) func OnShutdown(callback func()) { lock.Lock() @@ -20,20 +22,17 @@ func OnShutdown(callback func()) { // Start the Signal Manager func Start() { - // Hook into interrupts gosignal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) // Spin off signal listener and wait for either a cancellation // or signal go func() { - select { - case <-signalChannel: - println("") - println("Ctrl+C detected. Shutting down...") - for _, callback := range callbacks { - callback() - } + <-signalChannel + println("") + println("Ctrl+C detected. Shutting down...") + for _, callback := range callbacks { + callback() } }() } diff --git a/v2/internal/staticanalysis/staticanalysis.go b/v2/internal/staticanalysis/staticanalysis.go index 0d8fb92d3..cde436633 100644 --- a/v2/internal/staticanalysis/staticanalysis.go +++ b/v2/internal/staticanalysis/staticanalysis.go @@ -2,9 +2,10 @@ package staticanalysis import ( "go/ast" - "golang.org/x/tools/go/packages" "path/filepath" "strings" + + "golang.org/x/tools/go/packages" ) type EmbedDetails struct { @@ -51,24 +52,31 @@ func GetEmbedDetailsForFile(file *ast.File, baseDir string) []*EmbedDetails { for _, c := range comment.List { if strings.HasPrefix(c.Text, "//go:embed") { sl := strings.Split(c.Text, " ") - path := "" - all := false if len(sl) == 1 { continue } - embedPath := strings.TrimSpace(sl[1]) - switch true { - case strings.HasPrefix(embedPath, "all:"): - path = strings.TrimPrefix(embedPath, "all:") - all = true - default: - path = embedPath + // support for multiple paths in one comment + for _, arg := range sl[1:] { + embedPath := strings.TrimSpace(arg) + // ignores all pattern matching characters except escape sequence + if strings.Contains(embedPath, "*") || strings.Contains(embedPath, "?") || strings.Contains(embedPath, "[") { + continue + } + if strings.HasPrefix(embedPath, "all:") { + result = append(result, &EmbedDetails{ + EmbedPath: strings.TrimPrefix(embedPath, "all:"), + All: true, + BaseDir: baseDir, + }) + } else { + result = append(result, &EmbedDetails{ + EmbedPath: embedPath, + All: false, + BaseDir: baseDir, + }) + } + } - result = append(result, &EmbedDetails{ - EmbedPath: path, - All: all, - BaseDir: baseDir, - }) } } } diff --git a/v2/internal/staticanalysis/staticanalysis_test.go b/v2/internal/staticanalysis/staticanalysis_test.go index 17599676e..77ad2fa6c 100644 --- a/v2/internal/staticanalysis/staticanalysis_test.go +++ b/v2/internal/staticanalysis/staticanalysis_test.go @@ -1,8 +1,9 @@ package staticanalysis import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestGetEmbedDetails(t *testing.T) { @@ -25,6 +26,10 @@ func TestGetEmbedDetails(t *testing.T) { EmbedPath: "frontend/dist", All: true, }, + { + EmbedPath: "frontend/static", + All: false, + }, }, wantErr: false, }, diff --git a/v2/internal/staticanalysis/test/standard/go.mod b/v2/internal/staticanalysis/test/standard/go.mod index 56184c62d..c9fe1fb52 100644 --- a/v2/internal/staticanalysis/test/standard/go.mod +++ b/v2/internal/staticanalysis/test/standard/go.mod @@ -25,11 +25,11 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - golang.org/x/crypto v0.0.0-20221012134737-56aed061732a // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect ) // replace github.com/wailsapp/wails/v2 v2.0.0 => C:\Users\leaan diff --git a/v2/internal/staticanalysis/test/standard/go.sum b/v2/internal/staticanalysis/test/standard/go.sum index 54b3fbb03..2cd0cf773 100644 --- a/v2/internal/staticanalysis/test/standard/go.sum +++ b/v2/internal/staticanalysis/test/standard/go.sum @@ -57,13 +57,13 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/wails/v2 v2.3.1 h1:ZJz+pyIBKyASkgO8JO31NuHO1gTTHmvwiHYHwei1CqM= github.com/wailsapp/wails/v2 v2.3.1/go.mod h1:zlNLI0E2c2qA6miiuAHtp0Bac8FaGH0tlhA19OssR/8= -golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg= -golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 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.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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= @@ -73,12 +73,12 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/internal/staticanalysis/test/standard/main.go b/v2/internal/staticanalysis/test/standard/main.go index 3f735d640..2b6ab33b6 100644 --- a/v2/internal/staticanalysis/test/standard/main.go +++ b/v2/internal/staticanalysis/test/standard/main.go @@ -8,9 +8,12 @@ import ( "github.com/wailsapp/wails/v2/pkg/options/assetserver" ) -//go:embed all:frontend/dist +//go:embed all:frontend/dist frontend/static var assets embed.FS +//go:embed frontend/src/*.json +var srcjson embed.FS + func main() { // Create an instance of the app structure app := NewApp() diff --git a/v2/internal/system/operatingsystem/os.go b/v2/internal/system/operatingsystem/os.go index 39f1de8e0..028a97b2e 100644 --- a/v2/internal/system/operatingsystem/os.go +++ b/v2/internal/system/operatingsystem/os.go @@ -2,9 +2,10 @@ package operatingsystem // OS contains information about the operating system type OS struct { - ID string - Name string - Version string + ID string + Name string + Version string + Branding string } // Info retrieves information about the current platform diff --git a/v2/internal/system/operatingsystem/os_windows.go b/v2/internal/system/operatingsystem/os_windows.go index 38ea43a12..a9aa05a92 100644 --- a/v2/internal/system/operatingsystem/os_windows.go +++ b/v2/internal/system/operatingsystem/os_windows.go @@ -4,10 +4,44 @@ package operatingsystem import ( "fmt" + "strings" + "syscall" + "unsafe" + "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) +func stripNulls(str string) string { + // Split the string into substrings at each null character + substrings := strings.Split(str, "\x00") + + // Join the substrings back into a single string + strippedStr := strings.Join(substrings, "") + + return strippedStr +} + +func mustStringToUTF16Ptr(input string) *uint16 { + input = stripNulls(input) + result, err := syscall.UTF16PtrFromString(input) + if err != nil { + panic(err) + } + return result +} + +func getBranding() string { + var modBranding = syscall.NewLazyDLL("winbrand.dll") + var brandingFormatString = modBranding.NewProc("BrandingFormatString") + + windowsLong := mustStringToUTF16Ptr("%WINDOWS_LONG%\x00") + ret, _, _ := brandingFormatString.Call( + uintptr(unsafe.Pointer(windowsLong)), + ) + return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ret))) +} + func platformInfo() (*OS, error) { // Default value var result OS @@ -27,6 +61,7 @@ func platformInfo() (*OS, error) { result.Name = productName result.Version = fmt.Sprintf("%s (Build: %s)", releaseId, currentBuild) result.ID = displayVersion + result.Branding = getBranding() return &result, key.Close() } diff --git a/v2/internal/system/packagemanager/eopkg.go b/v2/internal/system/packagemanager/eopkg.go index dbeab96de..936127eac 100644 --- a/v2/internal/system/packagemanager/eopkg.go +++ b/v2/internal/system/packagemanager/eopkg.go @@ -40,7 +40,7 @@ func (e *Eopkg) Packages() packagemap { {Name: "gcc", SystemPackage: true}, }, "pkg-config": []*Package{ - {Name: "pkg-config", SystemPackage: true}, + {Name: "pkgconf", SystemPackage: true}, }, "npm": []*Package{ {Name: "nodejs", SystemPackage: true}, diff --git a/v2/internal/system/packagemanager/pm.go b/v2/internal/system/packagemanager/pm.go index dfd394299..bba45cd05 100644 --- a/v2/internal/system/packagemanager/pm.go +++ b/v2/internal/system/packagemanager/pm.go @@ -16,9 +16,9 @@ type packagemap = map[string][]*Package type PackageManager interface { Name() string Packages() packagemap - PackageInstalled(*Package) (bool, error) - PackageAvailable(*Package) (bool, error) - InstallCommand(*Package) string + PackageInstalled(pkg *Package) (bool, error) + PackageAvailable(pkg *Package) (bool, error) + InstallCommand(pkg *Package) string } // Dependency represents a system package that we require @@ -37,7 +37,6 @@ type DependencyList []*Dependency // InstallAllRequiredCommand returns the command you need to use to install all required dependencies func (d DependencyList) InstallAllRequiredCommand() string { - result := "" for _, dependency := range d { if !dependency.Installed && !dependency.Optional { @@ -50,7 +49,6 @@ func (d DependencyList) InstallAllRequiredCommand() string { // InstallAllOptionalCommand returns the command you need to use to install all optional dependencies func (d DependencyList) InstallAllOptionalCommand() string { - result := "" for _, dependency := range d { if !dependency.Installed && dependency.Optional { diff --git a/v2/internal/system/packagemanager/zypper.go b/v2/internal/system/packagemanager/zypper.go index c486b53e1..efaeb0b1b 100644 --- a/v2/internal/system/packagemanager/zypper.go +++ b/v2/internal/system/packagemanager/zypper.go @@ -45,6 +45,7 @@ func (z *Zypper) Packages() packagemap { }, "npm": []*Package{ {Name: "npm10", SystemPackage: true}, + {Name: "npm20", SystemPackage: true}, }, "docker": []*Package{ {Name: "docker", SystemPackage: true, Optional: true}, diff --git a/v2/internal/system/system.go b/v2/internal/system/system.go index a633989ef..67453538f 100644 --- a/v2/internal/system/system.go +++ b/v2/internal/system/system.go @@ -9,12 +9,10 @@ import ( "github.com/wailsapp/wails/v2/internal/system/packagemanager" ) -var ( - IsAppleSilicon bool -) +var IsAppleSilicon bool // Info holds information about the current operating system, -// package manager and required dependancies +// package manager and required dependencies type Info struct { OS *operatingsystem.OS PM packagemanager.PackageManager @@ -23,7 +21,7 @@ type Info struct { // GetInfo scans the system for operating system details, // the system package manager and the status of required -// dependancies. +// dependencies. func GetInfo() (*Info, error) { var result Info err := result.discover() @@ -34,7 +32,6 @@ func GetInfo() (*Info, error) { } func checkNodejs() *packagemanager.Dependency { - // Check for Nodejs output, err := exec.Command("node", "-v").Output() installed := true @@ -58,7 +55,6 @@ func checkNodejs() *packagemanager.Dependency { } func checkNPM() *packagemanager.Dependency { - // Check for npm output, err := exec.Command("npm", "-version").Output() installed := true @@ -80,7 +76,6 @@ func checkNPM() *packagemanager.Dependency { } func checkUPX() *packagemanager.Dependency { - // Check for npm output, err := exec.Command("upx", "-V").Output() installed := true @@ -102,7 +97,6 @@ func checkUPX() *packagemanager.Dependency { } func checkNSIS() *packagemanager.Dependency { - // Check for nsis installer output, err := exec.Command("makensis", "-VERSION").Output() installed := true @@ -141,7 +135,6 @@ func checkLibrary(name string) func() *packagemanager.Dependency { } func checkDocker() *packagemanager.Dependency { - // Check for npm output, err := exec.Command("docker", "version").Output() installed := true diff --git a/v2/internal/typescriptify/typescriptify.go b/v2/internal/typescriptify/typescriptify.go index b14059a51..e732c5976 100644 --- a/v2/internal/typescriptify/typescriptify.go +++ b/v2/internal/typescriptify/typescriptify.go @@ -2,13 +2,15 @@ package typescriptify import ( "bufio" + "cmp" "fmt" - "io/ioutil" + "io" "log" "os" "path" "reflect" "regexp" + "slices" "strings" "time" @@ -24,7 +26,7 @@ const ( if (!a) { return a; } - if (a.slice) { + if (a.slice && a.map) { return (a as any[]).map(elem => this.convertValues(elem, classs)); } else if ("object" === typeof a) { if (asMap) { @@ -40,6 +42,18 @@ const ( jsVariableNameRegex = `^([A-Z]|[a-z]|\$|_)([A-Z]|[a-z]|[0-9]|\$|_)*$` ) +var jsVariableUnsafeChars = regexp.MustCompile(`[^A-Za-z0-9_]`) + +func nameTypeOf(typeOf reflect.Type) string { + tname := typeOf.Name() + gidx := strings.IndexRune(tname, '[') + if gidx > 0 { // its a generic type + rem := strings.SplitN(tname, "[", 2) + tname = rem[0] + "_" + jsVariableUnsafeChars.ReplaceAllLiteralString(rem[1], "_") + } + return tname +} + // TypeOptions overrides options set by `ts_*` tags. type TypeOptions struct { TSType string @@ -104,6 +118,7 @@ type TypeScriptify struct { Namespace string KnownStructs *slicer.StringSlicer + KnownEnums *slicer.StringSlicer } func New() *TypeScriptify { @@ -156,10 +171,10 @@ func (t *TypeScriptify) deepFields(typeOf reflect.Type) []reflect.StructField { kind := f.Type.Kind() isPointer := kind == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct if f.Anonymous && kind == reflect.Struct { - //fmt.Println(v.Interface()) + // fmt.Println(v.Interface()) fields = append(fields, t.deepFields(f.Type)...) } else if f.Anonymous && isPointer { - //fmt.Println(v.Interface()) + // fmt.Println(v.Interface()) fields = append(fields, t.deepFields(f.Type.Elem())...) } else { // Check we have a json tag @@ -260,15 +275,34 @@ func (t *TypeScriptify) AddType(typeOf reflect.Type) *TypeScriptify { func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField) { keyType := field.Type.Key() valueType := field.Type.Elem() - valueTypeName := valueType.Name() + valueTypeName := nameTypeOf(valueType) + valueTypeSuffix := "" + valueTypePrefix := "" + if valueType.Kind() == reflect.Ptr { + valueType = valueType.Elem() + valueTypeName = nameTypeOf(valueType) + } + if valueType.Kind() == reflect.Array || valueType.Kind() == reflect.Slice { + arrayDepth := 1 + for valueType.Elem().Kind() == reflect.Array || valueType.Elem().Kind() == reflect.Slice { + valueType = valueType.Elem() + arrayDepth++ + } + valueType = valueType.Elem() + valueTypeName = nameTypeOf(valueType) + valueTypeSuffix = strings.Repeat(">", arrayDepth) + valueTypePrefix = strings.Repeat("Array<", arrayDepth) + } + if valueType.Kind() == reflect.Ptr { + valueType = valueType.Elem() + valueTypeName = nameTypeOf(valueType) + } if name, ok := t.types[valueType.Kind()]; ok { valueTypeName = name } - if valueType.Kind() == reflect.Array || valueType.Kind() == reflect.Slice { - valueTypeName = valueType.Elem().Name() + "[]" - } - if valueType.Kind() == reflect.Ptr { - valueTypeName = valueType.Elem().Name() + if valueType.Kind() == reflect.Map { + // TODO: support nested maps + valueTypeName = "any" // valueType.Elem().Name() } if valueType.Kind() == reflect.Struct && differentNamespaces(t.namespace, valueType) { valueTypeName = valueType.String() @@ -293,11 +327,13 @@ func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.Str fieldName = fmt.Sprintf(`"%s"?`, strippedFieldName) } } - t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, valueTypeName)) + t.fields = append(t.fields, fmt.Sprintf("%s%s: Record<%s, %s>;", t.indent, fieldName, keyTypeStr, valueTypePrefix+valueTypeName+valueTypeSuffix)) if valueType.Kind() == reflect.Struct { - t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = this.convertValues(source[\"%s\"], %s, true);", t.indent, t.indent, dotField, strippedFieldName, t.prefix+valueTypeName+t.suffix)) + t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = this.convertValues(source[\"%s\"], %s, true);", + t.indent, t.indent, dotField, strippedFieldName, t.prefix+valueTypePrefix+valueTypeName+valueTypeSuffix+t.suffix)) } else { - t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = source[\"%s\"];", t.indent, t.indent, dotField, strippedFieldName)) + t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = source[\"%s\"];", + t.indent, t.indent, dotField, strippedFieldName)) } } @@ -338,6 +374,9 @@ func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify { elements = append(elements, el) } + slices.SortFunc(elements, func(a, b enumElement) int { + return cmp.Compare(a.name, b.name) + }) ty := reflect.TypeOf(elements[0].value) t.enums[ty] = elements t.enumTypes = append(t.enumTypes, EnumType{Type: ty}) @@ -393,7 +432,7 @@ func loadCustomCode(fileName string) (map[string]string, error) { } defer f.Close() - bytes, err := ioutil.ReadAll(f) + bytes, err := io.ReadAll(f) if err != nil { return result, err } @@ -429,7 +468,7 @@ func (t TypeScriptify) backup(fileName string) error { } defer fileIn.Close() - bytes, err := ioutil.ReadAll(fileIn) + bytes, err := io.ReadAll(fileIn) if err != nil { return err } @@ -439,7 +478,7 @@ func (t TypeScriptify) backup(fileName string) error { backupFn = path.Join(t.BackupDir, backupFn) } - return ioutil.WriteFile(backupFn, bytes, os.FileMode(0700)) + return os.WriteFile(backupFn, bytes, os.FileMode(0o700)) } func (t TypeScriptify) ConvertToFile(fileName string, packageName string) error { @@ -482,9 +521,6 @@ func (t TypeScriptify) ConvertToFile(fileName string, packageName string) error if _, err := f.WriteString(converted); err != nil { return err } - if err != nil { - return err - } return nil } @@ -500,7 +536,7 @@ func (t *TypeScriptify) convertEnum(depth int, typeOf reflect.Type, elements []e } t.alreadyConverted[typeOf.String()] = true - entityName := t.Prefix + typeOf.Name() + t.Suffix + entityName := t.Prefix + nameTypeOf(typeOf) + t.Suffix result := "enum " + entityName + " {\n" for _, val := range elements { @@ -552,7 +588,21 @@ func (t *TypeScriptify) getFieldOptions(structType reflect.Type, field reflect.S func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool) string { jsonFieldName := "" - jsonTag := field.Tag.Get("json") + // function, complex, and channel types cannot be json-encoded + if field.Type.Kind() == reflect.Chan || + field.Type.Kind() == reflect.Func || + field.Type.Kind() == reflect.UnsafePointer || + field.Type.Kind() == reflect.Complex128 || + field.Type.Kind() == reflect.Complex64 { + return "" + } + jsonTag, hasTag := field.Tag.Lookup("json") + if !hasTag && field.IsExported() { + jsonFieldName = field.Name + if isPtr { + jsonFieldName += "?" + } + } if len(jsonTag) > 0 { jsonTagParts := strings.Split(jsonTag, ",") if len(jsonTagParts) > 0 { @@ -585,9 +635,6 @@ func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode m return "", nil } fields := t.deepFields(typeOf) - if len(fields) == 0 { - return "", nil - } t.logf(depth, "Converting type %s", typeOf.String()) if differentNamespaces(t.Namespace, typeOf) { return "", nil @@ -595,7 +642,7 @@ func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode m t.alreadyConverted[typeOf.String()] = true - entityName := t.Prefix + typeOf.Name() + t.Suffix + entityName := t.Prefix + nameTypeOf(typeOf) + t.Suffix if typeClashWithReservedKeyword(entityName) { warnAboutTypesClash(entityName) @@ -655,8 +702,10 @@ func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode m } isKnownType := t.KnownStructs.Contains(getStructFQN(field.Type.String())) - println("KnownStructs:", t.KnownStructs.Join("\t")) - println(getStructFQN(field.Type.String())) + if !isKnownType { + println("KnownStructs:", t.KnownStructs.Join("\t")) + println("Not found:", getStructFQN(field.Type.String())) + } builder.AddStructField(jsonFieldName, field, !isKnownType) } else if field.Type.Kind() == reflect.Map { t.logf(depth, "- map field %s.%s", typeOf.Name(), field.Name) @@ -697,16 +746,20 @@ func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode m builder.AddMapField(jsonFieldName, field) } else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array { // Slice: - if field.Type.Elem().Kind() == reflect.Ptr { //extract ptr type + if field.Type.Elem().Kind() == reflect.Ptr { // extract ptr type field.Type = field.Type.Elem() } arrayDepth := 1 - for field.Type.Elem().Kind() == reflect.Slice { // Slice of slices: + for field.Type.Elem().Kind() == reflect.Slice || field.Type.Elem().Kind() == reflect.Array { // Slice of slices: field.Type = field.Type.Elem() arrayDepth++ } + if field.Type.Elem().Kind() == reflect.Ptr { // extract ptr type + field.Type = field.Type.Elem() + } + if field.Type.Elem().Kind() == reflect.Struct { // Slice of structs: t.logf(depth, "- struct slice %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String()) typeScriptChunk, err := t.convertType(depth+1, field.Type.Elem(), customCode) @@ -723,7 +776,16 @@ func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode m } } else { // Simple field: t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) - err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + // check if type is in known enum. If so, then replace TStype with enum name to avoid missing types + isKnownEnum := t.KnownEnums.Contains(getStructFQN(field.Type.String())) + if isKnownEnum { + err = builder.AddSimpleField(jsonFieldName, field, TypeOptions{ + TSType: getStructFQN(field.Type.String()), + TSTransform: fldOpts.TSTransform, + }) + } else { + err = builder.AddSimpleField(jsonFieldName, field, fldOpts) + } } if err != nil { return "", err @@ -787,8 +849,12 @@ type typeScriptClassBuilder struct { } func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error { - fieldType, kind := field.Type.Elem().Name(), field.Type.Elem().Kind() - typeScriptType := t.types[kind] + fieldType := nameTypeOf(field.Type.Elem()) + kind := field.Type.Elem().Kind() + typeScriptType, ok := t.types[kind] + if !ok { + typeScriptType = "any" + } if len(fieldName) > 0 { strippedFieldName := strings.ReplaceAll(fieldName, "?", "") @@ -807,9 +873,14 @@ func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field ref } func (t *typeScriptClassBuilder) AddSimpleField(fieldName string, field reflect.StructField, opts TypeOptions) error { - fieldType, kind := field.Type.Name(), field.Type.Kind() + fieldType := nameTypeOf(field.Type) + kind := field.Type.Kind() + + typeScriptType, ok := t.types[kind] + if !ok { + typeScriptType = "any" + } - typeScriptType := t.types[kind] if len(opts.TSType) > 0 { typeScriptType = opts.TSType } @@ -831,7 +902,7 @@ func (t *typeScriptClassBuilder) AddSimpleField(fieldName string, field reflect. } func (t *typeScriptClassBuilder) AddEnumField(fieldName string, field reflect.StructField) { - fieldType := field.Type.Name() + fieldType := nameTypeOf(field.Type) t.addField(fieldName, t.prefix+fieldType+t.suffix, false) strippedFieldName := strings.ReplaceAll(fieldName, "?", "") t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName)) @@ -841,7 +912,7 @@ func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect. strippedFieldName := strings.ReplaceAll(fieldName, "?", "") classname := "null" namespace := strings.Split(field.Type.String(), ".")[0] - fqname := t.prefix + field.Type.Name() + t.suffix + fqname := t.prefix + nameTypeOf(field.Type) + t.suffix if namespace != t.namespace { fqname = namespace + "." + fqname } @@ -860,7 +931,7 @@ func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect. } func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int) { - fieldType := field.Type.Elem().Name() + fieldType := nameTypeOf(field.Type.Elem()) if differentNamespaces(t.namespace, field.Type.Elem()) { fieldType = field.Type.Elem().String() } @@ -884,7 +955,7 @@ func (t *typeScriptClassBuilder) addField(fld, fldType string, isAnyType bool) { isOptional := strings.HasSuffix(fld, "?") strippedFieldName := strings.ReplaceAll(fld, "?", "") if !regexp.MustCompile(jsVariableNameRegex).Match([]byte(strippedFieldName)) { - fld = fmt.Sprintf(`"%s"`, fld) + fld = fmt.Sprintf(`"%s"`, strippedFieldName) if isOptional { fld += "?" } @@ -935,6 +1006,6 @@ func typeClashWithReservedKeyword(input string) bool { func warnAboutTypesClash(entity string) { // TODO: Refactor logging l := log.New(os.Stderr, "", 0) - l.Println(fmt.Sprintf("Usage of reserved keyword found and not supported: %s", entity)) + l.Printf("Usage of reserved keyword found and not supported: %s", entity) log.Println("Please rename returned type or consider adding bindings config to your wails.json") } diff --git a/v2/internal/webview2runtime/webview2installer.go b/v2/internal/webview2runtime/webview2installer.go index a2a2922dc..3645dae02 100644 --- a/v2/internal/webview2runtime/webview2installer.go +++ b/v2/internal/webview2runtime/webview2installer.go @@ -11,7 +11,7 @@ var setupexe []byte // WriteInstallerToFile writes the installer file to the given file. func WriteInstallerToFile(targetFile string) error { - return os.WriteFile(targetFile, setupexe, 0755) + return os.WriteFile(targetFile, setupexe, 0o755) } // WriteInstaller writes the installer exe file to the given directory and returns the path to it. diff --git a/v2/pkg/application/application.go b/v2/pkg/application/application.go index 8d8d72ef6..8ba586969 100644 --- a/v2/pkg/application/application.go +++ b/v2/pkg/application/application.go @@ -2,11 +2,12 @@ package application import ( "context" + "sync" + "github.com/wailsapp/wails/v2/internal/app" "github.com/wailsapp/wails/v2/internal/signal" "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/options" - "sync" ) // Application is the main Wails application @@ -86,7 +87,6 @@ func (a *Application) Bind(boundStruct any) { } func (a *Application) On(eventType EventType, callback func()) { - c := func(ctx context.Context) { callback() } diff --git a/v2/pkg/assetserver/assethandler.go b/v2/pkg/assetserver/assethandler.go index c85bf81e6..b8e2df076 100644 --- a/v2/pkg/assetserver/assethandler.go +++ b/v2/pkg/assetserver/assethandler.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "path" + "strconv" "strings" "github.com/wailsapp/wails/v2/pkg/options/assetserver" @@ -37,7 +38,6 @@ type assetHandler struct { } func NewAssetHandler(options assetserver.Options, log Logger) (http.Handler, error) { - vfs := options.Assets if vfs != nil { if _, err := vfs.Open("."); err != nil { @@ -110,7 +110,7 @@ func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } -// serveFile will try to load the file from the fs.FS and write it to the response +// serveFSFile will try to load the file from the fs.FS and write it to the response func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, filename string) error { if d.fs == nil { return os.ErrNotExist @@ -178,7 +178,8 @@ func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, fi return nil } - rw.Header().Set(HeaderContentLength, fmt.Sprintf("%d", statInfo.Size())) + size := strconv.FormatInt(statInfo.Size(), 10) + rw.Header().Set(HeaderContentLength, size) // Write the first 512 bytes used for MimeType sniffing _, err = io.Copy(rw, bytes.NewReader(buf[:n])) diff --git a/v2/pkg/assetserver/assethandler_external.go b/v2/pkg/assetserver/assethandler_external.go index 743f051e7..98b3404e9 100644 --- a/v2/pkg/assetserver/assethandler_external.go +++ b/v2/pkg/assetserver/assethandler_external.go @@ -1,18 +1,22 @@ -//go:build dev -// +build dev - package assetserver import ( "errors" "fmt" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" "net/http" "net/http/httputil" "net/url" - - "github.com/wailsapp/wails/v2/pkg/options/assetserver" ) +func NewProxyServer(proxyURL string) http.Handler { + parsedURL, err := url.Parse(proxyURL) + if err != nil { + panic(err) + } + return httputil.NewSingleHostReverseProxy(parsedURL) +} + func NewExternalAssetsHandler(logger Logger, options assetserver.Options, url *url.URL) http.Handler { baseHandler := options.Handler diff --git a/v2/pkg/assetserver/assetserver.go b/v2/pkg/assetserver/assetserver.go index 625c3f245..59665c091 100644 --- a/v2/pkg/assetserver/assetserver.go +++ b/v2/pkg/assetserver/assetserver.go @@ -5,10 +5,10 @@ import ( "fmt" "math/rand" "net/http" - "net/http/httptest" "strings" "golang.org/x/net/html" + "html/template" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" @@ -68,9 +68,11 @@ func NewAssetServer(bindingsJSON string, options assetserver.Options, servingFro } func NewAssetServerWithHandler(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { + var buffer bytes.Buffer if bindingsJSON != "" { - buffer.WriteString(`window.wailsbindings='` + bindingsJSON + `';` + "\n") + escapedBindingsJSON := template.JSEscapeString(bindingsJSON) + buffer.WriteString(`window.wailsbindings='` + escapedBindingsJSON + `';` + "\n") } buffer.Write(runtime.RuntimeDesktopJS()) @@ -111,23 +113,58 @@ func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } - header := rw.Header() if d.servingFromDisk { - header.Add(HeaderCacheControl, "no-cache") + rw.Header().Add(HeaderCacheControl, "no-cache") + } + + handler := d.handler + if req.Method != http.MethodGet { + handler.ServeHTTP(rw, req) + return } path := req.URL.Path - switch path { - case "", "/", "/index.html": - recorder := httptest.NewRecorder() - d.handler.ServeHTTP(recorder, req) - for k, v := range recorder.HeaderMap { - header[k] = v + if path == runtimeJSPath { + d.writeBlob(rw, path, d.runtimeJS) + } else if path == runtimePath && d.runtimeHandler != nil { + d.runtimeHandler.HandleRuntimeCall(rw, req) + } else if path == ipcJSPath { + content := d.runtime.DesktopIPC() + if d.ipcJS != nil { + content = d.ipcJS(req) + } + d.writeBlob(rw, path, content) + + } else if script, ok := d.pluginScripts[path]; ok { + d.writeBlob(rw, path, []byte(script)) + } else if d.isRuntimeInjectionMatch(path) { + recorder := &bodyRecorder{ + ResponseWriter: rw, + doRecord: func(code int, h http.Header) bool { + if code == http.StatusNotFound { + return true + } + + if code != http.StatusOK { + return false + } + + return strings.Contains(h.Get(HeaderContentType), "text/html") + }, } - switch recorder.Code { + handler.ServeHTTP(recorder, req) + + body := recorder.Body() + if body == nil { + // The body has been streamed and not recorded, we are finished + return + } + + code := recorder.Code() + switch code { case http.StatusOK: - content, err := d.processIndexHTML(recorder.Body.Bytes()) + content, err := d.processIndexHTML(body.Bytes()) if err != nil { d.serveError(rw, err, "Unable to processIndexHTML") return @@ -138,34 +175,12 @@ func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { d.writeBlob(rw, indexHTML, defaultHTML) default: - rw.WriteHeader(recorder.Code) + rw.WriteHeader(code) } - case runtimeJSPath: - d.writeBlob(rw, path, d.runtimeJS) - - case runtimePath: - if d.runtimeHandler != nil { - d.runtimeHandler.HandleRuntimeCall(rw, req) - } else { - d.handler.ServeHTTP(rw, req) - } - - case ipcJSPath: - content := d.runtime.DesktopIPC() - if d.ipcJS != nil { - content = d.ipcJS(req) - } - d.writeBlob(rw, path, content) - - default: - // Check if this is a plugin script - if script, ok := d.pluginScripts[path]; ok { - d.writeBlob(rw, path, []byte(script)) - return - } - d.handler.ServeHTTP(rw, req) + } else { + handler.ServeHTTP(rw, req) } } @@ -229,3 +244,12 @@ func (d *AssetServer) logError(message string, args ...interface{}) { d.logger.Error("[AssetServer] "+message, args...) } } + +func (AssetServer) isRuntimeInjectionMatch(path string) bool { + if path == "" { + path = "/" + } + + return strings.HasSuffix(path, "/") || + strings.HasSuffix(path, "/"+indexHTML) +} diff --git a/v2/pkg/assetserver/assetserver_webview.go b/v2/pkg/assetserver/assetserver_webview.go index 575c81bb1..63f80f0ae 100644 --- a/v2/pkg/assetserver/assetserver_webview.go +++ b/v2/pkg/assetserver/assetserver_webview.go @@ -60,7 +60,7 @@ func (d *AssetServer) processWebViewRequest(r webview.Request) { } } -// processHTTPRequest processes the HTTP Request by faking a golang HTTP Server. +// processWebViewRequestInternal processes the HTTP Request by faking a golang HTTP Server. // The request will be finished with a StatusNotImplemented code if no handler has written to the response. func (d *AssetServer) processWebViewRequestInternal(r webview.Request) { uri := "unknown" @@ -131,9 +131,10 @@ func (d *AssetServer) processWebViewRequestInternal(r webview.Request) { } if req.ContentLength == 0 { - req.ContentLength, _ = strconv.ParseInt(req.Header.Get(HeaderContentLength), 10, 64) + req.ContentLength = -1 } else { - req.Header.Set(HeaderContentLength, fmt.Sprintf("%d", req.ContentLength)) + size := strconv.FormatInt(req.ContentLength, 10) + req.Header.Set(HeaderContentLength, size) } if host := req.Header.Get(HeaderHost); host != "" { diff --git a/v2/pkg/assetserver/body_recorder.go b/v2/pkg/assetserver/body_recorder.go new file mode 100644 index 000000000..fa3bc1e7c --- /dev/null +++ b/v2/pkg/assetserver/body_recorder.go @@ -0,0 +1,61 @@ +package assetserver + +import ( + "bytes" + "net/http" +) + +type bodyRecorder struct { + http.ResponseWriter + doRecord func(code int, header http.Header) bool + + body *bytes.Buffer + code int + wroteHeader bool +} + +func (rw *bodyRecorder) Write(buf []byte) (int, error) { + rw.writeHeader(buf, http.StatusOK) + if rw.body != nil { + return rw.body.Write(buf) + } + return rw.ResponseWriter.Write(buf) +} + +func (rw *bodyRecorder) WriteHeader(code int) { + rw.writeHeader(nil, code) +} + +func (rw *bodyRecorder) Code() int { + return rw.code +} + +func (rw *bodyRecorder) Body() *bytes.Buffer { + return rw.body +} + +func (rw *bodyRecorder) writeHeader(buf []byte, code int) { + if rw.wroteHeader { + return + } + + if rw.doRecord != nil { + header := rw.Header() + if len(buf) != 0 { + if _, hasType := header[HeaderContentType]; !hasType { + header.Set(HeaderContentType, http.DetectContentType(buf)) + } + } + + if rw.doRecord(code, header) { + rw.body = bytes.NewBuffer(nil) + } + } + + if rw.body == nil { + rw.ResponseWriter.WriteHeader(code) + } + + rw.code = code + rw.wroteHeader = true +} diff --git a/v2/pkg/assetserver/common.go b/v2/pkg/assetserver/common.go index 01e51f2be..57934e08e 100644 --- a/v2/pkg/assetserver/common.go +++ b/v2/pkg/assetserver/common.go @@ -3,9 +3,9 @@ package assetserver import ( "bytes" "errors" - "fmt" "io" "net/http" + "strconv" "strings" "github.com/wailsapp/wails/v2/pkg/options" @@ -44,7 +44,7 @@ const ( func serveFile(rw http.ResponseWriter, filename string, blob []byte) error { header := rw.Header() - header.Set(HeaderContentLength, fmt.Sprintf("%d", len(blob))) + header.Set(HeaderContentLength, strconv.Itoa(len(blob))) if mimeType := header.Get(HeaderContentType); mimeType == "" { mimeType = GetMimetype(filename, blob) header.Set(HeaderContentType, mimeType) diff --git a/v2/pkg/assetserver/webview/request_darwin.go b/v2/pkg/assetserver/webview/request_darwin.go index f0e85780b..c44e5f196 100644 --- a/v2/pkg/assetserver/webview/request_darwin.go +++ b/v2/pkg/assetserver/webview/request_darwin.go @@ -197,7 +197,10 @@ func (r *request) Close() error { if r.body != nil { err = r.body.Close() } - r.Response().Finish() + err = r.Response().Finish() + if err != nil { + return err + } C.URLSchemeTaskRelease(r.task) return err } diff --git a/v2/pkg/assetserver/webview/request_linux.go b/v2/pkg/assetserver/webview/request_linux.go index 101ee12fb..c6785fb1c 100644 --- a/v2/pkg/assetserver/webview/request_linux.go +++ b/v2/pkg/assetserver/webview/request_linux.go @@ -4,7 +4,9 @@ package webview /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 gio-unix-2.0 +#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 #include "gtk/gtk.h" #include "webkit2/webkit2.h" diff --git a/v2/pkg/assetserver/webview/responsewriter_darwin.go b/v2/pkg/assetserver/webview/responsewriter_darwin.go index 77de3c455..a3c73b6f1 100644 --- a/v2/pkg/assetserver/webview/responsewriter_darwin.go +++ b/v2/pkg/assetserver/webview/responsewriter_darwin.go @@ -69,6 +69,7 @@ import "C" import ( "encoding/json" + "fmt" "net/http" "unsafe" ) @@ -98,16 +99,31 @@ func (rw *responseWriter) Write(buf []byte) (int, error) { rw.WriteHeader(http.StatusOK) - var content unsafe.Pointer var contentLen int if buf != nil { - content = unsafe.Pointer(&buf[0]) contentLen = len(buf) } - if !C.URLSchemeTaskDidReceiveData(rw.r.task, content, C.int(contentLen)) { - return 0, errRequestStopped + if contentLen > 0 { + // Create a C array to hold the data + cBuf := C.malloc(C.size_t(contentLen)) + if cBuf == nil { + return 0, fmt.Errorf("memory allocation failed for %d bytes", contentLen) + } + defer C.free(cBuf) + + // Copy the Go slice to the C array + C.memcpy(cBuf, unsafe.Pointer(&buf[0]), C.size_t(contentLen)) + + if !C.URLSchemeTaskDidReceiveData(rw.r.task, cBuf, C.int(contentLen)) { + return 0, errRequestStopped + } + } else { + if !C.URLSchemeTaskDidReceiveData(rw.r.task, nil, 0) { + return 0, errRequestStopped + } } + return contentLen, nil } diff --git a/v2/pkg/assetserver/webview/responsewriter_linux.go b/v2/pkg/assetserver/webview/responsewriter_linux.go index 52e28aa5d..59646ce29 100644 --- a/v2/pkg/assetserver/webview/responsewriter_linux.go +++ b/v2/pkg/assetserver/webview/responsewriter_linux.go @@ -4,7 +4,9 @@ package webview /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 gio-unix-2.0 +#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 #include "gtk/gtk.h" #include "webkit2/webkit2.h" diff --git a/v2/pkg/assetserver/webview/webkit2_36+.go b/v2/pkg/assetserver/webview/webkit2_36+.go index 2c1a79c43..1f0db3c89 100644 --- a/v2/pkg/assetserver/webview/webkit2_36+.go +++ b/v2/pkg/assetserver/webview/webkit2_36+.go @@ -1,9 +1,11 @@ -//go:build linux && (webkit2_36 || webkit2_40) +//go:build linux && (webkit2_36 || webkit2_40 || webkit2_41 ) package webview /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 libsoup-2.4 +#cgo linux pkg-config: gtk+-3.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 libsoup-2.4 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 libsoup-3.0 #include "gtk/gtk.h" #include "webkit2/webkit2.h" diff --git a/v2/pkg/assetserver/webview/webkit2_40+.go b/v2/pkg/assetserver/webview/webkit2_40+.go index dceb0803d..eb3e439f2 100644 --- a/v2/pkg/assetserver/webview/webkit2_40+.go +++ b/v2/pkg/assetserver/webview/webkit2_40+.go @@ -1,9 +1,11 @@ -//go:build linux && webkit2_40 +//go:build linux && (webkit2_40 || webkit2_41) package webview /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 gio-unix-2.0 +#cgo linux pkg-config: gtk+-3.0 gio-unix-2.0 +#cgo !webkit2_41 pkg-config: webkit2gtk-4.0 +#cgo webkit2_41 pkg-config: webkit2gtk-4.1 #include "gtk/gtk.h" #include "webkit2/webkit2.h" diff --git a/v2/pkg/assetserver/webview/webkit2_41.go b/v2/pkg/assetserver/webview/webkit2_41.go new file mode 100644 index 000000000..82f948d06 --- /dev/null +++ b/v2/pkg/assetserver/webview/webkit2_41.go @@ -0,0 +1,5 @@ +//go:build linux && webkit2_41 + +package webview + +const Webkit2MinMinorVersion = 41 diff --git a/v2/pkg/assetserver/webview/webkit2_legacy.go b/v2/pkg/assetserver/webview/webkit2_legacy.go index 1a87fe96a..1d1cf7c2b 100644 --- a/v2/pkg/assetserver/webview/webkit2_legacy.go +++ b/v2/pkg/assetserver/webview/webkit2_legacy.go @@ -1,4 +1,4 @@ -//go:build linux && !(webkit2_36 || webkit2_40) +//go:build linux && !(webkit2_36 || webkit2_40 || webkit2_41) package webview diff --git a/v2/pkg/buildassets/build/darwin/Info.dev.plist b/v2/pkg/buildassets/build/darwin/Info.dev.plist index 02e7358ee..14121ef7c 100644 --- a/v2/pkg/buildassets/build/darwin/Info.dev.plist +++ b/v2/pkg/buildassets/build/darwin/Info.dev.plist @@ -6,7 +6,7 @@ CFBundleName {{.Info.ProductName}} CFBundleExecutable - {{.Name}} + {{.OutputFilename}} CFBundleIdentifier com.wails.{{.Name}} CFBundleVersion @@ -23,10 +23,46 @@ true NSHumanReadableCopyright {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} NSAppTransportSecurity NSAllowsLocalNetworking - \ No newline at end of file + diff --git a/v2/pkg/buildassets/build/darwin/Info.plist b/v2/pkg/buildassets/build/darwin/Info.plist index e7819a7e8..d17a7475c 100644 --- a/v2/pkg/buildassets/build/darwin/Info.plist +++ b/v2/pkg/buildassets/build/darwin/Info.plist @@ -6,7 +6,7 @@ CFBundleName {{.Info.ProductName}} CFBundleExecutable - {{.Name}} + {{.OutputFilename}} CFBundleIdentifier com.wails.{{.Name}} CFBundleVersion @@ -23,5 +23,41 @@ true NSHumanReadableCopyright {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} - \ No newline at end of file + diff --git a/v2/pkg/buildassets/build/windows/installer/project.nsi b/v2/pkg/buildassets/build/windows/installer/project.nsi index 13cc4f023..654ae2e49 100644 --- a/v2/pkg/buildassets/build/windows/installer/project.nsi +++ b/v2/pkg/buildassets/build/windows/installer/project.nsi @@ -3,10 +3,10 @@ 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 with the values from ProjectInfo. -## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows 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: @@ -17,7 +17,7 @@ Unicode true ## 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 ProjectInfo file, but they can be overwritten here. +## The following information is taken from the ProjectInfo file, but they can be overwritten here. #### ## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" ## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" @@ -85,16 +85,19 @@ Section !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.associateCustomProtocols + !insertmacro wails.writeUninstaller SectionEnd -Section "uninstall" +Section "uninstall" !insertmacro wails.setShellContext RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath @@ -104,5 +107,8 @@ Section "uninstall" Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + !insertmacro wails.deleteUninstaller SectionEnd diff --git a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh index 467c349ac..2f6d32195 100644 --- a/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh +++ b/v2/pkg/buildassets/build/windows/installer/wails_tools.nsh @@ -158,22 +158,92 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" ${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" + 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 "tmp\MicrosoftEdgeWebview2Setup.exe" ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' - + SetDetailsPrint both ok: -!macroend \ No newline at end of file +!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 + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + File "..\{{.IconName}}.ico" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconName}}.ico" + {{end}} +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!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 + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/v2/pkg/buildassets/buildassets.go b/v2/pkg/buildassets/buildassets.go index 26401745d..6934b98bd 100644 --- a/v2/pkg/buildassets/buildassets.go +++ b/v2/pkg/buildassets/buildassets.go @@ -102,8 +102,9 @@ func ReadOriginalFileWithProjectDataAndSave(projectData *project.Project, file s } type assetData struct { - Name string - Info project.Info + Name string + Info project.Info + OutputFilename string } func resolveProjectData(content []byte, projectData *project.Project) ([]byte, error) { @@ -113,8 +114,9 @@ func resolveProjectData(content []byte, projectData *project.Project) ([]byte, e } data := &assetData{ - Name: projectData.Name, - Info: projectData.Info, + Name: projectData.Name, + Info: projectData.Info, + OutputFilename: projectData.OutputFilename, } var out bytes.Buffer @@ -128,12 +130,12 @@ func writeFileSystemFile(projectData *project.Project, file string, content []by targetPath := GetLocalPath(projectData, file) if dir := filepath.Dir(targetPath); !fs.DirExists(dir) { - if err := fs.MkDirs(dir, 0755); err != nil { + if err := fs.MkDirs(dir, 0o755); err != nil { return fmt.Errorf("Unable to create directory: %w", err) } } - if err := os.WriteFile(targetPath, content, 0644); err != nil { + if err := os.WriteFile(targetPath, content, 0o644); err != nil { return err } return nil diff --git a/v2/pkg/commands/bindings/bindings.go b/v2/pkg/commands/bindings/bindings.go index 71c1747b7..82ce0d58f 100644 --- a/v2/pkg/commands/bindings/bindings.go +++ b/v2/pkg/commands/bindings/bindings.go @@ -18,15 +18,16 @@ type Options struct { Filename string Tags []string ProjectDirectory string + Compiler string GoModTidy bool TsPrefix string TsSuffix string + TsOutputType string } // GenerateBindings generates bindings for the Wails project in the given ProjectDirectory. // If no project directory is given then the current working directory is used. func GenerateBindings(options Options) (string, error) { - filename, _ := lo.Coalesce(options.Filename, "wailsbindings") if runtime.GOOS == "windows" { filename += ".exe" @@ -46,17 +47,32 @@ func GenerateBindings(options Options) (string, error) { tagString := buildtags.Stringify(genModuleTags) if options.GoModTidy { - stdout, stderr, err = shell.RunCommand(workingDirectory, "go", "mod", "tidy") + stdout, stderr, err = shell.RunCommand(workingDirectory, options.Compiler, "mod", "tidy") if err != nil { return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err) } } - stdout, stderr, err = shell.RunCommand(workingDirectory, "go", "build", "-tags", tagString, "-o", filename) + envBuild := os.Environ() + envBuild = shell.SetEnv(envBuild, "GOOS", runtime.GOOS) + envBuild = shell.SetEnv(envBuild, "GOARCH", runtime.GOARCH) + // wailsbindings is executed on the build machine. + // So, use the default C compiler, not the one set for cross compiling. + envBuild = shell.RemoveEnv(envBuild, "CC") + + stdout, stderr, err = shell.RunCommandWithEnv(envBuild, workingDirectory, options.Compiler, "build", "-buildvcs=false", "-tags", tagString, "-o", filename) if err != nil { return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err) } + if runtime.GOOS == "darwin" { + // Remove quarantine attribute + stdout, stderr, err = shell.RunCommand(workingDirectory, "/usr/bin/xattr", "-rc", filename) + if err != nil { + return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err) + } + } + defer func() { // Best effort removal of temp file _ = os.Remove(filename) @@ -66,6 +82,7 @@ func GenerateBindings(options Options) (string, error) { env := os.Environ() env = shell.SetEnv(env, "tsprefix", options.TsPrefix) env = shell.SetEnv(env, "tssuffix", options.TsSuffix) + env = shell.SetEnv(env, "tsoutputtype", options.TsOutputType) stdout, stderr, err = shell.RunCommandWithEnv(env, workingDirectory, filename) if err != nil { diff --git a/v2/pkg/commands/bindings/bindings_test.go b/v2/pkg/commands/bindings/bindings_test.go index a2cbed436..53f42f2c7 100644 --- a/v2/pkg/commands/bindings/bindings_test.go +++ b/v2/pkg/commands/bindings/bindings_test.go @@ -1,13 +1,14 @@ package bindings import ( - "github.com/matryer/is" - "github.com/wailsapp/wails/v2/pkg/templates" "os" "path/filepath" "runtime" "strings" "testing" + + "github.com/matryer/is" + "github.com/wailsapp/wails/v2/pkg/templates" ) const standardBindings = `// @ts-check @@ -80,6 +81,7 @@ func TestGenerateBindings(t *testing.T) { name: "should generate standard bindings with no user tags", options: Options{ ProjectDirectory: projectDir, + Compiler: "go", GoModTidy: true, }, expectedBindings: standardBindings, @@ -90,6 +92,7 @@ func TestGenerateBindings(t *testing.T) { name: "should generate bindings when given tags", options: Options{ ProjectDirectory: projectDir, + Compiler: "go", Tags: []string{"test"}, GoModTidy: true, }, @@ -101,6 +104,7 @@ func TestGenerateBindings(t *testing.T) { name: "should generate obfuscated bindings", options: Options{ ProjectDirectory: projectDir, + Compiler: "go", Tags: []string{"obfuscated"}, GoModTidy: true, }, diff --git a/v2/pkg/commands/build/base.go b/v2/pkg/commands/build/base.go index abfbafff5..239932ce8 100644 --- a/v2/pkg/commands/build/base.go +++ b/v2/pkg/commands/build/base.go @@ -74,7 +74,6 @@ func (b *BaseBuilder) convertFileToIntegerString(filename string) (string, error } func (b *BaseBuilder) convertByteSliceToIntegerString(data []byte) string { - // Create string builder var result strings.Builder @@ -85,8 +84,7 @@ func (b *BaseBuilder) convertByteSliceToIntegerString(data []byte) string { result.WriteString(fmt.Sprintf("%v,", data[i])) } - result.WriteString(fmt.Sprintf("%v", data[len(data)-1])) - + result.WriteString(strconv.FormatUint(uint64(data[len(data)-1]), 10)) } return result.String() @@ -94,10 +92,8 @@ func (b *BaseBuilder) convertByteSliceToIntegerString(data []byte) string { // CleanUp does post-build housekeeping func (b *BaseBuilder) CleanUp() { - // Delete all the files b.filesToDelete.Each(func(filename string) { - // if file doesn't exist, ignore if !b.fileExists(filename) { return @@ -106,7 +102,6 @@ func (b *BaseBuilder) CleanUp() { // Delete file. We ignore errors because these files will be overwritten // by the next build anyway. _ = os.Remove(filename) - }) } @@ -159,7 +154,6 @@ func (b *BaseBuilder) OutputFilename(options *Options) string { // CompileProject compiles the project func (b *BaseBuilder) CompileProject(options *Options) error { - // Check if the runtime wrapper exists err := generateRuntimeWrapper(options) if err != nil { @@ -199,6 +193,8 @@ func (b *BaseBuilder) CompileProject(options *Options) error { // Default go build command commands.Add("build") + commands.Add("-buildvcs=false") + // Add better debugging flags if options.Mode == Dev || options.Mode == Debug { commands.Add("-gcflags") @@ -313,7 +309,9 @@ func (b *BaseBuilder) CompileProject(options *Options) error { if v != "" { v += " " } - v += "-mmacosx-version-min=10.13" + if !strings.Contains(v, "-mmacosx-version-min") { + v += "-mmacosx-version-min=10.13" + } } return v }) @@ -350,7 +348,9 @@ func (b *BaseBuilder) CompileProject(options *Options) error { if addUTIFramework { v += "-framework UniformTypeIdentifiers " } - v += "-mmacosx-version-min=10.13" + if !strings.Contains(v, "-mmacosx-version-min") { + v += "-mmacosx-version-min=10.13" + } return v }) @@ -402,7 +402,7 @@ Please reinstall by doing the following: return nil } - var args = []string{"--best", "--no-color", "--no-progress", options.CompiledBinary} + args := []string{"--best", "--no-color", "--no-progress", options.CompiledBinary} if options.CompressFlags != "" { args = strings.Split(options.CompressFlags, " ") @@ -426,7 +426,6 @@ Please reinstall by doing the following: } func generateRuntimeWrapper(options *Options) error { - if options.WailsJSDir == "" { cwd, err := os.Getwd() if err != nil { @@ -452,7 +451,6 @@ func (b *BaseBuilder) NpmInstall(sourceDir string, verbose bool) error { // NpmInstallUsingCommand runs the given install command in the specified npm project directory func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand string, verbose bool) error { - packageJSON := filepath.Join(sourceDir, "package.json") // Check package.json exists @@ -492,7 +490,7 @@ func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand st } // Shortcut installation - if install == false { + if !install { if verbose { pterm.Println("Skipping npm install") } @@ -549,7 +547,6 @@ func (b *BaseBuilder) NpmRunWithEnvironment(projectDir, buildTarget string, verb // BuildFrontend executes the `npm build` command for the frontend directory func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error { - verbose := b.options.Verbosity == VERBOSE frontendDir := b.projectData.GetFrontendDir() diff --git a/v2/pkg/commands/build/build.go b/v2/pkg/commands/build/build.go index a29840b1b..491a57801 100644 --- a/v2/pkg/commands/build/build.go +++ b/v2/pkg/commands/build/build.go @@ -3,6 +3,7 @@ package build import ( "fmt" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -69,11 +70,11 @@ type Options struct { Obfuscated bool // Indicates that bound methods should be obfuscated GarbleArgs string // The arguments for Garble SkipBindings bool // Skip binding generation + SkipEmbedCreate bool // Skip creation of embed files } // Build the project! func Build(options *Options) (string, error) { - // Extract logger outputLogger := options.Logger @@ -121,8 +122,10 @@ func Build(options *Options) (string, error) { } // Create embed directories if they don't exist - if err := CreateEmbedDirectories(cwd, options); err != nil { - return "", err + if !options.SkipEmbedCreate { + if err := CreateEmbedDirectories(cwd, options); err != nil { + return "", err + } } // Generate bindings @@ -170,21 +173,23 @@ func CreateEmbedDirectories(cwd string, buildOptions *Options) error { for _, embedDetail := range embedDetails { fullPath := embedDetail.GetFullPath() - if _, err := os.Stat(fullPath); os.IsNotExist(err) { - err := os.MkdirAll(fullPath, 0755) - if err != nil { - return err + // assumes path is directory only if it has no extension + if filepath.Ext(fullPath) == "" { + if _, err := os.Stat(fullPath); os.IsNotExist(err) { + err := os.MkdirAll(fullPath, 0o755) + if err != nil { + return err + } + f, err := os.Create(filepath.Join(fullPath, "gitkeep")) + if err != nil { + return err + } + _ = f.Close() } - f, err := os.Create(filepath.Join(fullPath, "gitkeep")) - if err != nil { - return err - } - _ = f.Close() } } return nil - } func fatal(message string) { @@ -213,7 +218,6 @@ func printBulletPoint(text string, args ...any) { } func GenerateBindings(buildOptions *Options) error { - obfuscated := buildOptions.Obfuscated if obfuscated { printBulletPoint("Generating obfuscated bindings: ") @@ -222,12 +226,18 @@ func GenerateBindings(buildOptions *Options) error { printBulletPoint("Generating bindings: ") } + if buildOptions.ProjectData.Bindings.TsGeneration.OutputType == "" { + buildOptions.ProjectData.Bindings.TsGeneration.OutputType = "classes" + } + // Generate Bindings output, err := bindings.GenerateBindings(bindings.Options{ - Tags: buildOptions.UserTags, - GoModTidy: !buildOptions.SkipModTidy, - TsPrefix: buildOptions.ProjectData.Bindings.TsGeneration.Prefix, - TsSuffix: buildOptions.ProjectData.Bindings.TsGeneration.Suffix, + Compiler: buildOptions.Compiler, + Tags: buildOptions.UserTags, + GoModTidy: !buildOptions.SkipModTidy, + TsPrefix: buildOptions.ProjectData.Bindings.TsGeneration.Prefix, + TsSuffix: buildOptions.ProjectData.Bindings.TsGeneration.Suffix, + TsOutputType: buildOptions.ProjectData.Bindings.TsGeneration.OutputType, }) if err != nil { return err @@ -255,7 +265,7 @@ func execBuildApplication(builder Builder, options *Options) (string, error) { // When we finish, we will want to remove the syso file defer func() { - err := os.Remove(filepath.Join(options.ProjectData.Path, options.ProjectData.Name+"-res.syso")) + err := os.Remove(filepath.Join(options.ProjectData.Path, strings.ReplaceAll(options.ProjectData.Name, " ", "_")+"-res.syso")) if err != nil { fatal(err.Error()) } @@ -319,6 +329,20 @@ func execBuildApplication(builder Builder, options *Options) (string, error) { } } + if runtime.GOOS == "darwin" { + // Remove quarantine attribute + if _, err := os.Stat(options.CompiledBinary); os.IsNotExist(err) { + return "", fmt.Errorf("compiled binary does not exist at path: %s", options.CompiledBinary) + } + stdout, stderr, err := shell.RunCommand(options.BinDirectory, "/usr/bin/xattr", "-rc", options.CompiledBinary) + if err != nil { + return "", fmt.Errorf("%s - %s", err.Error(), stderr) + } + if options.Verbosity == VERBOSE && stdout != "" { + pterm.Info.Println(stdout) + } + } + pterm.Println("Done.") // Do we need to pack the app for non-windows? @@ -334,6 +358,16 @@ func execBuildApplication(builder Builder, options *Options) (string, error) { pterm.Println("Done.") } + if runtime.GOOS == "darwin" && options.Platform == "darwin" { + // On macOS, self-sign the .app bundle so notifications work + printBulletPoint("Self-signing application: ") + cmd := exec.Command("/usr/bin/codesign", "--force", "--deep", "--sign", "-", options.CompiledBinary) + if out, err := cmd.CombinedOutput(); err != nil { + return "", fmt.Errorf("codesign failed: %v – %s", err, out) + } + pterm.Println("Done.") + } + if options.Platform == "windows" { const nativeWebView2Loader = "native_webview2loader" @@ -371,7 +405,6 @@ func execPostBuildHook(outputLogger *clilogger.CLILogger, options *Options, hook } return executeBuildHook(outputLogger, options, hookIdentifier, argReplacements, postBuildHook, "post") - } func executeBuildHook(_ *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string, buildHook string, hookName string) error { @@ -407,7 +440,6 @@ func executeBuildHook(_ *clilogger.CLILogger, options *Options, hookIdentifier s if options.Verbosity == VERBOSE { pterm.Info.Println(strings.Join(args, " ")) - } if !fs.DirExists(options.BinDirectory) { diff --git a/v2/pkg/commands/build/builder.go b/v2/pkg/commands/build/builder.go index 4840341c0..6a220c530 100644 --- a/v2/pkg/commands/build/builder.go +++ b/v2/pkg/commands/build/builder.go @@ -8,8 +8,8 @@ import ( // Builder defines a builder that can build Wails applications type Builder interface { SetProjectData(projectData *project.Project) - BuildFrontend(*clilogger.CLILogger) error - CompileProject(*Options) error - OutputFilename(*Options) string + BuildFrontend(logger *clilogger.CLILogger) error + CompileProject(options *Options) error + OutputFilename(options *Options) string CleanUp() } diff --git a/v2/pkg/commands/build/nsis_installer.go b/v2/pkg/commands/build/nsis_installer.go index 11f1407a3..820df2d1d 100644 --- a/v2/pkg/commands/build/nsis_installer.go +++ b/v2/pkg/commands/build/nsis_installer.go @@ -41,7 +41,7 @@ func GenerateNSISInstaller(options *Options, amd64Binary string, arm64Binary str // Write the WebView2 SetupFile webviewSetup := buildassets.GetLocalPath(options.ProjectData, path.Join(nsisFolder, nsisWebView2SetupFile)) if dir := filepath.Dir(webviewSetup); !fs.DirExists(dir) { - if err := fs.MkDirs(dir, 0755); err != nil { + if err := fs.MkDirs(dir, 0o755); err != nil { return err } } @@ -92,7 +92,7 @@ func makeNSIS(options *Options, installerKind string, amd64Binary string, arm64B outputLogger := options.Logger outputLogger.Print(" - Building '%s' installer: ", installerKind) - var args = []string{} + args := []string{} if amd64Binary != "" { args = append(args, "-DARG_WAILS_AMD64_BINARY="+amd64Binary) } diff --git a/v2/pkg/commands/build/packager.go b/v2/pkg/commands/build/packager.go index 92ce37e90..d406256f9 100644 --- a/v2/pkg/commands/build/packager.go +++ b/v2/pkg/commands/build/packager.go @@ -3,12 +3,15 @@ package build import ( "bytes" "fmt" - "github.com/leaanthony/winicon" - "github.com/tc-hib/winres" - "github.com/tc-hib/winres/version" "image" "os" "path/filepath" + "strings" + + "github.com/leaanthony/winicon" + "github.com/tc-hib/winres" + "github.com/tc-hib/winres/version" + "github.com/wailsapp/wails/v2/internal/project" "github.com/jackmordaunt/icns" "github.com/pkg/errors" @@ -19,7 +22,6 @@ import ( // PackageProject packages the application func packageProject(options *Options, platform string) error { - var err error switch platform { case "darwin": @@ -41,7 +43,6 @@ func packageProject(options *Options, platform string) error { // cleanBinDirectory will remove an existing bin directory and recreate it func cleanBinDirectory(options *Options) error { - buildDirectory := options.BinDirectory // Clear out old builds @@ -53,7 +54,7 @@ func cleanBinDirectory(options *Options) error { } // Create clean directory - err := os.MkdirAll(buildDirectory, 0700) + err := os.MkdirAll(buildDirectory, 0o700) if err != nil { return err } @@ -62,7 +63,6 @@ func cleanBinDirectory(options *Options) error { } func packageApplicationForDarwin(options *Options) error { - var err error // Create directory structure @@ -73,20 +73,20 @@ func packageApplicationForDarwin(options *Options) error { contentsDirectory := filepath.Join(options.BinDirectory, bundlename, "/Contents") exeDir := filepath.Join(contentsDirectory, "/MacOS") - err = fs.MkDirs(exeDir, 0755) + err = fs.MkDirs(exeDir, 0o755) if err != nil { return err } resourceDir := filepath.Join(contentsDirectory, "/Resources") - err = fs.MkDirs(resourceDir, 0755) + err = fs.MkDirs(resourceDir, 0o755) if err != nil { return err } // Copy binary - packedBinaryPath := filepath.Join(exeDir, options.ProjectData.Name) + packedBinaryPath := filepath.Join(exeDir, options.ProjectData.OutputFilename) err = fs.MoveFile(options.CompiledBinary, packedBinaryPath) if err != nil { - return errors.Wrap(err, "Cannot move file: "+options.ProjectData.OutputFilename) + return errors.Wrap(err, "Cannot move file: "+options.CompiledBinary) } // Generate Info.plist @@ -95,19 +95,26 @@ func packageApplicationForDarwin(options *Options) error { return err } - // Generate Icons - err = processApplicationIcon(options, resourceDir) + // Generate App Icon + err = processDarwinIcon(options.ProjectData, "appicon", resourceDir, "iconfile") if err != nil { return err } + // Generate FileAssociation Icons + for _, fileAssociation := range options.ProjectData.Info.FileAssociations { + err = processDarwinIcon(options.ProjectData, fileAssociation.IconName, resourceDir, "") + if err != nil { + return err + } + } + options.CompiledBinary = packedBinaryPath return nil } func processPList(options *Options, contentsDirectory string) error { - sourcePList := "Info.plist" if options.Mode == Dev { // Use Info.dev.plist if using build mode @@ -121,11 +128,11 @@ func processPList(options *Options, contentsDirectory string) error { } targetFile := filepath.Join(contentsDirectory, "Info.plist") - return os.WriteFile(targetFile, content, 0644) + return os.WriteFile(targetFile, content, 0o644) } -func processApplicationIcon(options *Options, resourceDir string) (err error) { - appIcon, err := buildassets.ReadFile(options.ProjectData, "appicon.png") +func processDarwinIcon(projectData *project.Project, iconName string, resourceDir string, destIconName string) (err error) { + appIcon, err := buildassets.ReadFile(projectData, iconName+".png") if err != nil { return err } @@ -135,11 +142,14 @@ func processApplicationIcon(options *Options, resourceDir string) (err error) { return err } - tgtBundle := filepath.Join(resourceDir, "iconfile.icns") + if destIconName == "" { + destIconName = iconName + } + + tgtBundle := filepath.Join(resourceDir, destIconName+".icns") dest, err := os.Create(tgtBundle) if err != nil { return err - } defer func() { err = dest.Close() @@ -151,13 +161,21 @@ func processApplicationIcon(options *Options, resourceDir string) (err error) { } func packageApplicationForWindows(options *Options) error { - // Generate icon + // Generate app icon var err error - err = generateIcoFile(options) + err = generateIcoFile(options, "appicon", "icon") if err != nil { return err } + // Generate FileAssociation Icons + for _, fileAssociation := range options.ProjectData.Info.FileAssociations { + err = generateIcoFile(options, fileAssociation.IconName, "") + if err != nil { + return err + } + } + // Create syso file err = compileResources(options) if err != nil { @@ -171,21 +189,26 @@ func packageApplicationForLinux(_ *Options) error { return nil } -func generateIcoFile(options *Options) error { - content, err := buildassets.ReadFile(options.ProjectData, "appicon.png") +func generateIcoFile(options *Options, iconName string, destIconName string) error { + content, err := buildassets.ReadFile(options.ProjectData, iconName+".png") if err != nil { return err } + + if destIconName == "" { + destIconName = iconName + } + // Check ico file exists already - icoFile := buildassets.GetLocalPath(options.ProjectData, "windows/icon.ico") + icoFile := buildassets.GetLocalPath(options.ProjectData, "windows/"+destIconName+".ico") if !fs.FileExists(icoFile) { if dir := filepath.Dir(icoFile); !fs.DirExists(dir) { - if err := fs.MkDirs(dir, 0755); err != nil { + if err := fs.MkDirs(dir, 0o755); err != nil { return err } } - output, err := os.OpenFile(icoFile, os.O_CREATE|os.O_WRONLY, 0644) + output, err := os.OpenFile(icoFile, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return err } @@ -200,13 +223,12 @@ func generateIcoFile(options *Options) error { } func compileResources(options *Options) error { - currentDir, err := os.Getwd() if err != nil { return err } defer func() { - os.Chdir(currentDir) + _ = os.Chdir(currentDir) }() windowsDir := filepath.Join(options.ProjectData.GetBuildDir(), "windows") err = os.Chdir(windowsDir) @@ -253,7 +275,8 @@ func compileResources(options *Options) error { rs.SetVersionInfo(v) } - targetFile := filepath.Join(options.ProjectData.Path, options.ProjectData.Name+"-res.syso") + // replace spaces with underscores as go build behaves weirdly with spaces in syso filename + targetFile := filepath.Join(options.ProjectData.Path, strings.ReplaceAll(options.ProjectData.Name, " ", "_")+"-res.syso") fout, err := os.Create(targetFile) if err != nil { return err diff --git a/v2/pkg/commands/buildtags/buildtags.go b/v2/pkg/commands/buildtags/buildtags.go index 70820d03d..5cca16acf 100644 --- a/v2/pkg/commands/buildtags/buildtags.go +++ b/v2/pkg/commands/buildtags/buildtags.go @@ -8,7 +8,7 @@ import ( ) // Parse parses the given tags string and returns -// a cleaned slice of strings. Both comma and space delimeted +// a cleaned slice of strings. Both comma and space delimited // tags are supported but not mixed. If mixed, an error is returned. func Parse(tags string) ([]string, error) { if tags == "" { diff --git a/v2/pkg/git/git.go b/v2/pkg/git/git.go index 319c5672b..a0ac68ca9 100644 --- a/v2/pkg/git/git.go +++ b/v2/pkg/git/git.go @@ -1,7 +1,8 @@ package git import ( - "html/template" + "encoding/json" + "fmt" "runtime" "strings" @@ -30,9 +31,31 @@ func Email() (string, error) { // Name tries to retrieve the func Name() (string, error) { + errMsg := "failed to retrieve git user name: %w" stdout, _, err := shell.RunCommand(".", gitcommand(), "config", "user.name") - name := template.JSEscapeString(strings.TrimSpace(stdout)) - return name, err + if err != nil { + return "", fmt.Errorf(errMsg, err) + } + name := strings.TrimSpace(stdout) + return EscapeName(name) +} + +func EscapeName(str string) (string, error) { + b, err := json.Marshal(str) + if err != nil { + return "", err + } + // Remove the surrounding quotes + escaped := string(b[1 : len(b)-1]) + + // Check if username is JSON compliant + var js json.RawMessage + jsonVal := fmt.Sprintf(`{"name": "%s"}`, escaped) + err = json.Unmarshal([]byte(jsonVal), &js) + if err != nil { + return "", fmt.Errorf("failed to retrieve git user name: %w", err) + } + return escaped, nil } func InitRepo(projectDir string) error { diff --git a/v2/pkg/git/git_test.go b/v2/pkg/git/git_test.go new file mode 100644 index 000000000..238008ec3 --- /dev/null +++ b/v2/pkg/git/git_test.go @@ -0,0 +1,44 @@ +package git + +import ( + "testing" +) + +func TestEscapeName1(t *testing.T) { + type args struct { + str string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "Escape Apostrophe", + args: args{ + str: `John O'Keefe`, + }, + want: `John O'Keefe`, + }, + { + name: "Escape backslash", + args: args{ + str: `MYDOMAIN\USER`, + }, + want: `MYDOMAIN\\USER`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := EscapeName(tt.args.str) + if (err != nil) != tt.wantErr { + t.Errorf("EscapeName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("EscapeName() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/pkg/logger/filelogger.go b/v2/pkg/logger/filelogger.go index 5cf68fc1a..954c46f59 100644 --- a/v2/pkg/logger/filelogger.go +++ b/v2/pkg/logger/filelogger.go @@ -19,7 +19,7 @@ func NewFileLogger(filename string) Logger { // Print works like Sprintf. func (l *FileLogger) Print(message string) { - f, err := os.OpenFile(l.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + f, err := os.OpenFile(l.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { log.Fatal(err) } diff --git a/v2/pkg/logger/logger.go b/v2/pkg/logger/logger.go index abc288265..990dffe75 100644 --- a/v2/pkg/logger/logger.go +++ b/v2/pkg/logger/logger.go @@ -41,6 +41,24 @@ func StringToLogLevel(input string) (LogLevel, error) { return result, nil } +// String returns the string representation of the LogLevel +func (l LogLevel) String() string { + switch l { + case TRACE: + return "trace" + case DEBUG: + return "debug" + case INFO: + return "info" + case WARNING: + return "warning" + case ERROR: + return "error" + default: + return "debug" + } +} + // Logger specifies the methods required to attach // a logger to a Wails application type Logger interface { diff --git a/v2/pkg/mac/login_darwin.go b/v2/pkg/mac/login_darwin.go index 2ff49be83..b2390e305 100644 --- a/v2/pkg/mac/login_darwin.go +++ b/v2/pkg/mac/login_darwin.go @@ -33,7 +33,7 @@ func StartAtLogin(enabled bool) error { } _, stde, err := shell.RunCommand("/tmp", "osascript", "-e", command) if err != nil { - errors.Wrap(err, stde) + return errors.Wrap(err, stde) } return nil } diff --git a/v2/pkg/mac/notification_darwin.go b/v2/pkg/mac/notification_darwin.go index a06ecb53a..243f07c78 100644 --- a/v2/pkg/mac/notification_darwin.go +++ b/v2/pkg/mac/notification_darwin.go @@ -8,7 +8,7 @@ import ( "github.com/wailsapp/wails/v2/internal/shell" ) -// StartAtLogin will either add or remove this application to/from the login +// ShowNotification will either add or remove this application to/from the login // items, depending on the given boolean flag. The limitation is that the // currently running app must be in an app bundle. func ShowNotification(title string, subtitle string, message string, sound string) error { @@ -24,7 +24,7 @@ func ShowNotification(title string, subtitle string, message string, sound strin } _, stde, err := shell.RunCommand("/tmp", "osascript", "-e", command) if err != nil { - errors.Wrap(err, stde) + return errors.Wrap(err, stde) } return nil } diff --git a/v2/pkg/menu/callback.go b/v2/pkg/menu/callback.go index fe6160361..a02664ac0 100644 --- a/v2/pkg/menu/callback.go +++ b/v2/pkg/menu/callback.go @@ -2,7 +2,7 @@ package menu type CallbackData struct { MenuItem *MenuItem - //ContextData string + // ContextData string } type Callback func(*CallbackData) diff --git a/v2/pkg/menu/colours/colours.go b/v2/pkg/menu/colours/colours.go index 28564a09e..5fb74eabd 100644 --- a/v2/pkg/menu/colours/colours.go +++ b/v2/pkg/menu/colours/colours.go @@ -36,7 +36,6 @@ type InputCol struct { var Template string func main() { - var Cols []InputCol resp, err := http.Get("https://jonasjacek.github.io/colors/data.json") @@ -62,5 +61,8 @@ func main() { if err != nil { log.Fatal(err) } - os.WriteFile(filepath.Join("..", "cols.go"), buffer.Bytes(), 0755) + err = os.WriteFile(filepath.Join("..", "cols.go"), buffer.Bytes(), 0o755) + if err != nil { + log.Fatal(err) + } } diff --git a/v2/pkg/menu/keys/keys.go b/v2/pkg/menu/keys/keys.go index fa8027a33..961edab2d 100644 --- a/v2/pkg/menu/keys/keys.go +++ b/v2/pkg/menu/keys/keys.go @@ -16,7 +16,7 @@ const ( // ShiftKey represents the shift key on all systems ShiftKey Modifier = "shift" // SuperKey represents Command on Mac and the Windows key on the other platforms - //SuperKey Modifier = "super" + // SuperKey Modifier = "super" // ControlKey represents the control key on all systems ControlKey Modifier = "ctrl" ) diff --git a/v2/pkg/menu/keys/parser.go b/v2/pkg/menu/keys/parser.go index 91a05783d..6e8e12376 100644 --- a/v2/pkg/menu/keys/parser.go +++ b/v2/pkg/menu/keys/parser.go @@ -11,7 +11,6 @@ import ( var namedKeys = slicer.String([]string{"backspace", "tab", "return", "enter", "escape", "left", "right", "up", "down", "space", "delete", "home", "end", "page up", "page down", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "f21", "f22", "f23", "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31", "f32", "f33", "f34", "f35", "numlock"}) func parseKey(key string) (string, bool) { - // Lowercase! key = strings.ToLower(key) @@ -38,11 +37,9 @@ func parseKey(key string) (string, bool) { } return "", false - } func Parse(shortcut string) (*Accelerator, error) { - var result Accelerator // Split the shortcut by + diff --git a/v2/pkg/menu/keys/stringify.go b/v2/pkg/menu/keys/stringify.go index ccc8c5e9e..92498f5d4 100644 --- a/v2/pkg/menu/keys/stringify.go +++ b/v2/pkg/menu/keys/stringify.go @@ -1,8 +1,9 @@ package keys import ( - "github.com/leaanthony/slicer" "strings" + + "github.com/leaanthony/slicer" ) var modifierStringMap = map[string]map[Modifier]string{ @@ -11,21 +12,21 @@ var modifierStringMap = map[string]map[Modifier]string{ ControlKey: "Ctrl", OptionOrAltKey: "Alt", ShiftKey: "Shift", - //SuperKey: "Win", + // SuperKey: "Win", }, "darwin": { CmdOrCtrlKey: "Cmd", ControlKey: "Ctrl", OptionOrAltKey: "Option", ShiftKey: "Shift", - //SuperKey: "Cmd", + // SuperKey: "Cmd", }, "linux": { CmdOrCtrlKey: "Ctrl", ControlKey: "Ctrl", OptionOrAltKey: "Alt", ShiftKey: "Shift", - //SuperKey: "Super", + // SuperKey: "Super", }, } diff --git a/v2/pkg/menu/menu.go b/v2/pkg/menu/menu.go index 819939bbf..86acbd1d0 100644 --- a/v2/pkg/menu/menu.go +++ b/v2/pkg/menu/menu.go @@ -59,8 +59,7 @@ func (m *Menu) Prepend(item *MenuItem) { } func NewMenuFromItems(first *MenuItem, rest ...*MenuItem) *Menu { - - var result = NewMenu() + result := NewMenu() result.Append(first) for _, item := range rest { result.Append(item) diff --git a/v2/pkg/menu/menuitem.go b/v2/pkg/menu/menuitem.go index f6ea681d7..bffc522d8 100644 --- a/v2/pkg/menu/menuitem.go +++ b/v2/pkg/menu/menuitem.go @@ -22,8 +22,8 @@ type MenuItem struct { Hidden bool // Checked indicates if the item is selected (used by Checkbox and Radio types only) Checked bool - // Submenu contains a list of menu items that will be shown as a submenu - //SubMenu []*MenuItem `json:"SubMenu,omitempty"` + // SubMenu contains a list of menu items that will be shown as a submenu + // SubMenu []*MenuItem `json:"SubMenu,omitempty"` SubMenu *Menu // Callback function when menu clicked @@ -106,7 +106,6 @@ func (m *MenuItem) removeChild(item *MenuItem) { // menu. If there is no parent menu (we are a top level menu) then false is // returned func (m *MenuItem) InsertAfter(item *MenuItem) bool { - // We need to find my parent if m.parent == nil { return false @@ -120,7 +119,6 @@ func (m *MenuItem) InsertAfter(item *MenuItem) bool { // menu. If there is no parent menu (we are a top level menu) then false is // returned func (m *MenuItem) InsertBefore(item *MenuItem) bool { - // We need to find my parent if m.parent == nil { return false @@ -134,8 +132,8 @@ func (m *MenuItem) InsertBefore(item *MenuItem) bool { // in this item's submenu. If we are not a submenu, // then something bad has happened :/ func (m *MenuItem) insertNewItemAfterGivenItem(target *MenuItem, - newItem *MenuItem) bool { - + newItem *MenuItem, +) bool { if !m.isSubMenu() { return false } @@ -154,8 +152,8 @@ func (m *MenuItem) insertNewItemAfterGivenItem(target *MenuItem, // target in this item's submenu. If we are not a submenu, then something bad // has happened :/ func (m *MenuItem) insertNewItemBeforeGivenItem(target *MenuItem, - newItem *MenuItem) bool { - + newItem *MenuItem, +) bool { if !m.isSubMenu() { return false } @@ -176,7 +174,6 @@ func (m *MenuItem) isSubMenu() bool { // getItemIndex returns the index of the given target relative to this menu func (m *MenuItem) getItemIndex(target *MenuItem) int { - // This should only be called on submenus if !m.isSubMenu() { return -1 @@ -196,7 +193,6 @@ func (m *MenuItem) getItemIndex(target *MenuItem) int { // the given index // Credit: https://stackoverflow.com/a/61822301 func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool { - // If index is OOB, return false if index > len(m.SubMenu.Items) { return false diff --git a/v2/pkg/menu/menuroles.go b/v2/pkg/menu/menuroles.go index e6b15b243..bcc0657fc 100644 --- a/v2/pkg/menu/menuroles.go +++ b/v2/pkg/menu/menuroles.go @@ -11,29 +11,29 @@ const ( AppMenuRole Role = 1 EditMenuRole = 2 WindowMenuRole = 3 - //AboutRole Role = "about" - //UndoRole Role = "undo" - //RedoRole Role = "redo" - //CutRole Role = "cut" - //CopyRole Role = "copy" - //PasteRole Role = "paste" - //PasteAndMatchStyleRole Role = "pasteAndMatchStyle" - //SelectAllRole Role = "selectAll" - //DeleteRole Role = "delete" - //MinimizeRole Role = "minimize" - //QuitRole Role = "quit" - //TogglefullscreenRole Role = "togglefullscreen" - //FileMenuRole Role = "fileMenu" - //ViewMenuRole Role = "viewMenu" - //WindowMenuRole Role = "windowMenu" - //HideRole Role = "hide" - //HideOthersRole Role = "hideOthers" - //UnhideRole Role = "unhide" - //FrontRole Role = "front" - //ZoomRole Role = "zoom" - //WindowSubMenuRole Role = "windowSubMenu" - //HelpSubMenuRole Role = "helpSubMenu" - //SeparatorItemRole Role = "separatorItem" + // AboutRole Role = "about" + // UndoRole Role = "undo" + // RedoRole Role = "redo" + // CutRole Role = "cut" + // CopyRole Role = "copy" + // PasteRole Role = "paste" + // PasteAndMatchStyleRole Role = "pasteAndMatchStyle" + // SelectAllRole Role = "selectAll" + // DeleteRole Role = "delete" + // MinimizeRole Role = "minimize" + // QuitRole Role = "quit" + // TogglefullscreenRole Role = "togglefullscreen" + // FileMenuRole Role = "fileMenu" + // ViewMenuRole Role = "viewMenu" + // WindowMenuRole Role = "windowMenu" + // HideRole Role = "hide" + // HideOthersRole Role = "hideOthers" + // UnhideRole Role = "unhide" + // FrontRole Role = "front" + // ZoomRole Role = "zoom" + // WindowSubMenuRole Role = "windowSubMenu" + // HelpSubMenuRole Role = "helpSubMenu" + // SeparatorItemRole Role = "separatorItem" ) /* diff --git a/v2/pkg/menu/styledlabel.go b/v2/pkg/menu/styledlabel.go index 11a0254c9..1e996b971 100644 --- a/v2/pkg/menu/styledlabel.go +++ b/v2/pkg/menu/styledlabel.go @@ -29,24 +29,31 @@ type StyledText struct { func (s *StyledText) Bold() bool { return s.Style&Bold == Bold } + func (s *StyledText) Faint() bool { return s.Style&Faint == Faint } + func (s *StyledText) Italic() bool { return s.Style&Italic == Italic } + func (s *StyledText) Blinking() bool { return s.Style&Blinking == Blinking } + func (s *StyledText) Inversed() bool { return s.Style&Inversed == Inversed } + func (s *StyledText) Invisible() bool { return s.Style&Invisible == Invisible } + func (s *StyledText) Underlined() bool { return s.Style&Underlined == Underlined } + func (s *StyledText) Strikethrough() bool { return s.Style&Strikethrough == Strikethrough } diff --git a/v2/pkg/menu/tray.go b/v2/pkg/menu/tray.go index 7554795ad..c8728f1f7 100644 --- a/v2/pkg/menu/tray.go +++ b/v2/pkg/menu/tray.go @@ -2,7 +2,6 @@ package menu // TrayMenu are the options type TrayMenu struct { - // Label is the text we wish to display in the tray Label string @@ -27,7 +26,7 @@ type TrayMenu struct { Tooltip string // Callback function when menu clicked - //Click Callback `json:"-"` + // Click Callback `json:"-"` // Disabled makes the item unselectable Disabled bool diff --git a/v2/pkg/options/linux/linux.go b/v2/pkg/options/linux/linux.go index 062119cb7..797450c27 100644 --- a/v2/pkg/options/linux/linux.go +++ b/v2/pkg/options/linux/linux.go @@ -28,6 +28,11 @@ type Options struct { // - WebviewGpuPolicyAlways // - WebviewGpuPolicyOnDemand // - WebviewGpuPolicyNever + // + // Due to https://github.com/wailsapp/wails/issues/2977, if options.Linux is nil + // in the call to wails.Run(), WebviewGpuPolicy is set by default to WebviewGpuPolicyNever. + // Client code may override this behavior by passing a non-nil Options and set + // WebviewGpuPolicy as needed. WebviewGpuPolicy WebviewGpuPolicy // ProgramName is used to set the program's name for the window manager via GTK's g_set_prgname(). diff --git a/v2/pkg/options/mac/mac.go b/v2/pkg/options/mac/mac.go index 9838b145b..152145114 100644 --- a/v2/pkg/options/mac/mac.go +++ b/v2/pkg/options/mac/mac.go @@ -18,10 +18,14 @@ type AboutInfo struct { type Options struct { TitleBar *TitleBar Appearance AppearanceType + ContentProtection bool WebviewIsTransparent bool WindowIsTranslucent bool Preferences *Preferences - //ActivationPolicy ActivationPolicy - About *AboutInfo - //URLHandlers map[string]func(string) + DisableZoom bool + // ActivationPolicy ActivationPolicy + About *AboutInfo + OnFileOpen func(filePath string) `json:"-"` + OnUrlOpen func(filePath string) `json:"-"` + // URLHandlers map[string]func(string) } diff --git a/v2/pkg/options/mac/preferences.go b/v2/pkg/options/mac/preferences.go index 2c690e7f7..0749ccb18 100644 --- a/v2/pkg/options/mac/preferences.go +++ b/v2/pkg/options/mac/preferences.go @@ -2,8 +2,10 @@ package mac import "github.com/leaanthony/u" -var Enabled = u.True -var Disabled = u.False +var ( + Enabled = u.True + Disabled = u.False +) // Preferences allows to set webkit preferences type Preferences struct { @@ -13,4 +15,7 @@ type Preferences struct { // A Boolean value that indicates whether to allow people to select or otherwise interact with text. // Set to true by default. TextInteractionEnabled u.Bool + // A Boolean value that indicates whether a web view can display content full screen. + // Set to false by default + FullscreenEnabled u.Bool } diff --git a/v2/pkg/options/mac/titlebar.go b/v2/pkg/options/mac/titlebar.go index c18c4eea8..51e0832ca 100644 --- a/v2/pkg/options/mac/titlebar.go +++ b/v2/pkg/options/mac/titlebar.go @@ -41,7 +41,6 @@ func TitleBarHidden() *TitleBar { // TitleBarHiddenInset results in a hidden title bar with an alternative look where // the traffic light buttons are slightly more inset from the window edge. func TitleBarHiddenInset() *TitleBar { - return &TitleBar{ TitlebarAppearsTransparent: true, HideTitle: true, @@ -50,5 +49,4 @@ func TitleBarHiddenInset() *TitleBar { UseToolbar: true, HideToolbarSeparator: true, } - } diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go index b64befe12..0f62d5e4b 100644 --- a/v2/pkg/options/options.go +++ b/v2/pkg/options/options.go @@ -5,6 +5,8 @@ import ( "html" "io/fs" "net/http" + "os" + "path/filepath" "runtime" "github.com/wailsapp/wails/v2/pkg/options/assetserver" @@ -26,8 +28,7 @@ const ( Fullscreen WindowStartState = 3 ) -type Experimental struct { -} +type Experimental struct{} // App contains options for creating the App type App struct { @@ -62,6 +63,7 @@ type App struct { OnShutdown func(ctx context.Context) `json:"-"` OnBeforeClose func(ctx context.Context) (prevent bool) `json:"-"` Bind []interface{} + EnumBind []interface{} WindowStartState WindowStartState // ErrorFormatter overrides the formatting of errors returned by backend methods @@ -82,6 +84,8 @@ type App struct { // services of Apple and Microsoft. EnableFraudulentWebsiteDetection bool + SingleInstanceLock *SingleInstanceLock + Windows *windows.Options Mac *mac.Options Linux *linux.Options @@ -91,6 +95,15 @@ type App struct { // Debug options for debug builds. These options will be ignored in a production build. Debug Debug + + // DragAndDrop options for drag and drop behavior + DragAndDrop *DragAndDrop + + // DisablePanicRecovery disables the panic recovery system in messages processing + DisablePanicRecovery bool + + // List of additional allowed origins for bindings in format "https://*.myapp.com,https://example.com" + BindingsAllowedOrigins string } type ErrorFormatter func(error) any @@ -146,6 +159,15 @@ func MergeDefaults(appoptions *App) { if appoptions.CSSDragValue == "" { appoptions.CSSDragValue = "drag" } + if appoptions.DragAndDrop == nil { + appoptions.DragAndDrop = &DragAndDrop{} + } + if appoptions.DragAndDrop.CSSDropProperty == "" { + appoptions.DragAndDrop.CSSDropProperty = "--wails-drop-target" + } + if appoptions.DragAndDrop.CSSDropValue == "" { + appoptions.DragAndDrop.CSSDropValue = "drop" + } if appoptions.BackgroundColour == nil { appoptions.BackgroundColour = &RGBA{ R: 255, @@ -165,6 +187,47 @@ func MergeDefaults(appoptions *App) { processDragOptions(appoptions) } +type SingleInstanceLock struct { + // uniqueId that will be used for setting up messaging between instances + UniqueId string + OnSecondInstanceLaunch func(secondInstanceData SecondInstanceData) +} + +type SecondInstanceData struct { + Args []string + WorkingDirectory string +} + +type DragAndDrop struct { + + // EnableFileDrop enables wails' drag and drop functionality that returns the dropped in files' absolute paths. + EnableFileDrop bool + + // Disable webview's drag and drop functionality. + // + // It can be used to prevent accidental file opening of dragged in files in the webview, when there is no need for drag and drop. + DisableWebViewDrop bool + + // CSS property to test for drag and drop target elements. Default "--wails-drop-target" + CSSDropProperty string + + // The CSS Value that the CSSDropProperty must have to be a valid drop target. Default "drop" + CSSDropValue string +} + +func NewSecondInstanceData() (*SecondInstanceData, error) { + ex, err := os.Executable() + if err != nil { + return nil, err + } + workingDirectory := filepath.Dir(ex) + + return &SecondInstanceData{ + Args: os.Args[1:], + WorkingDirectory: workingDirectory, + }, nil +} + func processMenus(appoptions *App) { switch runtime.GOOS { case "darwin": diff --git a/v2/pkg/options/windows/windows.go b/v2/pkg/options/windows/windows.go index 7adf0bf72..1fe351455 100644 --- a/v2/pkg/options/windows/windows.go +++ b/v2/pkg/options/windows/windows.go @@ -35,8 +35,29 @@ const ( Tabbed BackdropType = 4 ) +const ( + // Default is 0, which means no changes to the default Windows DLL search behavior + DLLSearchDefault uint32 = 0 + // LoadLibrary flags for determining from where to search for a DLL + DLLSearchDontResolveDllReferences uint32 = 0x1 // windows.DONT_RESOLVE_DLL_REFERENCES + DLLSearchAsDataFile uint32 = 0x2 // windows.LOAD_LIBRARY_AS_DATAFILE + DLLSearchWithAlteredPath uint32 = 0x8 // windows.LOAD_WITH_ALTERED_SEARCH_PATH + DLLSearchIgnoreCodeAuthzLevel uint32 = 0x10 // windows.LOAD_IGNORE_CODE_AUTHZ_LEVEL + DLLSearchAsImageResource uint32 = 0x20 // windows.LOAD_LIBRARY_AS_IMAGE_RESOURCE + DLLSearchAsDataFileExclusive uint32 = 0x40 // windows.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE + DLLSearchRequireSignedTarget uint32 = 0x80 // windows.LOAD_LIBRARY_REQUIRE_SIGNED_TARGET + DLLSearchDllLoadDir uint32 = 0x100 // windows.LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR + DLLSearchApplicationDir uint32 = 0x200 // windows.LOAD_LIBRARY_SEARCH_APPLICATION_DIR + DLLSearchUserDirs uint32 = 0x400 // windows.LOAD_LIBRARY_SEARCH_USER_DIRS + DLLSearchSystem32 uint32 = 0x800 // windows.LOAD_LIBRARY_SEARCH_SYSTEM32 + DLLSearchDefaultDirs uint32 = 0x1000 // windows.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS + DLLSearchSafeCurrentDirs uint32 = 0x2000 // windows.LOAD_LIBRARY_SAFE_CURRENT_DIRS + DLLSearchSystem32NoForwarder uint32 = 0x4000 // windows.LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER + DLLSearchOsIntegrityContinuity uint32 = 0x8000 // windows.LOAD_LIBRARY_OS_INTEGRITY_CONTINUITY +) + func RGB(r, g, b uint8) int32 { - var col = int32(b) + col := int32(b) col = col<<8 | int32(g) col = col<<8 | int32(r) return col @@ -61,6 +82,7 @@ type ThemeSettings struct { // Options are options specific to Windows type Options struct { + ContentProtection bool WebviewIsTransparent bool WindowIsTranslucent bool DisableWindowIcon bool @@ -68,6 +90,8 @@ type Options struct { IsZoomControlEnabled bool ZoomFactor float64 + DisablePinchZoom bool + // 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. DisableFramelessWindowDecorations bool @@ -116,6 +140,14 @@ type Options struct { // Configure whether swipe gestures should be enabled EnableSwipeGestures bool + + // Class name for the window. If empty, 'wailsWindow' will be used. + WindowClassName string + + // DLLSearchPaths controls which directories are searched when loading DLLs + // Set to 0 for default behavior, or combine multiple flags with bitwise OR + // Example: DLLSearchApplicationDir | DLLSearchSystem32 + DLLSearchPaths uint32 } func DefaultMessages() *Messages { diff --git a/v2/pkg/runtime/dialog.go b/v2/pkg/runtime/dialog.go index d53a89c15..16ae659e1 100644 --- a/v2/pkg/runtime/dialog.go +++ b/v2/pkg/runtime/dialog.go @@ -3,6 +3,7 @@ package runtime import ( "context" "fmt" + "github.com/wailsapp/wails/v2/internal/frontend" "github.com/wailsapp/wails/v2/internal/fs" ) diff --git a/v2/pkg/runtime/draganddrop.go b/v2/pkg/runtime/draganddrop.go new file mode 100644 index 000000000..2db9c773c --- /dev/null +++ b/v2/pkg/runtime/draganddrop.go @@ -0,0 +1,37 @@ +package runtime + +import ( + "context" + "fmt" +) + +// OnFileDrop returns a slice of file path strings when a drop is finished. +func OnFileDrop(ctx context.Context, callback func(x, y int, paths []string)) { + if callback == nil { + LogError(ctx, "OnFileDrop called with a nil callback") + return + } + EventsOn(ctx, "wails:file-drop", func(optionalData ...interface{}) { + if len(optionalData) != 3 { + callback(0, 0, nil) + } + x, ok := optionalData[0].(int) + if !ok { + LogError(ctx, fmt.Sprintf("invalid x coordinate in drag and drop: %v", optionalData[0])) + } + y, ok := optionalData[1].(int) + if !ok { + LogError(ctx, fmt.Sprintf("invalid y coordinate in drag and drop: %v", optionalData[1])) + } + paths, ok := optionalData[2].([]string) + if !ok { + LogError(ctx, fmt.Sprintf("invalid path data in drag and drop: %v", optionalData[2])) + } + callback(x, y, paths) + }) +} + +// OnFileDropOff removes the drag and drop listeners and handlers. +func OnFileDropOff(ctx context.Context) { + EventsOff(ctx, "wails:file-drop") +} diff --git a/v2/pkg/runtime/events.go b/v2/pkg/runtime/events.go index 493d81168..84aff7d74 100644 --- a/v2/pkg/runtime/events.go +++ b/v2/pkg/runtime/events.go @@ -10,7 +10,7 @@ func EventsOn(ctx context.Context, eventName string, callback func(optionalData return events.On(eventName, callback) } -// EventsOff unregisters a listener for the given event name, optionally multiple listeneres can be unregistered via `additionalEventNames` +// EventsOff unregisters a listener for the given event name, optionally multiple listeners can be unregistered via `additionalEventNames` func EventsOff(ctx context.Context, eventName string, additionalEventNames ...string) { events := getEvents(ctx) events.Off(eventName) @@ -22,7 +22,7 @@ func EventsOff(ctx context.Context, eventName string, additionalEventNames ...st } } -// EventsOff unregisters a listener for the given event name, optionally multiple listeneres can be unregistered via `additionalEventNames` +// EventsOff unregisters a listener for the given event name, optionally multiple listeners can be unregistered via `additionalEventNames` func EventsOffAll(ctx context.Context) { events := getEvents(ctx) events.OffAll() diff --git a/v2/pkg/runtime/log.go b/v2/pkg/runtime/log.go index 4d3f56d3f..3c2756f06 100644 --- a/v2/pkg/runtime/log.go +++ b/v2/pkg/runtime/log.go @@ -3,6 +3,7 @@ package runtime import ( "context" "fmt" + "github.com/wailsapp/wails/v2/pkg/logger" ) diff --git a/v2/pkg/runtime/menu.go b/v2/pkg/runtime/menu.go index 176c9bb1d..09bd640c5 100644 --- a/v2/pkg/runtime/menu.go +++ b/v2/pkg/runtime/menu.go @@ -2,6 +2,7 @@ package runtime import ( "context" + "github.com/wailsapp/wails/v2/pkg/menu" ) diff --git a/v2/pkg/runtime/notifications.go b/v2/pkg/runtime/notifications.go new file mode 100644 index 000000000..46ae09fac --- /dev/null +++ b/v2/pkg/runtime/notifications.go @@ -0,0 +1,136 @@ +package runtime + +import ( + "context" + + "github.com/wailsapp/wails/v2/internal/frontend" +) + +// NotificationOptions contains configuration for a notification. +type NotificationOptions = frontend.NotificationOptions + +// NotificationAction represents an action button for a notification. +type NotificationAction = frontend.NotificationAction + +// NotificationCategory groups actions for notifications. +type NotificationCategory = frontend.NotificationCategory + +// NotificationResponse represents the response sent by interacting with a notification. +type NotificationResponse = frontend.NotificationResponse + +// NotificationResult represents the result of a notification response, +// returning the response or any errors that occurred. +type NotificationResult = frontend.NotificationResult + +// InitializeNotifications initializes the notification service for the application. +// This must be called before sending any notifications. On macOS, this also ensures +// the notification delegate is properly initialized. +func InitializeNotifications(ctx context.Context) error { + fe := getFrontend(ctx) + return fe.InitializeNotifications() +} + +// CleanupNotifications cleans up notification resources and releases any held connections. +// This should be called when shutting down the application to properly release resources +// (primarily needed on Linux to close D-Bus connections). +func CleanupNotifications(ctx context.Context) { + fe := getFrontend(ctx) + fe.CleanupNotifications() +} + +// IsNotificationAvailable checks if notifications are available on the current platform. +func IsNotificationAvailable(ctx context.Context) bool { + fe := getFrontend(ctx) + return fe.IsNotificationAvailable() +} + +// RequestNotificationAuthorization requests notification authorization from the user. +// On macOS, this prompts the user to allow notifications. On other platforms, this +// always returns true. Returns true if authorization was granted, false otherwise. +func RequestNotificationAuthorization(ctx context.Context) (bool, error) { + fe := getFrontend(ctx) + return fe.RequestNotificationAuthorization() +} + +// CheckNotificationAuthorization checks the current notification authorization status. +// On macOS, this checks if the app has notification permissions. On other platforms, +// this always returns true. +func CheckNotificationAuthorization(ctx context.Context) (bool, error) { + fe := getFrontend(ctx) + return fe.CheckNotificationAuthorization() +} + +// SendNotification sends a basic notification with the given options. +// The notification will display with the provided title, subtitle (if supported), +// and body text. +func SendNotification(ctx context.Context, options NotificationOptions) error { + fe := getFrontend(ctx) + return fe.SendNotification(options) +} + +// SendNotificationWithActions sends a notification with action buttons. +// A NotificationCategory must be registered first using RegisterNotificationCategory. +// The options.CategoryID must match a previously registered category ID. +// If the category is not found, a basic notification will be sent instead. +func SendNotificationWithActions(ctx context.Context, options NotificationOptions) error { + fe := getFrontend(ctx) + return fe.SendNotificationWithActions(options) +} + +// RegisterNotificationCategory registers a notification category that can be used +// with SendNotificationWithActions. Categories define the action buttons and optional +// reply fields that will appear on notifications. +func RegisterNotificationCategory(ctx context.Context, category NotificationCategory) error { + fe := getFrontend(ctx) + return fe.RegisterNotificationCategory(category) +} + +// RemoveNotificationCategory removes a previously registered notification category. +func RemoveNotificationCategory(ctx context.Context, categoryId string) error { + fe := getFrontend(ctx) + return fe.RemoveNotificationCategory(categoryId) +} + +// RemoveAllPendingNotifications removes all pending notifications from the notification center. +// On Windows, this is a no-op as the platform manages notification lifecycle automatically. +func RemoveAllPendingNotifications(ctx context.Context) error { + fe := getFrontend(ctx) + return fe.RemoveAllPendingNotifications() +} + +// RemovePendingNotification removes a specific pending notification by its identifier. +// On Windows, this is a no-op as the platform manages notification lifecycle automatically. +func RemovePendingNotification(ctx context.Context, identifier string) error { + fe := getFrontend(ctx) + return fe.RemovePendingNotification(identifier) +} + +// RemoveAllDeliveredNotifications removes all delivered notifications from the notification center. +// On Windows, this is a no-op as the platform manages notification lifecycle automatically. +func RemoveAllDeliveredNotifications(ctx context.Context) error { + fe := getFrontend(ctx) + return fe.RemoveAllDeliveredNotifications() +} + +// RemoveDeliveredNotification removes a specific delivered notification by its identifier. +// On Windows, this is a no-op as the platform manages notification lifecycle automatically. +func RemoveDeliveredNotification(ctx context.Context, identifier string) error { + fe := getFrontend(ctx) + return fe.RemoveDeliveredNotification(identifier) +} + +// RemoveNotification removes a notification by its identifier. +// This is a convenience function that works across platforms. On macOS, use the +// more specific RemovePendingNotification or RemoveDeliveredNotification functions. +func RemoveNotification(ctx context.Context, identifier string) error { + fe := getFrontend(ctx) + return fe.RemoveNotification(identifier) +} + +// OnNotificationResponse registers a callback function that will be invoked when +// a user interacts with a notification (e.g., clicks an action button or the notification itself). +// The callback receives a NotificationResult containing the response details or any errors. +func OnNotificationResponse(ctx context.Context, callback func(result NotificationResult)) { + fe := getFrontend(ctx) + fe.OnNotificationResponse(callback) +} diff --git a/v2/pkg/runtime/runtime.go b/v2/pkg/runtime/runtime.go index 4702b439a..6de5ea798 100644 --- a/v2/pkg/runtime/runtime.go +++ b/v2/pkg/runtime/runtime.go @@ -27,6 +27,7 @@ func getFrontend(ctx context.Context) frontend.Frontend { log.Fatalf("cannot call '%s': %s", funcName, contextError) return nil } + func getLogger(ctx context.Context) *logger.Logger { if ctx == nil { pc, _, _, _ := goruntime.Caller(1) diff --git a/v2/pkg/runtime/screen.go b/v2/pkg/runtime/screen.go index d92ed8308..c4d526692 100644 --- a/v2/pkg/runtime/screen.go +++ b/v2/pkg/runtime/screen.go @@ -1,11 +1,14 @@ package runtime -import "context" -import "github.com/wailsapp/wails/v2/internal/frontend" +import ( + "context" + + "github.com/wailsapp/wails/v2/internal/frontend" +) type Screen = frontend.Screen -// ScreenGetAllScreens returns all screens +// ScreenGetAll returns all screens func ScreenGetAll(ctx context.Context) ([]Screen, error) { appFrontend := getFrontend(ctx) return appFrontend.ScreenGetAll() diff --git a/v2/pkg/runtime/signal_linux.go b/v2/pkg/runtime/signal_linux.go new file mode 100644 index 000000000..6a7ed5db3 --- /dev/null +++ b/v2/pkg/runtime/signal_linux.go @@ -0,0 +1,65 @@ +//go:build linux + +package runtime + +/* +#include +#include +#include +#include + +static void fix_signal(int signum) +{ + struct sigaction st; + + if (sigaction(signum, NULL, &st) < 0) { + return; + } + st.sa_flags |= SA_ONSTACK; + sigaction(signum, &st, NULL); +} + +static void fix_all_signals() +{ +#if defined(SIGSEGV) + fix_signal(SIGSEGV); +#endif +#if defined(SIGBUS) + fix_signal(SIGBUS); +#endif +#if defined(SIGFPE) + fix_signal(SIGFPE); +#endif +#if defined(SIGABRT) + fix_signal(SIGABRT); +#endif +} +*/ +import "C" + +// ResetSignalHandlers resets signal handlers to allow panic recovery. +// +// On Linux, WebKit (used for the webview) may install signal handlers without +// the SA_ONSTACK flag, which prevents Go from properly recovering from panics +// caused by nil pointer dereferences or other memory access violations. +// +// Call this function immediately before code that might panic to ensure +// the signal handlers are properly configured for Go's panic recovery mechanism. +// +// Example usage: +// +// go func() { +// defer func() { +// if err := recover(); err != nil { +// log.Printf("Recovered from panic: %v", err) +// } +// }() +// runtime.ResetSignalHandlers() +// // Code that might panic... +// }() +// +// Note: This function only has an effect on Linux. On other platforms, +// it is a no-op. +func ResetSignalHandlers() { + C.fix_all_signals() +} diff --git a/v2/pkg/runtime/signal_other.go b/v2/pkg/runtime/signal_other.go new file mode 100644 index 000000000..3171a700c --- /dev/null +++ b/v2/pkg/runtime/signal_other.go @@ -0,0 +1,18 @@ +//go:build !linux + +package runtime + +// ResetSignalHandlers resets signal handlers to allow panic recovery. +// +// On Linux, WebKit (used for the webview) may install signal handlers without +// the SA_ONSTACK flag, which prevents Go from properly recovering from panics +// caused by nil pointer dereferences or other memory access violations. +// +// Call this function immediately before code that might panic to ensure +// the signal handlers are properly configured for Go's panic recovery mechanism. +// +// Note: This function only has an effect on Linux. On other platforms, +// it is a no-op. +func ResetSignalHandlers() { + // No-op on non-Linux platforms +} diff --git a/v2/pkg/templates/base/go.mod.tmpl b/v2/pkg/templates/base/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/base/go.mod.tmpl +++ b/v2/pkg/templates/base/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/generate/assets/common/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/generate/assets/vue-ts/frontend/READ-THIS.md b/v2/pkg/templates/generate/assets/vue-ts/frontend/READ-THIS.md deleted file mode 100644 index 15b2483d9..000000000 --- a/v2/pkg/templates/generate/assets/vue-ts/frontend/READ-THIS.md +++ /dev/null @@ -1,4 +0,0 @@ -This template uses a work around as the default template does not compile due to this issue: -https://github.com/vuejs/core/issues/1228 - -In `tsconfig.json`, `isolatedModules` is set to `false` rather than `true` to work around the issue. \ No newline at end of file diff --git a/v2/pkg/templates/generate/generate.go b/v2/pkg/templates/generate/generate.go index 3b01e5f2a..6842dc196 100644 --- a/v2/pkg/templates/generate/generate.go +++ b/v2/pkg/templates/generate/generate.go @@ -159,7 +159,6 @@ var templates = []*template{ } func main() { - rebuildRuntime() for _, t := range templates { diff --git a/v2/pkg/templates/generate/plain/go.mod.tmpl b/v2/pkg/templates/generate/plain/go.mod.tmpl index e01fbe9e7..f6d0daec4 100644 --- a/v2/pkg/templates/generate/plain/go.mod.tmpl +++ b/v2/pkg/templates/generate/plain/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates.go b/v2/pkg/templates/templates.go index d982454d0..e18185520 100644 --- a/v2/pkg/templates/templates.go +++ b/v2/pkg/templates/templates.go @@ -72,7 +72,6 @@ type Options struct { // Template holds data relating to a template // including the metadata stored in template.json type Template struct { - // Template details Name string `json:"name"` ShortName string `json:"shortname"` @@ -100,7 +99,6 @@ func parseTemplate(template gofs.FS) (Template, error) { // List returns the list of available templates func List() ([]Template, error) { - // If the cache isn't loaded, load it if templateCache == nil { err := loadTemplateCache() @@ -114,7 +112,6 @@ func List() ([]Template, error) { // getTemplateByShortname returns the template with the given short name func getTemplateByShortname(shortname string) (Template, error) { - var result Template // If the cache isn't loaded, load it @@ -136,7 +133,6 @@ func getTemplateByShortname(shortname string) (Template, error) { // Loads the template cache func loadTemplateCache() error { - templatesFS, err := debme.FS(templates, "templates") if err != nil { return err @@ -190,7 +186,16 @@ func Install(options *Options) (bool, *Template, error) { return false, nil, err } options.TargetDir = targetDir - if !fs.DirExists(options.TargetDir) { + if fs.DirExists(options.TargetDir) { + // Check if directory is non-empty + entries, err := os.ReadDir(options.TargetDir) + if err != nil { + return false, nil, err + } + if len(entries) > 0 { + return false, nil, fmt.Errorf("cannot initialise project in non-empty directory: %s", options.TargetDir) + } + } else { err := fs.Mkdir(options.TargetDir) if err != nil { return false, nil, err @@ -309,11 +314,9 @@ func gitclone(options *Options) (string, error) { _, err = git.PlainClone(dirname, false, cloneOption) return dirname, err - } func generateIDEFiles(options *Options) error { - switch options.IDE { case "vscode": return generateVSCodeFiles(options) @@ -361,7 +364,6 @@ func generateVSCodeFiles(options *Options) error { options: options, } return installIDEFiles(ideoptions) - } func installIDEFiles(o ideOptions) error { diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/lit-ts/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/lit-ts/go.mod.tmpl b/v2/pkg/templates/templates/lit-ts/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/lit-ts/go.mod.tmpl +++ b/v2/pkg/templates/templates/lit-ts/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/lit/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/lit/go.mod.tmpl b/v2/pkg/templates/templates/lit/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/lit/go.mod.tmpl +++ b/v2/pkg/templates/templates/lit/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/plain/go.mod.tmpl b/v2/pkg/templates/templates/plain/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/plain/go.mod.tmpl +++ b/v2/pkg/templates/templates/plain/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/preact-ts/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/preact-ts/go.mod.tmpl b/v2/pkg/templates/templates/preact-ts/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/preact-ts/go.mod.tmpl +++ b/v2/pkg/templates/templates/preact-ts/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/preact/frontend/src/assets/preact.svg b/v2/pkg/templates/templates/preact/frontend/src/assets/preact.svg index 8d4155b1d..23433fcf8 100644 --- a/v2/pkg/templates/templates/preact/frontend/src/assets/preact.svg +++ b/v2/pkg/templates/templates/preact/frontend/src/assets/preact.svg @@ -1,10 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/preact/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/preact/go.mod.tmpl b/v2/pkg/templates/templates/preact/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/preact/go.mod.tmpl +++ b/v2/pkg/templates/templates/preact/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/react-ts/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/react-ts/go.mod.tmpl b/v2/pkg/templates/templates/react-ts/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/react-ts/go.mod.tmpl +++ b/v2/pkg/templates/templates/react-ts/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/react/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/react/go.mod.tmpl b/v2/pkg/templates/templates/react/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/react/go.mod.tmpl +++ b/v2/pkg/templates/templates/react/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/svelte-ts/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/svelte-ts/go.mod.tmpl b/v2/pkg/templates/templates/svelte-ts/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/svelte-ts/go.mod.tmpl +++ b/v2/pkg/templates/templates/svelte-ts/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/svelte/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/svelte/go.mod.tmpl b/v2/pkg/templates/templates/svelte/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/svelte/go.mod.tmpl +++ b/v2/pkg/templates/templates/svelte/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/vanilla-ts/go.mod.tmpl b/v2/pkg/templates/templates/vanilla-ts/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/vanilla-ts/go.mod.tmpl +++ b/v2/pkg/templates/templates/vanilla-ts/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/vanilla/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/vanilla/go.mod.tmpl b/v2/pkg/templates/templates/vanilla/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/vanilla/go.mod.tmpl +++ b/v2/pkg/templates/templates/vanilla/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/vue-ts/frontend/READ-THIS.md b/v2/pkg/templates/templates/vue-ts/frontend/READ-THIS.md deleted file mode 100644 index 15b2483d9..000000000 --- a/v2/pkg/templates/templates/vue-ts/frontend/READ-THIS.md +++ /dev/null @@ -1,4 +0,0 @@ -This template uses a work around as the default template does not compile due to this issue: -https://github.com/vuejs/core/issues/1228 - -In `tsconfig.json`, `isolatedModules` is set to `false` rather than `true` to work around the issue. \ No newline at end of file diff --git a/v2/pkg/templates/templates/vue-ts/frontend/package.json b/v2/pkg/templates/templates/vue-ts/frontend/package.json index c06165c24..e65d0eff4 100644 --- a/v2/pkg/templates/templates/vue-ts/frontend/package.json +++ b/v2/pkg/templates/templates/vue-ts/frontend/package.json @@ -15,7 +15,7 @@ "@vitejs/plugin-vue": "^3.0.3", "typescript": "^4.6.4", "vite": "^3.0.7", - "vue-tsc": "^0.39.5", + "vue-tsc": "^1.8.27", "@babel/types": "^7.18.10" } -} \ No newline at end of file +} diff --git a/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/vue-ts/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/vue-ts/go.mod.tmpl b/v2/pkg/templates/templates/vue-ts/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/vue-ts/go.mod.tmpl +++ b/v2/pkg/templates/templates/vue-ts/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.d.ts b/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.d.ts index e0d662b38..336fb07aa 100644 --- a/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.d.ts +++ b/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.d.ts @@ -52,6 +52,10 @@ export function EventsOnce(eventName: string, callback: (...data: any) => void): // unregisters the listener for the given event name. export function EventsOff(eventName: string): void; +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all event listeners. +export function EventsOffAll(): void; + // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; @@ -126,7 +130,7 @@ export function WindowUnfullscreen(): void; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. -export function WindowSetSize(width: number, height: number): Promise; +export function WindowSetSize(width: number, height: number): void; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. diff --git a/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.js b/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.js index 2c3dafcc3..b5ae16d56 100644 --- a/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.js +++ b/v2/pkg/templates/templates/vue/frontend/wailsjs/runtime/runtime.js @@ -48,6 +48,10 @@ export function EventsOff(eventName) { return window.runtime.EventsOff(eventName); } +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + export function EventsOnce(eventName, callback) { EventsOnMultiple(eventName, callback, 1); } diff --git a/v2/pkg/templates/templates/vue/go.mod.tmpl b/v2/pkg/templates/templates/vue/go.mod.tmpl index dd7184879..4b34d1668 100644 --- a/v2/pkg/templates/templates/vue/go.mod.tmpl +++ b/v2/pkg/templates/templates/vue/go.mod.tmpl @@ -1,6 +1,6 @@ module changeme -go 1.18 +go 1.23.0 require github.com/wailsapp/wails/v2 {{.WailsVersion}} diff --git a/v2/pkg/templates/templates_test.go b/v2/pkg/templates/templates_test.go index 3b906601a..658ecadb6 100644 --- a/v2/pkg/templates/templates_test.go +++ b/v2/pkg/templates/templates_test.go @@ -52,3 +52,48 @@ func TestInstall(t *testing.T) { is2.NoErr(err) } + +func TestInstallFailsInNonEmptyDirectory(t *testing.T) { + is2 := is.New(t) + + // Create a temp directory with a file in it + tempDir, err := os.MkdirTemp("", "wails-test-nonempty-*") + is2.NoErr(err) + defer func() { + _ = os.RemoveAll(tempDir) + }() + + // Create a file in the directory to make it non-empty + err = os.WriteFile(filepath.Join(tempDir, "existing-file.txt"), []byte("test"), 0644) + is2.NoErr(err) + + options := &Options{ + ProjectName: "test", + TemplateName: "vanilla", + TargetDir: tempDir, + } + + _, _, err = Install(options) + is2.True(err != nil) // Should fail + is2.True(err.Error() == "cannot initialise project in non-empty directory: "+tempDir) +} + +func TestInstallSucceedsInEmptyDirectory(t *testing.T) { + is2 := is.New(t) + + // Create an empty temp directory + tempDir, err := os.MkdirTemp("", "wails-test-empty-*") + is2.NoErr(err) + defer func() { + _ = os.RemoveAll(tempDir) + }() + + options := &Options{ + ProjectName: "test", + TemplateName: "vanilla", + TargetDir: tempDir, + } + + _, _, err = Install(options) + is2.NoErr(err) // Should succeed in empty directory +} diff --git a/v2/tools/release/release.go b/v2/tools/release/release.go index dc6299ed7..4178fcc95 100644 --- a/v2/tools/release/release.go +++ b/v2/tools/release/release.go @@ -33,7 +33,7 @@ func updateVersion() string { minorVersion++ vsplit[len(vsplit)-1] = strconv.Itoa(minorVersion) newVersion := strings.Join(vsplit, ".") - err = os.WriteFile(versionFile, []byte(newVersion), 0755) + err = os.WriteFile(versionFile, []byte(newVersion), 0o755) checkError(err) return newVersion } @@ -68,7 +68,7 @@ func main() { newVersion = os.Args[1] currentVersion, err := os.ReadFile(versionFile) checkError(err) - err = os.WriteFile(versionFile, []byte(newVersion), 0755) + err = os.WriteFile(versionFile, []byte(newVersion), 0o755) checkError(err) isPointRelease = IsPointRelease(string(currentVersion), newVersion) } else { @@ -89,7 +89,7 @@ func main() { // Add the new version to the top of the changelog newChangelog := changelogSplit[0] + "## [Unreleased]\n\n## " + newVersion + " - " + today + changelogSplit[1] // Write the changelog back - err = os.WriteFile("src/pages/changelog.mdx", []byte(newChangelog), 0755) + err = os.WriteFile("src/pages/changelog.mdx", []byte(newChangelog), 0o755) checkError(err) if !isPointRelease { @@ -112,7 +112,7 @@ func main() { versions = versions[0 : len(versions)-1] newVersions, err := json.Marshal(&versions) checkError(err) - err = os.WriteFile("versions.json", newVersions, 0755) + err = os.WriteFile("versions.json", newVersions, 0o755) checkError(err) s.ECHO("Removing old version: " + oldestVersion) 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/website/Taskfile.yml b/website/Taskfile.yml index 53b38857a..dbb09105d 100644 --- a/website/Taskfile.yml +++ b/website/Taskfile.yml @@ -8,7 +8,7 @@ tasks: aliases: [i] cmds: - corepack enable - - corepack prepare pnpm@8.2.0 --activate + - corepack prepare pnpm@8.3.1 --activate - pnpm install sources: - package.json diff --git a/website/blog/2025-03-16-security-incident-response.mdx b/website/blog/2025-03-16-security-incident-response.mdx new file mode 100644 index 000000000..e9903c570 --- /dev/null +++ b/website/blog/2025-03-16-security-incident-response.mdx @@ -0,0 +1,89 @@ +--- +slug: security-incident-response-march-2025 +title: Proactive Security Response - GitHub Actions Supply Chain Attack +authors: [leaanthony] +tags: [wails, security] +--- + +
+ Security Shield +
+
+ +:::note TL;DR +**Good news! Wails was NOT affected by this security incident.** Our thorough investigation confirmed that no secrets were leaked, and the Wails codebase and releases remain completely secure. We've already taken proactive measures to further strengthen our security posture. +::: + +## Introduction + +On 15th March 2025 (AEST), the Wails team was alerted to a security incident involving the `tj-actions/changed-files` GitHub Action. This widely-used action (with over 23,000 repositories depending on it) was compromised in a supply chain attack. While this action was used in some of our CI/CD workflows, we're pleased to confirm that Wails remained fully protected throughout. + +This post shares the details of the incident, our response, and the additional safeguards we've implemented to ensure the continued security of the Wails project. + +## Incident Details + +The security company StepSecurity [reported](https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised) that the `tj-actions/changed-files` GitHub Action was compromised beginning around 9:00 AM March 14th, 2025 Pacific Time (4:00 PM UTC). + +In this attack, malicious code was injected into the action that attempted to dump CI/CD secrets from GitHub Actions runner processes into public logs. The attackers modified the action's code and retroactively updated multiple version tags to reference the malicious commit. + +## Our Proactive Assessment + +Upon learning this, we immediately launched a comprehensive assessment of our systems: + +1. We identified the following Wails workflows that were using the action: + - For Wails v2: `pr-v2.yml` and `upload-source-documents.yml` + - For Wails v3: `pr-v3.yml`, `publish-npm.yml`, and `upload-source-documents.yml` + +2. Our security team conducted a thorough review of all workflow logs for the affected actions during the time period of the compromise. + +3. We're happy to confirm that **no secrets were leaked** in any of our workflow logs, and the Wails codebase remained completely secure. + +## Action Taken + +We took immediate steps to address this situation: + +1. We swiftly replaced all instances of the affected `tj-actions/changed-files` action with the secure alternative `step-security/changed-files` provided by StepSecurity. + +2. As an extra precautionary measure, we temporarily removed all secrets from our GitHub Actions workflows. + +## What This Means for You + +We want to reassure our community that: + +1. The Wails codebase was never compromised in any way. +2. No malicious code was introduced into any Wails releases. +3. This situation only potentially affected our CI/CD pipeline, not the actual Wails source code or releases. +4. No sensitive information or secrets were exposed during this time. + +**In short: All Wails releases remain secure and trustworthy, and no action is required on your part.** + +## Strengthening Our Security Posture + +To minimise exposure to similar potential incidents in the future, we're enhancing our security practices by: + +1. Implementing stricter version pinning for all third-party actions used in our workflows, preferably pinning to specific commit hashes rather than version tags. + +2. Establishing a regular security review process for our CI/CD pipelines and dependencies. + +3. Exploring the use of additional security tools like StepSecurity's Harden-Runner to provide enhanced protection for our GitHub Actions workflows. + +4. Developing a more comprehensive security incident response plan to ensure we can respond quickly and effectively to any future security concerns. + +It's worth noting that the Wails project already employs several security tools as part of our development process: + +- **Semgrep**: We use Semgrep for static code analysis to identify potential security vulnerabilities and code quality issues. +- **Snyk**: We employ Snyk to continuously monitor our dependencies for known vulnerabilities and receive alerts when security patches are needed. + +These existing security measures, combined with our enhanced preventative steps, demonstrate our ongoing commitment to maintaining the security and integrity of the Wails project. + +## Moving Forward + +The security of the Wails project and the trust of our community are our highest priorities. We remain committed to transparency and will continue to promptly address any security concerns that arise. + +We would like to thank StepSecurity for their quick response in identifying this issue and providing a secure alternative action. + +If you have any questions or concerns about this, please don't hesitate to reach out to us on [GitHub](https://github.com/wailsapp/wails) or [Discord](https://discord.gg/JDdSxwjhGf). We're always here to help. diff --git a/website/bun.lockb b/website/bun.lockb new file mode 100644 index 000000000..63ed1b159 Binary files /dev/null and b/website/bun.lockb differ diff --git a/website/docs/community/showcase/cfntracker.mdx b/website/docs/community/showcase/cfntracker.mdx new file mode 100644 index 000000000..8fab23b75 --- /dev/null +++ b/website/docs/community/showcase/cfntracker.mdx @@ -0,0 +1,39 @@ +# CFN Tracker + +```mdx-code-block +

+ +
+

+``` + +[CFN Tracker](https://github.com/williamsjokvist/cfn-tracker) - Track any Street +Fighter 6 or V CFN profile's live matches. Check +[the website](https://cfn.williamsjokvist.se/) to get started. + +## Features + +- Real-time match tracking +- Storing match logs and statistics +- Support for displaying live stats to OBS via Browser Source +- Support for both SF6 and SFV +- Ability for users to create their own OBS Browser themes with CSS + +### Major tech used alongside Wails + +- [Task](https://github.com/go-task/task) - wrapping the Wails CLI to make + common commands easy to use +- [React](https://github.com/facebook/react) - chosen for its rich ecosystem + (radix, framer-motion) +- [Bun](https://github.com/oven-sh/bun) - used for its fast dependency + resolution and build-time +- [Rod](https://github.com/go-rod/rod) - headless browser automation for + authentication and polling changes +- [SQLite](https://github.com/mattn/go-sqlite3) - used for storing matches, + sessions and profiles +- [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) - + a http stream to send tracking updates to OBS browser sources +- [i18next](https://github.com/i18next/) - with backend connector to serve + localization objects from the Go layer +- [xstate](https://github.com/statelyai/xstate) - state machines for auth + process and tracking diff --git a/website/docs/community/showcase/clustta.mdx b/website/docs/community/showcase/clustta.mdx new file mode 100644 index 000000000..7da3195df --- /dev/null +++ b/website/docs/community/showcase/clustta.mdx @@ -0,0 +1,27 @@ +--- +title: Clustta +description: File manager and project management tool for creative professionals. +slug: /community/showcase/clustta +image: /img/showcase/clustta.png +--- + +
+ Clustta screenshot +
+ +[Clustta](https://clustta.com) is a file manager and project management tool designed for creative professionals. Built with Wails, it simplifies file management, collaboration, and version control for creative workflows. + +## Features + +- **File Management**: Track all projects and files with easy access even months after completion. +- **Version Control**: Save unlimited revisions with descriptive notes, without duplicating files. +- **Collaboration**: Share files or entire projects securely through simple user tags with fine-grained permissions. +- **Recovery**: Restore corrupted files from saved checkpoints if your software crashes. +- **Templates**: Quick start with preset project and task templates. +- **Kanban Boards**: Visual task tracking to keep tasks organized. +- **Dependencies**: Track and version project resources and dependencies. +- **Checkpoints**: Create memorable milestones and explore alternate creative directions non-destructively. +- **Search & Filters**: Powerful instant search with metadata filtering (task types, tags, status, file extensions). +- **Workspaces**: Save search and filter combinations for easy access to specific task sets. +- **Integrations**: Connect with creative software packages and production management tools like Blender and Kitsu. +- **Self-Hosting**: Host private instances for teams or studios on your own servers. diff --git a/website/docs/community/showcase/espstudio.mdx b/website/docs/community/showcase/espstudio.mdx new file mode 100644 index 000000000..44db858f9 --- /dev/null +++ b/website/docs/community/showcase/espstudio.mdx @@ -0,0 +1,13 @@ +# ESP Studio + +```mdx-code-block +

+ +
+

+``` + +[ESP Studio](https://github.com/torabian/esp-studio) - Cross platform, Desktop, Cloud, and Embedded software +for controlling ESP/Arduino devices, and building complex IOT workflows and control systems diff --git a/website/docs/community/showcase/gamestacker.mdx b/website/docs/community/showcase/gamestacker.mdx new file mode 100644 index 000000000..46245e146 --- /dev/null +++ b/website/docs/community/showcase/gamestacker.mdx @@ -0,0 +1,19 @@ +--- +title: GameStacker +description: A modern, console-like interface that unifies your game libraries. +slug: /community/showcase/gamestacker +image: /img/showcase/gamestacker.webp +--- + +
+ GameStacker main dashboard +
+ +[GameStacker](https://gamestacker.io) is a modern, elegant console-like interface that unifies your entire game collection into one beautiful dashboard. + +## Features + +- **Unified Library**: Automatically detects and imports games from external libraries (such as Steam, LaunchBox, and RetroBat), bringing your modern and retro collections into a single, cohesive interface. +- **True Console Experience**: Built for controllers with responsive navigation, UI sound effects, and a dedicated in-game "Guide" overlay to control music and manage your session without alt-tabbing. +- **Achievement Integration**: Tracks your progress across supported services, allowing you to view unlocks and Gamerscore directly from the dashboard. +- **Multi-Profile Support**: Create unique profiles with custom avatars and specific color themes for a personalized experience. diff --git a/website/docs/community/showcase/grpcmd-gui.mdx b/website/docs/community/showcase/grpcmd-gui.mdx new file mode 100644 index 000000000..891350290 --- /dev/null +++ b/website/docs/community/showcase/grpcmd-gui.mdx @@ -0,0 +1,10 @@ +# grpcmd-gui + +```mdx-code-block +

+ +
+

+``` + +[grpcmd-gui](https://grpc.md/gui) is a modern cross-platform desktop app and API client for gRPC development and testing. diff --git a/website/docs/community/showcase/kafka-king.mdx b/website/docs/community/showcase/kafka-king.mdx new file mode 100644 index 000000000..0ba78a6ad --- /dev/null +++ b/website/docs/community/showcase/kafka-king.mdx @@ -0,0 +1,22 @@ +# Kafka-King + +```mdx-code-block +

+ +
+

+``` + +[Kafka-King](https://github.com/Bronya0/Kafka-King) is a kafka GUI client that supports various systems and is compact and easy to use. +This is made of Wails+vue3 + +# Kafka-King function list +- [x] View the cluster node list, support dynamic configuration of broker and topic configuration items +- [x] Supports consumer clients, consumes the specified topic, size, and timeout according to the specified group, and displays the message information in various dimensions in a table +- [x] Supports PLAIN, SSL, SASL, kerberos, sasl_plaintext, etc. etc. +- [x] Create topics (support batches), delete topics, specify replicas, partitions +- [x] Support statistics of the total number of messages, total number of submissions, and backlog for each topic based on consumer groups +- [x] Support viewing topics Detailed information (offset) of the partition, and support adding additional partitions +- [x] Support simulated producers, batch sending messages, specify headers, partitions +- [x] Health check +- [x] Support viewing consumer groups , Consumer- …… diff --git a/website/docs/community/showcase/marasi.mdx b/website/docs/community/showcase/marasi.mdx new file mode 100644 index 000000000..5ff137523 --- /dev/null +++ b/website/docs/community/showcase/marasi.mdx @@ -0,0 +1,22 @@ +# Marasi + +```mdx-code-block +

+ +
+

+``` + +[Marasi](https://marasi.app/) is an open source application security testing proxy, it lets you intercept, inspect, modify, and extend requests as they flow through your applications. Read more about it on the [blog](https://marasi.app/blog/2025/introducing_marasi/). + +## Features + +- Desktop GUI Interface: Cross-platform desktop application built with Wails +- HTTP/HTTPS Proxy: TLS-capable proxy server with certificate management +- Request/Response Interception: Modify traffic in real-time with an intuitive interface +- Lua Extensions: Scriptable proxy behavior with built-in extensions +- Project Management: SQLite-based storage for all proxy data (requests, responses, metadata) +- Launchpad: Replay and modify HTTP requests +- Scope Management: Filter traffic with inclusion/exclusion rules +- Waypoints: Override hostnames for request routing +- Chrome Integration: Auto-configure Chrome with proxy settings \ No newline at end of file diff --git a/website/docs/community/showcase/mchat.mdx b/website/docs/community/showcase/mchat.mdx new file mode 100644 index 000000000..aa535a6f8 --- /dev/null +++ b/website/docs/community/showcase/mchat.mdx @@ -0,0 +1,10 @@ +# Mchat + +```mdx-code-block +

+ +
+

+``` + +[Official page](https://marcio199226.github.io/mchat-site/public/) Fully anonymous end2end encrypted chat. diff --git a/website/docs/community/showcase/minesweeper-xp.mdx b/website/docs/community/showcase/minesweeper-xp.mdx new file mode 100644 index 000000000..f127a005f --- /dev/null +++ b/website/docs/community/showcase/minesweeper-xp.mdx @@ -0,0 +1,10 @@ +# Minesweeper XP + +```mdx-code-block +

+ +
+

+``` + +[Minesweeper-XP](https://git.new/Minesweeper-XP) allows you to experience the classic Minesweeper XP (+ 98 and 3.1) on macOS, Windows, and Linux! diff --git a/website/docs/community/showcase/resizem.mdx b/website/docs/community/showcase/resizem.mdx new file mode 100644 index 000000000..27f168f48 --- /dev/null +++ b/website/docs/community/showcase/resizem.mdx @@ -0,0 +1,10 @@ +# Resizem + +```mdx-code-block +

+ +
+

+``` + +[Resizem](https://github.com/barats/resizem) - is an app designed for bulk image process. It is particularly useful for users who need to resize, convert, and manage large numbers of image files at once. diff --git a/website/docs/community/showcase/snippetexpander.mdx b/website/docs/community/showcase/snippetexpander.mdx new file mode 100644 index 000000000..1f9fb6157 --- /dev/null +++ b/website/docs/community/showcase/snippetexpander.mdx @@ -0,0 +1,27 @@ +# Snippet Expander + +```mdx-code-block +

+ +
+ Screenshot of Snippet Expander's Select Snippet window +

+

+ +
+ Screenshot of Snippet Expander's Add Snippet screen +

+

+ +
+ Screenshot of Snippet Expander's Search & Paste window +

+``` + +[Snippet Expander](https://snippetexpander.org) is "Your little expandable text snippets helper", for Linux. + +Snippet Expander comprises of a GUI application built with Wails for managing snippets and settings, with a Search & Paste window mode for quickly selecting and pasting a snippet. + +The Wails based GUI, go-lang CLI and vala-lang auto expander daemon all communicate with a go-lang daemon via D-Bus. The daemon does the majority of the work, managing the database of snippets and common settings, and providing services for expanding and pasting snippets etc. + +Check out the [source code](https://git.sr.ht/~ianmjones/snippetexpander/tree/trunk/item/cmd/snippetexpandergui/app.go#L38) to see how the Wails app sends messages from the UI to the backend that are then sent to the daemon, and subscribes to a D-Bus event to monitor changes to snippets via another instance of the app or CLI and show them instantly in the UI via a Wails event. diff --git a/website/docs/community/showcase/tinyrdm.mdx b/website/docs/community/showcase/tinyrdm.mdx new file mode 100644 index 000000000..e3124bab7 --- /dev/null +++ b/website/docs/community/showcase/tinyrdm.mdx @@ -0,0 +1,11 @@ +# Tiny RDM + +```mdx-code-block +

+ + +
+

+``` + +The [Tiny RDM](https://redis.tinycraft.cc/) application is an open-source, modern lightweight Redis GUI. It has a beautful UI, intuitive Redis database management, and compatible with Windows, Mac, and Linux. It provides visual key-value data operations, supports various data decoding and viewing options, built-in console for executing commands, slow log queries and more. diff --git a/website/docs/community/showcase/upbeat.mdx b/website/docs/community/showcase/upbeat.mdx new file mode 100644 index 000000000..2f85b6cce --- /dev/null +++ b/website/docs/community/showcase/upbeat.mdx @@ -0,0 +1,18 @@ +--- +title: UpBeat +description: An RSS/Atom Feed Reader that filters out negative news with locally run ML models. +slug: /community/showcase/upbeat +image: /img/showcase/upbeat.png +--- + +
+ UpBeat screenshot +
+ +[UpBeat](https://upbeat.mitchelltechnologies.co.uk) is An RSS/Atom Feed Reader for macOS that filters out negative news with locally running ML models. + +## Features + +- **Local ML**: UpBeat runs a transformer-based sentiment analysis model directly on your Mac's hardware. No waiting for a Cloud GPU to spin up before you can decide what you want to read +- **Apple Neural Engine Enabled**: UpBeat runs its ML models directly on the Neural Engine of Macs. That means you get super-fast inference, but without burning through your battery or electricity bill. +- **Widely Compatible**: Works with the vast majority of RSS + Atom versions. diff --git a/website/docs/community/showcase/wailsterm.mdx b/website/docs/community/showcase/wailsterm.mdx new file mode 100644 index 000000000..9924dace5 --- /dev/null +++ b/website/docs/community/showcase/wailsterm.mdx @@ -0,0 +1,10 @@ +# WailsTerm + +```mdx-code-block +

+ +
+

+``` + +[WailsTerm](https://github.com/rlshukhov/wailsterm) is a simple translucent terminal app powered by Wails and Xterm.js. diff --git a/website/docs/community/templates.mdx b/website/docs/community/templates.mdx index 6ff8344de..3b020b60b 100644 --- a/website/docs/community/templates.mdx +++ b/website/docs/community/templates.mdx @@ -24,11 +24,11 @@ If you are unsure about a template, inspect `package.json` and `wails.json` for ## Vue - [wails-template-vue](https://github.com/misitebao/wails-template-vue) - Wails template based on Vue ecology (Integrated TypeScript, Dark theme, Internationalization, Single page routing, TailwindCSS) -- [wails-vite-vue-ts](https://github.com/codydbentley/wails-vite-vue-ts) - Vue 3 TypeScript with Vite (and instructions to add features) -- [wails-vite-vue-the-works](https://github.com/codydbentley/wails-vite-vue-the-works) - Vue 3 TypeScript with Vite, Vuex, Vue Router, Sass, and ESLint + Prettier - [wails-template-quasar-js](https://github.com/sgosiaco/wails-template-quasar-js) - A template using JavaScript + Quasar V2 (Vue 3, Vite, Sass, Pinia, ESLint, Prettier) - [wails-template-quasar-ts](https://github.com/sgosiaco/wails-template-quasar-ts) - A template using TypeScript + Quasar V2 (Vue 3, Vite, Sass, Pinia, ESLint, Prettier, Composition API with <script setup>) - [wails-template-naive](https://github.com/tk103331/wails-template-naive) - Wails template based on Naive UI (A Vue 3 Component Library) +- [wails-template-primevue-sakai](https://github.com/TekWizely/wails-template-primevue-sakai) - Wails starter using [PrimeVue's Sakai Application Template](https://sakai.primevue.org) (Vite, Vue, PrimeVue, TailwindCSS, Vue Router, Themes, Dark Mode, UI Components, and more) +- [wails-template-tdesign-js](https://github.com/tongque0/wails-template-tdesign-js) - Wails template based on TDesign UI (a Vue 3 UI library by Tencent), using Vite, Pinia, Vue Router, ESLint, and Prettier. ## Angular @@ -40,16 +40,22 @@ If you are unsure about a template, inspect `package.json` and `wails.json` for - [wails-react-template](https://github.com/AlienRecall/wails-react-template) - A template using reactjs - [wails-react-template](https://github.com/flin7/wails-react-template) - A minimal template for React that supports live development - [wails-template-nextjs](https://github.com/LGiki/wails-template-nextjs) - A template using Next.js and TypeScript +- [wails-template-nextjs-app-router](https://github.com/thisisvk-in/wails-template-nextjs-app-router) - A template using Next.js and TypeScript with App router - [wails-vite-react-ts-tailwind-template](https://github.com/hotafrika/wails-vite-react-ts-tailwind-template) - A template for React + TypeScript + Vite + TailwindCSS +- [Wails-vite-ts-tailwindcss-shadcn-template-2025](https://github.com/darkb0ts/Wails-vite-ts-tailwindcss-shadcn-template-2025) - A template for React + TypeScript + Vite - [wails-vite-react-ts-tailwind-shadcnui-template](https://github.com/Mahcks/wails-vite-react-tailwind-shadcnui-ts) - A template with Vite, React, TypeScript, TailwindCSS, and shadcn/ui +- [wails-nextjs-tailwind-template](https://github.com/kairo913/wails-nextjs-tailwind-template) - A template using Next.js and Typescript with TailwindCSS ## Svelte - [wails-svelte-template](https://github.com/raitonoberu/wails-svelte-template) - A template using Svelte - [wails-vite-svelte-template](https://github.com/BillBuilt/wails-vite-svelte-template) - A template using Svelte and Vite +- [wails-vite-svelte-ts-tailwind-template](https://github.com/xvertile/wails-vite-svelte-tailwind-template) - A template using Wails, Svelte, Vite, TypeScript, and TailwindCSS v3 - [wails-vite-svelte-tailwind-template](https://github.com/BillBuilt/wails-vite-svelte-tailwind-template) - A template using Svelte and Vite with TailwindCSS v3 - [wails-svelte-tailwind-vite-template](https://github.com/PylotLight/wails-vite-svelte-tailwind-template/tree/master) - An updated template using Svelte v4.2.0 and Vite with TailwindCSS v3.3.3 - [wails-sveltekit-template](https://github.com/h8gi/wails-sveltekit-template) - A template using SvelteKit +- [wails-template-sveltekit-less-prettier-eslint](https://github.com/Alex6357/wails-template-sveltekit-less-prettier-eslint) - A template using SvelteKit with less, Prettier and ESlint +- [wails-template-svelte-ts-less-prettier-eslint-vite](https://github.com/Alex6357/wails-template-svelte-ts-less-prettier-eslint-vite) - A template using Svelte5 + TypeScript + less + Prettier + ESlint + Vite ## Solid @@ -61,6 +67,16 @@ If you are unsure about a template, inspect `package.json` and `wails.json` for - [wails-elm-template](https://github.com/benjamin-thomas/wails-elm-template) - Develop your GUI app with functional programming and a **snappy** hot-reload setup :tada: :rocket: - [wails-template-elm-tailwind](https://github.com/rnice01/wails-template-elm-tailwind) - Combine the powers :muscle: of Elm + Tailwind CSS + Wails! Hot reloading supported. +## HTMX + +- [wails-htmx-tailwind-daisyui-template](https://github.com/ltcovalt/wails-htmx-tailwind-daisyui-template) - HTMX template using Tailwind CSS + daisyUI for styling and the Go standard library for routing and HTML templating +- [wails-htmx-templ-chi-tailwind](https://github.com/PylotLight/wails-hmtx-templ-template) - Use a unique combination of pure htmx for interactivity plus templ for creating components and forms + ## Pure JavaScript (Vanilla) - [wails-pure-js-template](https://github.com/KiddoV/wails-pure-js-template) - A template with nothing but just basic JavaScript, HTML, and CSS + + +## Lit (web components) + +- [wails-lit-shoelace-esbuild-template](https://github.com/Braincompiler/wails-lit-shoelace-esbuild-template) - Wails template providing frontend with lit, Shoelace component library + pre-configured prettier and typescript. diff --git a/website/docs/gettingstarted/building.mdx b/website/docs/gettingstarted/building.mdx index 2668b19da..c4c16ea48 100644 --- a/website/docs/gettingstarted/building.mdx +++ b/website/docs/gettingstarted/building.mdx @@ -7,6 +7,10 @@ sidebar_position: 6 From the project directory, run `wails build`. This will compile your project and save the production-ready binary in the `build/bin` directory. +:::info Linux +If you are using a Linux distribution that does not have webkit2gtk-4.0 (such as Ubuntu 24.04), you will need to add `-tags webkit2_41`. +::: + If you run the binary, you should see the default application: ```mdx-code-block diff --git a/website/docs/gettingstarted/firstproject.mdx b/website/docs/gettingstarted/firstproject.mdx index e8880660d..5cf4dff58 100644 --- a/website/docs/gettingstarted/firstproject.mdx +++ b/website/docs/gettingstarted/firstproject.mdx @@ -128,5 +128,3 @@ The `frontend` directory has nothing specific to Wails and can be any frontend p The `build` directory is used during the build process. These files may be updated to customise your builds. If files are removed from the build directory, default versions will be regenerated. - -The default module name in `go.mod` is "changeme". You should change this to something more appropriate. diff --git a/website/docs/gettingstarted/installation.mdx b/website/docs/gettingstarted/installation.mdx index 331ca5062..6189c6d83 100644 --- a/website/docs/gettingstarted/installation.mdx +++ b/website/docs/gettingstarted/installation.mdx @@ -7,7 +7,7 @@ sidebar_position: 1 ## Supported Platforms - Windows 10/11 AMD64/ARM64 -- MacOS 10.13+ AMD64 +- MacOS 10.15+ AMD64 for development, MacOS 10.13+ for release - MacOS 11.0+ ARM64 - Linux AMD64/ARM64 @@ -15,7 +15,7 @@ sidebar_position: 1 Wails has a number of common dependencies that are required before installation: -- Go 1.18+ +- Go 1.21+ (macOS 15+ requires Go 1.23.3+) - NPM (Node 15+) ### Go @@ -58,6 +58,14 @@ import TabItem from "@theme/TabItem"; Linux requires the standard gcc build tools plus libgtk3 and libwebkit. Rather than list a ton of commands for different distros, Wails can try to determine what the installation commands are for your specific distribution. Run wails doctor after installation to be shown how to install the dependencies. If your distro/package manager is not supported, please consult the Add Linux Distro guide. +
Note:
+ If you are using latest Linux version (example: Ubuntu 24.04) and it is not supporting libwebkit2gtk-4.0-dev, then you might encounter an issue in wails doctor: libwebkit not found. To resolve this issue you can install libwebkit2gtk-4.1-dev and during your build use the tag -tags webkit2_41. +

+ After installing Wails via Go, ensure you run the following commands to update your PATH: +
+ export PATH=$PATH:$(go env GOPATH)/bin +
+ source ~/.bashrc or source ~/.zshrc
``` @@ -76,7 +84,9 @@ Note: If you get an error similar to this: ```shell ....\Go\pkg\mod\github.com\wailsapp\wails\v2@v2.1.0\pkg\templates\templates.go:28:12: pattern all:ides/*: no matching files found ``` + please check you have Go 1.18+ installed: + ```shell go version ``` diff --git a/website/docs/guides/application-development.mdx b/website/docs/guides/application-development.mdx index 9d04fe917..adefa4b04 100644 --- a/website/docs/guides/application-development.mdx +++ b/website/docs/guides/application-development.mdx @@ -10,6 +10,10 @@ The pattern used by the default templates are that `main.go` is used for configu The `app.go` file will define a struct that has 2 methods which act as hooks into the main application: ```go title="app.go" +import ( + "context" +) + type App struct { ctx context.Context } @@ -28,7 +32,7 @@ func (a *App) shutdown(ctx context.Context) { - The startup method is called as soon as Wails allocates the resources it needs and is a good place for creating resources, setting up event listeners and anything else the application needs at startup. - It is given a `context.Context` which is usually saved in a struct field. This context is needed for calling the + It is given a [`context.Context`](https://pkg.go.dev/context) which is usually saved in a struct field. This context is needed for calling the [runtime](../reference/runtime/intro.mdx). If this method returns an error, the application will terminate. In dev mode, the error will be output to the console. @@ -55,7 +59,6 @@ func main() { log.Fatal(err) } } - ``` More information on application lifecycle hooks can be found [here](../howdoesitwork.mdx#application-lifecycle-callbacks). @@ -65,7 +68,12 @@ More information on application lifecycle hooks can be found [here](../howdoesit It is likely that you will want to call Go methods from the frontend. This is normally done by adding public methods to the already defined struct in `app.go`: -```go {16-18} title="app.go" +```go {3,21-23} title="app.go" +import ( + "context" + "fmt" +) + type App struct { ctx context.Context } @@ -82,7 +90,7 @@ func (a *App) shutdown(ctx context.Context) { } func (a *App) Greet(name string) string { - return fmt.Sprintf("Hello %s!", name) + return fmt.Sprintf("Hello %s!", name) } ``` @@ -99,15 +107,14 @@ func main() { Height: 600, OnStartup: app.startup, OnShutdown: app.shutdown, - Bind: []interface{}{ - app, - }, + Bind: []interface{}{ + app, + }, }) if err != nil { log.Fatal(err) } } - ``` This will bind all public methods in our `App` struct (it will never bind the startup and shutdown methods). @@ -133,10 +140,10 @@ func main() { otherStruct.SetContext(ctx) }, OnShutdown: app.shutdown, - Bind: []interface{}{ - app, + Bind: []interface{}{ + app, otherStruct - }, + }, }) if err != nil { log.Fatal(err) @@ -144,6 +151,64 @@ func main() { } ``` +Also you might want to use Enums in your structs and have models for them on frontend. +In that case you should create array that will contain all possible enum values, instrument enum type and bind it to the app: + +```go {16-18} title="app.go" +type Weekday string + +const ( + Sunday Weekday = "Sunday" + Monday Weekday = "Monday" + Tuesday Weekday = "Tuesday" + Wednesday Weekday = "Wednesday" + Thursday Weekday = "Thursday" + Friday Weekday = "Friday" + Saturday Weekday = "Saturday" +) + +var AllWeekdays = []struct { + Value Weekday + TSName string +}{ + {Sunday, "SUNDAY"}, + {Monday, "MONDAY"}, + {Tuesday, "TUESDAY"}, + {Wednesday, "WEDNESDAY"}, + {Thursday, "THURSDAY"}, + {Friday, "FRIDAY"}, + {Saturday, "SATURDAY"}, +} +``` + +In the main application configuration, the `EnumBind` key is where we can tell Wails what we want to bind enums as well: + +```go {11-13} title="main.go" +func main() { + + app := NewApp() + + err := wails.Run(&options.App{ + Title: "My App", + Width: 800, + Height: 600, + OnStartup: app.startup, + OnShutdown: app.shutdown, + Bind: []interface{}{ + app, + }, + EnumBind: []interface{}{ + AllWeekdays, + }, + }) + if err != nil { + log.Fatal(err) + } +} +``` + +This will add missing enums to your `model.ts` file. + More information on Binding can be found [here](../howdoesitwork.mdx#method-binding). ## Application Menu @@ -164,15 +229,14 @@ func main() { OnStartup: app.startup, OnShutdown: app.shutdown, Menu: app.menu(), - Bind: []interface{}{ - app, - }, + Bind: []interface{}{ + app, + }, }) if err != nil { log.Fatal(err) } } - ``` ## Assets @@ -199,7 +263,7 @@ create files on the fly or process POST/PUT requests. GET requests are always first handled by the `assets` FS. If the FS doesn't find the requested file the request will be forwarded to the `http.Handler` for serving. Any requests other than GET will be directly processed by the `AssetsHandler` if specified. -It's also possible to only use the `AssetsHandler` by specifiy `nil` as the `Assets` option. +It's also possible to only use the `AssetsHandler` by specifying `nil` as the `Assets` option. ## Built in Dev Server diff --git a/website/docs/guides/crossplatform-build.mdx b/website/docs/guides/crossplatform-build.mdx index a9afc6161..f6fbc0f06 100644 --- a/website/docs/guides/crossplatform-build.mdx +++ b/website/docs/guides/crossplatform-build.mdx @@ -60,6 +60,6 @@ jobs: This example offers opportunities for various enhancements, including: - Caching dependencies - Code signing -- Uploading to platforms like S3, Supbase, etc. +- Uploading to platforms like S3, Supabase, etc. - Injecting secrets as environment variables - Utilizing environment variables as build variables (such as version variable extracted from the current Git tag) diff --git a/website/docs/guides/custom-protocol-schemes.mdx b/website/docs/guides/custom-protocol-schemes.mdx new file mode 100644 index 000000000..216fb7100 --- /dev/null +++ b/website/docs/guides/custom-protocol-schemes.mdx @@ -0,0 +1,207 @@ +# Custom Protocol Scheme association + +Custom Protocols feature allows you to associate specific custom protocol with your app so that when users open links with this protocol, +your app is launched to handle them. This can be particularly useful to connect your desktop app with your web app. +In this guide, we'll walk through the steps to implement custom protocols in Wails app. + + +## Set Up Custom Protocol Schemes Association: +To set up custom protocol, you need to modify your application's wails.json file. +In "info" section add a "protocols" section specifying the protocols your app should be associated with. + +For example: +```json +{ + "info": { + "protocols": [ + { + "scheme": "myapp", + "description": "My App Protocol", + "role": "Editor" + } + ] + } +} +``` + +| Property | Description | +|:------------|:--------------------------------------------------------------------------------------| +| scheme | Custom Protocol scheme. e.g. myapp | +| description | Windows-only. The description. | +| role | macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole. | + +## Platform Specifics: + +### macOS +When you open custom protocol with your app, the system will launch your app and call the `OnUrlOpen` function in your Wails app. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + Mac: &mac.Options{ + OnUrlOpen: func(url string) { println(url) }, + }, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} +``` + +If you want to handle universal links as well, follow this [guide](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) to add required entitlements, add required keys to Info.plist and configure `apple-app-site-association` on your website. + +Here is example for Info.plist: +```xml +NSUserActivityTypes + + NSUserActivityTypeBrowsingWeb + +``` + +And for entitlements.plist +```xml +com.apple.developer.associated-domains + + applinks:myawesomeapp.com + +``` + + +### Windows +On Windows Custom Protocol Schemes is supported only with NSIS installer. During installation, the installer will create a +registry entry for your schemes. When you open url with your app, new instance of app is launched and url is passed +as argument to your app. To handle this you should parse command line arguments in your app. Example: +```go title="main.go" +func main() { + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) != 0 { + println("launchArgs", argsWithoutProg) + } +} +``` + +You also can enable single instance lock for your app. In this case, when you open url with your app, new instance of app is not launched +and arguments are passed to already running instance. Check single instance lock guide for details. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` + +### Linux +Currently, Wails doesn't support bundling for Linux. So, you need to create file associations manually. +For example if you distribute your app as a .deb package, you can create file associations by adding required files in you bundle. +You can use [nfpm](https://nfpm.goreleaser.com/) to create .deb package for your app. + +1. Create a .desktop file for your app and specify file associations there (note that `%u` is important in Exec). Example: +```ini +[Desktop Entry] +Categories=Office +Exec=/usr/bin/wails-open-file %u +Icon=wails-open-file.png +Name=wails-open-file +Terminal=false +Type=Application +MimeType=x-scheme-handler/myapp; +``` + +2. Prepare postInstall/postRemove scripts for your package. Example: +```sh +# reload desktop database to load app in list of available +update-desktop-database /usr/share/applications +``` +3. Configure nfpm to use your scripts and files. Example: +```yaml +name: "wails-open-file" +arch: "arm64" +platform: "linux" +version: "1.0.0" +section: "default" +priority: "extra" +maintainer: "FooBarCorp " +description: "Sample Package" +vendor: "FooBarCorp" +homepage: "http://example.com" +license: "MIT" +contents: +- src: ../bin/wails-open-file + dst: /usr/bin/wails-open-file +- src: ./main.desktop + dst: /usr/share/applications/wails-open-file.desktop +- src: ../appicon.svg + dst: /usr/share/icons/hicolor/scalable/apps/wails-open-file.svg +# copy icons to Yaru theme as well. For some reason Ubuntu didn't pick up fileicons from hicolor theme +- src: ../appicon.svg + dst: /usr/share/icons/Yaru/scalable/apps/wails-open-file.svg +scripts: + postinstall: ./postInstall.sh + postremove: ./postRemove.sh +``` +6. Build your .deb package using nfpm: +```sh +nfpm pkg --packager deb --target . +``` +7. Now when your package is installed, your app will be associated with custom protocol scheme. When you open url with your app, +new instance of app is launched and file path is passed as argument to your app. +To handle this you should parse command line arguments in your app. Example: +```go title="main.go" +func main() { + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) != 0 { + println("launchArgs", argsWithoutProg) + } +} +``` + +You also can enable single instance lock for your app. In this case, when you open url with your app, new instance of app is not launched +and arguments are passed to already running instance. Check single instance lock guide for details. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` diff --git a/website/docs/guides/dynamic-assets.mdx b/website/docs/guides/dynamic-assets.mdx index 0516fb729..8d1debcef 100644 --- a/website/docs/guides/dynamic-assets.mdx +++ b/website/docs/guides/dynamic-assets.mdx @@ -1,5 +1,14 @@ # Dynamic Assets +:::info + +This does not work with vite v5.0.0+ and wails v2 due to changes in vite. +Changes are planned in v3 to support similar functionality under vite v5.0.0+. +If you need this feature, stay with vite v4.0.0+. +See [issue 3240](https://github.com/wailsapp/wails/issues/3240) for details + +::: + If you want to load or generate assets for your frontend dynamically, you can achieve that using the [AssetsHandler](../reference/options#assetshandler) option. The AssetsHandler is a generic `http.Handler` which will be called for any non GET request on the assets server and for GET requests which can not be served from the diff --git a/website/docs/guides/file-association.mdx b/website/docs/guides/file-association.mdx new file mode 100644 index 000000000..71bbff37e --- /dev/null +++ b/website/docs/guides/file-association.mdx @@ -0,0 +1,228 @@ +# File Association + +File association feature allows you to associate specific file types with your app so that when users open those files, +your app is launched to handle them. This can be particularly useful for text editors, image viewers, or any application +that works with specific file formats. In this guide, we'll walk through the steps to implement file association in Wails app. + + +## Set Up File Association: +To set up file association, you need to modify your application's wails.json file. +In "info" section add a "fileAssociations" section specifying the file types your app should be associated with. + +For example: +```json +{ + "info": { + "fileAssociations": [ + { + "ext": "wails", + "name": "Wails", + "description": "Wails Application File", + "iconName": "wailsFileIcon", + "role": "Editor" + }, + { + "ext": "jpg", + "name": "JPEG", + "description": "Image File", + "iconName": "jpegFileIcon", + "role": "Editor" + } + ] + } +} +``` + +| Property | Description | +|:------------|:---------------------------------------------------------------------------------------------------------------------------------------------------| +| ext | The extension (minus the leading period). e.g. png | +| name | The name. e.g. PNG File | +| iconName | The icon name without extension. Icons should be located in build folder. Proper icons will be generated from .png file for both macOS and Windows | +| description | Windows-only. The description. It is displayed on the `Type` column on Windows Explorer. | +| role | macOS-only. The app’s role with respect to the type. Corresponds to CFBundleTypeRole. | + +## Platform Specifics: + +### macOS +When you open file (or files) with your app, the system will launch your app and call the `OnFileOpen` function in your Wails app. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + Mac: &mac.Options{ + OnFileOpen: func(filePaths []string) { println(filestring) }, + }, + Bind: []interface{}{ + app, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} +``` + + +### Windows +On Windows file association is supported only with NSIS installer. During installation, the installer will create a +registry entry for your file associations. When you open file with your app, new instance of app is launched and file path is passed +as argument to your app. To handle this you should parse command line arguments in your app. Example: +```go title="main.go" +func main() { + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) != 0 { + println("launchArgs", argsWithoutProg) + } +} +``` + +You also can enable single instance lock for your app. In this case, when you open file with your app, new instance of app is not launched +and arguments are passed to already running instance. Check single instance lock guide for details. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` + +### Linux +Currently, Wails doesn't support bundling for Linux. So, you need to create file associations manually. +For example if you distribute your app as a .deb package, you can create file associations by adding required files in you bundle. +You can use [nfpm](https://nfpm.goreleaser.com/) to create .deb package for your app. + +1. Create a .desktop file for your app and specify file associations there. Example: +```ini +[Desktop Entry] +Categories=Office +Exec=/usr/bin/wails-open-file %u +Icon=wails-open-file.png +Name=wails-open-file +Terminal=false +Type=Application +MimeType=application/x-wails;application/x-test +``` + +2. Create mime types file. Example: +```xml + + + + Wails Application File + + + +``` + +3. Create icons for your file types. SVG icons are recommended. +4. Prepare postInstall/postRemove scripts for your package. Example: +```sh +# reload mime types to register file associations +update-mime-database /usr/share/mime +# reload desktop database to load app in list of available +update-desktop-database /usr/share/applications +# update icons +update-icon-caches /usr/share/icons/* +``` +5. Configure nfpm to use your scripts and files. Example: +```yaml +name: "wails-open-file" +arch: "arm64" +platform: "linux" +version: "1.0.0" +section: "default" +priority: "extra" +maintainer: "FooBarCorp " +description: "Sample Package" +vendor: "FooBarCorp" +homepage: "http://example.com" +license: "MIT" +contents: +- src: ../bin/wails-open-file + dst: /usr/bin/wails-open-file +- src: ./main.desktop + dst: /usr/share/applications/wails-open-file.desktop +- src: ./application-wails-mime.xml + dst: /usr/share/mime/packages/application-x-wails.xml +- src: ./application-test-mime.xml + dst: /usr/share/mime/packages/application-x-test.xml +- src: ../appicon.svg + dst: /usr/share/icons/hicolor/scalable/apps/wails-open-file.svg +- src: ../wailsFileIcon.svg + dst: /usr/share/icons/hicolor/scalable/mimetypes/application-x-wails.svg +- src: ../testFileIcon.svg + dst: /usr/share/icons/hicolor/scalable/mimetypes/application-x-test.svg +# copy icons to Yaru theme as well. For some reason Ubuntu didn't pick up fileicons from hicolor theme +- src: ../appicon.svg + dst: /usr/share/icons/Yaru/scalable/apps/wails-open-file.svg +- src: ../wailsFileIcon.svg + dst: /usr/share/icons/Yaru/scalable/mimetypes/application-x-wails.svg +- src: ../testFileIcon.svg + dst: /usr/share/icons/Yaru/scalable/mimetypes/application-x-test.svg +scripts: + postinstall: ./postInstall.sh + postremove: ./postRemove.sh +``` +6. Build your .deb package using nfpm: +```sh +nfpm pkg --packager deb --target . +``` +7. Now when your package is installed, your app will be associated with specified file types. When you open file with your app, +new instance of app is launched and file path is passed as argument to your app. +To handle this you should parse command line arguments in your app. Example: +```go title="main.go" +func main() { + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) != 0 { + println("launchArgs", argsWithoutProg) + } +} +``` + +You also can enable single instance lock for your app. In this case, when you open file with your app, new instance of app is not launched +and arguments are passed to already running instance. Check single instance lock guide for details. Example: +```go title="main.go" +func main() { + // Create application with options + err := wails.Run(&options.App{ + Title: "wails-open-file", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, + SingleInstanceLock: &options.SingleInstanceLock{ + UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc", + OnSecondInstanceLaunch: app.onSecondInstanceLaunch, + }, + Bind: []interface{}{ + app, + }, + }) +} +``` diff --git a/website/docs/guides/frontend.mdx b/website/docs/guides/frontend.mdx index 1384087da..2c3c78e42 100644 --- a/website/docs/guides/frontend.mdx +++ b/website/docs/guides/frontend.mdx @@ -45,7 +45,7 @@ The options are as follows: | noautoinjectipc | Disable the autoinjection of `/wails/ipc.js` | | noautoinject | Disable all autoinjection of scripts | -Multiple options may be used provided they are comma seperated. +Multiple options may be used provided they are comma separated. This code is perfectly valid and operates the same as the autoinjection version: diff --git a/website/docs/guides/ides.mdx b/website/docs/guides/ides.mdx index f22aefe9e..5fe5a7bb9 100644 --- a/website/docs/guides/ides.mdx +++ b/website/docs/guides/ides.mdx @@ -3,7 +3,7 @@ Wails aims to provide a great development experience. To that aim, we now support generating IDE specific configuration to provide smoother project setup. -Currently, we support [Visual Studio Code](https://code.visualstudio.com/) but aim to support other IDEs such as Goland. +Currently, we support [Visual Studio Code](https://code.visualstudio.com/) and [Goland](https://www.jetbrains.com/go/). ## Visual Studio Code diff --git a/website/docs/guides/linux.mdx b/website/docs/guides/linux.mdx index fa74fe6cf..2cfc2e62a 100644 --- a/website/docs/guides/linux.mdx +++ b/website/docs/guides/linux.mdx @@ -18,3 +18,109 @@ videoTag.addEventListener("timeupdate", (event) => { Source: [Lyimmi](https://github.com/Lyimmi) on the [discussions board](https://github.com/wailsapp/wails/issues/1729#issuecomment-1212291275) + +## GStreamer error when using Audio or Video elements + +If you are seeing the following error when including `