diff --git a/.github/workflows/comment-review.yml b/.github/workflows/_comment-review.yml.archived similarity index 95% rename from .github/workflows/comment-review.yml rename to .github/workflows/_comment-review.yml.archived index de8cb0a..4e21949 100644 --- a/.github/workflows/comment-review.yml +++ b/.github/workflows/_comment-review.yml.archived @@ -1,4 +1,4 @@ -name: comment-review +name: Comment Review on: issue_comment: @@ -7,7 +7,7 @@ on: types: [created] jobs: - opencode: + comment_review: if: | contains(github.event.comment.body, ' /oc') || startsWith(github.event.comment.body, '/oc') || diff --git a/.github/workflows/pr-review.yml b/.github/workflows/_pr-review.yml.archived similarity index 90% rename from .github/workflows/pr-review.yml rename to .github/workflows/_pr-review.yml.archived index c8b8932..e0972e9 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/_pr-review.yml.archived @@ -1,4 +1,4 @@ -name: pr-review +name: PR Review on: pull_request: @@ -6,6 +6,8 @@ on: jobs: review: + if: | + github.event.pull_request.draft == false runs-on: ubuntu-latest permissions: id-token: write diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f69ea62 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,108 @@ +name: CI + +on: + pull_request: + branches: + - main + paths: + - "**.go" + - "go.mod" + - "go.sum" + - "**.js" + - "**.jsx" + - "**.ts" + - "**.tsx" + - "package.json" + - "bun.lockb" + push: + branches: + - main + paths: + - "**.go" + - "go.mod" + - "go.sum" + - "**.js" + - "**.jsx" + - "**.ts" + - "**.tsx" + - "package.json" + - "bun.lockb" + +jobs: + go-tests: + name: Go Tests + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.25.0" + check-latest: true + + - name: Run Go Tests + run: | + go test ./internal/... -v -race -coverprofile=coverage.out + go install github.com/boumenot/gocover-cobertura@latest + gocover-cobertura < coverage.out > coverage.xml + + - name: Code Coverage Report + if: github.event_name == 'pull_request' + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: coverage.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + thresholds: "60 80" + + - name: Code Coverage Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: github.event_name == 'pull_request' + with: + recreate: true + path: code-coverage-results.md + + app-build: + name: App Build + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.25.0" + check-latest: true + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev + go install github.com/wailsapp/wails/v3/cmd/wails3@latest + chmod +x $(go env GOPATH)/bin/wails3 + sudo cp $(go env GOPATH)/bin/wails3 /usr/local/bin/wails + + - name: Build the app + run: | + wails generate bindings + wails build diff --git a/.github/workflows/build.yml b/.github/workflows/release.yml similarity index 82% rename from .github/workflows/build.yml rename to .github/workflows/release.yml index ca49cec..5cb3ea6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,10 @@ -name: Wails Build +name: Release on: # Only run on version tags (releases) push: tags: - - 'v*' + - "v*" # Allow manual trigger workflow_dispatch: @@ -41,7 +41,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.24.0' + go-version: "1.24.0" check-latest: true - name: Setup Bun @@ -49,14 +49,14 @@ jobs: with: bun-version: latest - - name: Install Wails - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - - - name: Install Linux dependencies + - name: Install dependencies if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev + go install github.com/wailsapp/wails/v3/cmd/wails3@latest + chmod +x $(go env GOPATH)/bin/wails3 + sudo cp $(go env GOPATH)/bin/wails3 /usr/local/bin/wails - name: Build Application run: | @@ -96,17 +96,17 @@ jobs: if: matrix.os == 'macos-latest' && github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v') && env.HAS_NOTARIZE_SECRETS == 'true' run: | # Create a zip for notarization - ditto -c -k --keepParent "build/bin/dev-toolbox.app" "build/bin/dev-toolbox.zip" - + ditto -c -k --keepParent "build/bin/devtoolbox.app" "build/bin/devtoolbox.zip" + # Submit for notarization - xcrun notarytool submit "build/bin/dev-toolbox.zip" \ + xcrun notarytool submit "build/bin/devtoolbox.zip" \ --apple-id "${{ secrets.APPLE_ID }}" \ --password "${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}" \ --team-id "${{ secrets.APPLE_TEAM_ID }}" \ --wait - + # Staple the notarization ticket - xcrun stapler staple "build/bin/dev-toolbox.app" + xcrun stapler staple "build/bin/devtoolbox.app" env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} @@ -121,29 +121,29 @@ jobs: # macOS: create DMG using create-dmg brew install create-dmg # Clean up any existing files - rm -f release/dev-toolbox-${{ matrix.build }}.dmg - rm -f /tmp/rw.*.dev-toolbox-${{ matrix.build }}.dmg + rm -f release/devtoolbox-${{ matrix.build }}.dmg + rm -f /tmp/rw.*.devtoolbox-${{ matrix.build }}.dmg create-dmg \ - --volname "Dev-Toolbox" \ + --volname "devtoolbox" \ --window-pos 200 120 \ --window-size 800 400 \ --icon-size 100 \ --app-drop-link 600 185 \ - "release/dev-toolbox-${{ matrix.build }}.dmg" \ - "build/bin/dev-toolbox.app" + "release/devtoolbox-${{ matrix.build }}.dmg" \ + "build/bin/devtoolbox.app" elif [ "${{ matrix.os }}" = "windows-latest" ]; then # Windows: Wails already outputs .exe, just copy it - cp build/bin/dev-toolbox.exe release/dev-toolbox-${{ matrix.build }}.exe + cp build/bin/devtoolbox.exe release/devtoolbox-${{ matrix.build }}.exe else # Linux: create AppImage or tar.gz - tar -czf "release/dev-toolbox-${{ matrix.build }}.tar.gz" -C build/bin dev-toolbox + tar -czf "release/devtoolbox-${{ matrix.build }}.tar.gz" -C build/bin devtoolbox fi shell: bash - name: Upload Artifacts uses: actions/upload-artifact@v4 with: - name: dev-toolbox-${{ matrix.build }}-${{ github.ref_name }} + name: devtoolbox-${{ matrix.build }}-${{ github.ref_name }} path: release/* # Create Release and upload assets (only on tags) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index f09ff93..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: Continuous Integration - -on: - pull_request: - branches: - - main - - master - paths: - - '**.go' - - 'go.mod' - - 'go.sum' - - '**.js' - - '**.jsx' - - '**.ts' - - '**.tsx' - - 'package.json' - - 'bun.lockb' - push: - branches: - - main - - master - paths: - - '**.go' - - 'go.mod' - - 'go.sum' - - '**.js' - - '**.jsx' - - '**.ts' - - '**.tsx' - - 'package.json' - - 'bun.lockb' - -jobs: - go-tests: - name: Go Tests - runs-on: ubuntu-latest - permissions: - pull-requests: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '1.24.0' - check-latest: true - - - name: Download Dependencies - run: go mod download - - - name: Run Go Tests - run: go test ./internal/... -v -race - - - name: Run Tests with Coverage - run: go test ./internal/... -coverprofile=coverage.out - - - name: Generate Coverage Report - run: | - go install github.com/jandelgado/gcov2lcov@latest - gcov2lcov -infile=coverage.out -outfile=coverage.lcov - - - name: Post Coverage Report to PR - if: github.event_name == 'pull_request' - uses: romeovs/lcov-reporter-action@v0.4.0 - with: - lcov-file: ./coverage.lcov - github-token: ${{ secrets.GITHUB_TOKEN }} - title: "Go Test Coverage Report" - delete-old-comments: true - - - name: Upload Coverage Report - uses: actions/upload-artifact@v4 - with: - name: coverage-report - path: | - coverage.out - coverage.lcov - - frontend-build: - name: Frontend Build (Bun) - runs-on: ubuntu-latest - needs: go-tests - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - - name: Install Dependencies - run: bun install - - - name: Build Web Application - run: bun run build - - - name: Verify Build Output - run: | - if [ ! -d "dist" ]; then - echo "Error: dist directory not found after build" - exit 1 - fi - echo "Build successful - dist directory created" - ls -la dist/ diff --git a/.gitignore b/.gitignore index e6e2649..4d2d1e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,11 @@ build/bin -node_modules -dist -dev-toolbox \ No newline at end of file +frontend/node_modules +frontend/dist +.task +devtoolbox +vendor +bin +runtime-debug.js +runtime.js +.task/* +/package.json diff --git a/AGENTS.md b/AGENTS.md index ff5bc58..9619ce6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -281,8 +281,8 @@ func TestDecode(t *testing.T) { ### Installation ```bash # Clone the repository -git clone https://github.com/vuon9/dev-toolbox.git -cd dev-toolbox +git clone https://github.com/vuon9/devtoolbox.git +cd devtoolbox # Install dependencies (using Bun) bun install @@ -482,7 +482,7 @@ These guidelines are intended for AI assistants (like opencode) working on this 1. **Run linting & formatting** – Execute any available lint/format commands (see section 8). 2. **Test the tool** – Verify functionality with `wails dev`. 3. **Update `TOOL_STATUS.md`** – Update the tool's status and add completion notes (mark as ðŸŸĒ Done when complete). -4. **Update `README.md`** – **REQUIRED: Whenever TOOL_STATUS.md is updated, README.md MUST also be updated to ensure consistency**: +4. **Update `README.md`** – **REQUIRED: Whenever TOOL_STATUS.md is updated, README.md MUST also be updated to ensure consistency**: - Add new tools to the feature table - Remove deprecated tools - Update descriptions if features changed diff --git a/README.md b/README.md index c34627e..22f0ee8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Developer Toolbox -[![Tests & Build](https://github.com/vuon9/dev-toolbox/actions/workflows/tests.yml/badge.svg)](https://github.com/vuon9/dev-toolbox/actions/workflows/tests.yml) -[![Wails Build](https://github.com/vuon9/dev-toolbox/actions/workflows/build.yml/badge.svg)](https://github.com/vuon9/dev-toolbox/actions/workflows/build.yml) +[![Tests & Build](https://github.com/vuon9/devtoolbox/actions/workflows/tests.yml/badge.svg)](https://github.com/vuon9/devtoolbox/actions/workflows/tests.yml) +[![Wails Build](https://github.com/vuon9/devtoolbox/actions/workflows/build.yml/badge.svg)](https://github.com/vuon9/devtoolbox/actions/workflows/build.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?style=flat&logo=go&logoColor=white)](https://go.dev) @@ -50,7 +50,7 @@ The central hub with 45+ algorithms across 5 categories: ## Installation ### Download Pre-built Binaries -Download the latest release for your platform from the [Releases](https://github.com/your-org/dev-toolbox/releases) page. +Download the latest release for your platform from the [Releases](https://github.com/vuon9/devtoolbox/releases) page. **Supported Platforms:** - Windows (x64) @@ -67,8 +67,8 @@ Download the latest release for your platform from the [Releases](https://github **Build Steps:** ```bash # Clone the repository -git clone https://github.com/your-org/dev-toolbox.git -cd dev-toolbox +git clone https://github.com/your-org/devtoolbox.git +cd devtoolbox # Install dependencies and build wails build @@ -79,7 +79,7 @@ wails dev ## Installation -Download the latest release for your platform from the [Releases](https://github.com/vuon9/dev-toolbox/releases) page. +Download the latest release for your platform from the [Releases](https://github.com/vuon9/devtoolbox/releases) page. ### macOS @@ -87,41 +87,41 @@ Download the latest release for your platform from the [Releases](https://github **To bypass the security warning:** -1. Download the `dev-toolbox-macos.dmg` file +1. Download the `devtoolbox-macos.dmg` file 2. Open the DMG and drag the app to your Applications folder 3. **First time only:** Open Terminal and run: ```bash - xattr -cr /Applications/dev-toolbox.app + xattr -cr /Applications/devtoolbox.app ``` Or alternatively: - Go to **System Settings** → **Privacy & Security** - Scroll down to the "Security" section - - Click **"Open Anyway"** next to the message about "dev-toolbox" + - Click **"Open Anyway"** next to the message about "devtoolbox" - Click **"Open"** in the dialog that appears 4. The app will now open normally ### Windows -1. Download `dev-toolbox-windows.exe` +1. Download `devtoolbox-windows.exe` 2. Run the executable 3. If Windows Defender shows a warning, click **"More info"** → **"Run anyway"** ### Linux -1. Download `dev-toolbox-linux.tar.gz` -2. Extract: `tar -xzf dev-toolbox-linux.tar.gz` -3. Run: `./dev-toolbox` +1. Download `devtoolbox-linux.tar.gz` +2. Extract: `tar -xzf devtoolbox-linux.tar.gz` +3. Run: `./devtoolbox` ## Key Features -✅ **Works Offline** - All tools run locally, no internet connection required -✅ **Dark/Light Themes** - Switch between themes or use system preference -✅ **Pin Tools** - Pin frequently used tools to the top of the sidebar -✅ **Keyboard Shortcuts** - `Cmd/Ctrl + B` to toggle sidebar -✅ **Copy to Clipboard** - One-click copy buttons on all output fields -✅ **Auto-run** - See results instantly as you type (can be disabled) -✅ **Responsive Layout** - Horizontal or vertical split panes +✅ **Works Offline** - All tools run locally, no internet connection required +✅ **Dark/Light Themes** - Switch between themes or use system preference +✅ **Pin Tools** - Pin frequently used tools to the top of the sidebar +✅ **Keyboard Shortcuts** - `Cmd/Ctrl + B` to toggle sidebar +✅ **Copy to Clipboard** - One-click copy buttons on all output fields +✅ **Auto-run** - See results instantly as you type (can be disabled) +✅ **Responsive Layout** - Horizontal or vertical split panes ## UI Design diff --git a/TOOL_STATUS.md b/TOOL_STATUS.md index 95c2764..b53e049 100644 --- a/TOOL_STATUS.md +++ b/TOOL_STATUS.md @@ -25,7 +25,7 @@ This document tracks the refactoring and development status of each tool compone | **CronJobParser** | ðŸŸĒ Done | Refactored to follow Carbon Design System. Features: Split-pane layout, 8 common examples in clickable tiles, real-time parsing, large centered output display, layout toggle. | Completed 2026-01-31 | | **RegExpTester** | ðŸŸĄ In Progress | Refactored with improved UI. Features: Flag toggle tags (g, i, m, s, u, y), split-pane layout, match count in output label, error display with styling, layout toggle. | Updated 2026-01-31 | | **TextDiffChecker** | ðŸŸĄ In Progress | Refactored with enhanced features. Features: Diff mode switcher (Lines/Words/Chars), auto-compare on input change, Clear button, improved diff view with color coding, layout toggle. | Updated 2026-01-31 | -| **UnixTimeConverter** | ðŸŸĄ In Progress | Refactored with new features. Features: Relative time display (e.g., "2 hours ago"), split-pane layout (ISO 8601 / Local), "Now" button with icon, layout toggle, auto-initialization with current time. | Updated 2026-01-31 | +| **DateTimeConverter** | ðŸŸĒ Done | Complete redesign as unified DateTime Converter. All features on single screen - no tabs. Client-side only (no backend dependency). Features: Auto-detect input format (Unix timestamps: s/ms/Ξs/ns, ISO dates, SQL dates, US/EU formats), Quick presets (Now, Start/End of Day, Tomorrow, Yesterday, Next Week, Unix Epoch), Output format selector (ISO, RFC, SQL, US, EU, Compact), Timezone support, Main result display with relative time, All formats grid with copy buttons, Toggle-able sections: Visual Widgets (Calendar + Analog Clock), Time Calculator (Date A vs B with delta), Batch Converter (multi-line input with table results), Timezone Comparison (6 major cities), History persistence (localStorage, last 20), URL share support (?ts=). Unified, user-friendly interface designed for real-world datetime conversion needs. | Completed 2026-02-01 | --- diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..cf70263 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,60 @@ +version: '3' + +includes: + common: ./build/Taskfile.yml + windows: ./build/windows/Taskfile.yml + darwin: ./build/darwin/Taskfile.yml + linux: ./build/linux/Taskfile.yml + ios: ./build/ios/Taskfile.yml + android: ./build/android/Taskfile.yml + +vars: + APP_NAME: "DevToolbox" + BIN_DIR: "bin" + VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' + +tasks: + build: + summary: Builds the application + cmds: + - task: "{{OS}}:build" + + package: + summary: Packages a production build of the application + cmds: + - task: "{{OS}}:package" + + run: + summary: Runs the application + cmds: + - task: "{{OS}}:run" + + dev: + summary: Runs the application in development mode + cmds: + - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} + + setup:docker: + summary: Builds Docker image for cross-compilation (~800MB download) + cmds: + - task: common:setup:docker + + build:server: + summary: Builds the application in server mode (no GUI, HTTP server only) + cmds: + - task: common:build:server + + run:server: + summary: Runs the application in server mode + cmds: + - task: common:run:server + + build:docker: + summary: Builds a Docker image for server mode deployment + cmds: + - task: common:build:docker + + run:docker: + summary: Builds and runs the Docker image + cmds: + - task: common:run:docker diff --git a/app.go b/app.go index af53038..10e7a0b 100644 --- a/app.go +++ b/app.go @@ -1,27 +1,13 @@ 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 -} +// GreetService struct +type GreetService struct{} // Greet returns a greeting for the given name -func (a *App) Greet(name string) string { +func (a *GreetService) Greet(name string) string { return fmt.Sprintf("Hello %s, It's show time!", name) } diff --git a/build/README.md b/build/README.md deleted file mode 100644 index 1ae2f67..0000000 --- a/build/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# 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/build/Taskfile.yml b/build/Taskfile.yml new file mode 100644 index 0000000..714416a --- /dev/null +++ b/build/Taskfile.yml @@ -0,0 +1,250 @@ +version: "3" + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + dir: frontend + sources: + - package.json + - package-lock.json + generates: + - node_modules + preconditions: + - sh: bun --version + msg: "Looks like bun isn't installed. bun is part of the Node installer: https://nodejs.org/en/download/" + cmds: + - bun install + + build:frontend: + label: build:frontend (DEV={{.DEV}}) + summary: Build the frontend project + dir: frontend + sources: + - "**/*" + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - bun run {{.BUILD_COMMAND}} + env: + PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}' + vars: + BUILD_COMMAND: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}' + + frontend:vendor:puppertino: + summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling + sources: + - frontend/public/puppertino/puppertino.css + generates: + - frontend/public/puppertino/puppertino.css + cmds: + - | + set -euo pipefail + mkdir -p frontend/public/puppertino + # If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error. + if [ ! -f frontend/public/puppertino/puppertino.css ]; then + echo "No bundled Puppertino found. Attempting to fetch from GitHub..." + if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then + curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true + echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css" + else + echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it." + fi + else + echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css" + fi + # Ensure index.html includes Puppertino CSS and button classes + INDEX_HTML=frontend/index.html + if [ -f "$INDEX_HTML" ]; then + if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then + # Insert Puppertino link tag after style.css link + awk ' + /href="\/style.css"\/?/ && !x { print; print " "; x=1; next }1 + ' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML" + fi + # Replace default .btn with Puppertino primary button classes if present + sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true + fi + + generate:bindings: + label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) + summary: Generates bindings for the frontend + deps: + - task: go:mod:tidy + sources: + - "**/*.[jt]s" + - exclude: frontend/**/* + - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms). + dir: build + sources: + - "appicon.png" + - "appicon.icon" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin + + dev:frontend: + summary: Runs the frontend in development mode + dir: frontend + deps: + - task: install:frontend:deps + cmds: + - bun run dev -- --port {{.VITE_PORT}} --strictPort + + update:build-assets: + summary: Updates the build assets + dir: build + cmds: + - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . + + build:server: + summary: Builds the application in server mode (no GUI, HTTP server only) + desc: | + Builds the application with the server build tag enabled. + Server mode runs as a pure HTTP server without native GUI dependencies. + Usage: task build:server + deps: + - task: build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - go build -tags server {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}} + vars: + BUILD_FLAGS: "{{.BUILD_FLAGS}}" + + run:server: + summary: Builds and runs the application in server mode + deps: + - task: build:server + cmds: + - ./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}} + + build:docker: + summary: Builds a Docker image for server mode deployment + desc: | + Creates a minimal Docker image containing the server mode binary. + The image is based on distroless for security and small size. + Usage: task build:docker [TAG=myapp:latest] + cmds: + - docker build -t {{.TAG | default (printf "%s:latest" .APP_NAME)}} -f build/docker/Dockerfile.server . + vars: + TAG: "{{.TAG}}" + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required. Please install Docker first." + - sh: test -f build/docker/Dockerfile.server + msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it." + + run:docker: + summary: Builds and runs the Docker image + desc: | + Builds the Docker image and runs it, exposing port 8080. + Usage: task run:docker [TAG=myapp:latest] [PORT=8080] + Note: The internal container port is always 8080. The PORT variable + only changes the host port mapping. Ensure your app uses port 8080 + or modify the Dockerfile to match your ServerOptions.Port setting. + deps: + - task: build:docker + vars: + TAG: + ref: .TAG + cmds: + - docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}} + vars: + TAG: "{{.TAG}}" + PORT: "{{.PORT}}" + + setup:docker: + summary: Builds Docker image for cross-compilation (~800MB download) + desc: | + Builds the Docker image needed for cross-compiling to any platform. + Run this once to enable cross-platform builds from any OS. + cmds: + - docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/ + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required. Please install Docker first." + + ios:device:list: + summary: Lists connected iOS devices (UDIDs) + cmds: + - xcrun xcdevice list + + ios:run:device: + summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl) + vars: + PROJECT: "{{.PROJECT}}" # e.g., build/ios/xcode/.xcodeproj + SCHEME: "{{.SCHEME}}" # e.g., ios.dev + CONFIG: '{{.CONFIG | default "Debug"}}' + DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}' + UDID: "{{.UDID}}" # from `task ios:device:list` + BUNDLE_ID: "{{.BUNDLE_ID}}" # e.g., com.yourco.wails.ios.dev + TEAM_ID: "{{.TEAM_ID}}" # optional, if your project is not already set up for signing + preconditions: + - sh: xcrun -f xcodebuild + msg: "xcodebuild not found. Please install Xcode." + - sh: xcrun -f devicectl + msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)." + - sh: test -n '{{.PROJECT}}' + msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)." + - sh: test -n '{{.SCHEME}}' + msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)." + - sh: test -n '{{.UDID}}' + msg: "Set UDID to your device UDID (see: task ios:device:list)." + - sh: test -n '{{.BUNDLE_ID}}' + msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)." + cmds: + - | + set -euo pipefail + echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}" + XCB_ARGS=( + -project "{{.PROJECT}}" + -scheme "{{.SCHEME}}" + -configuration "{{.CONFIG}}" + -destination "id={{.UDID}}" + -derivedDataPath "{{.DERIVED}}" + -allowProvisioningUpdates + -allowProvisioningDeviceRegistration + ) + # Optionally inject signing identifiers if provided + if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi + if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi + xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true + # If xcpretty isn't installed, run without it + if [ "${PIPESTATUS[0]}" -ne 0 ]; then + xcodebuild "${XCB_ARGS[@]}" build + fi + # Find built .app + APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1) + if [ -z "$APP_PATH" ]; then + echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2 + exit 1 + fi + echo "Installing: $APP_PATH" + xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH" + echo "Launching: {{.BUNDLE_ID}}" + xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}" diff --git a/build/android/Taskfile.yml b/build/android/Taskfile.yml new file mode 100644 index 0000000..aca62e4 --- /dev/null +++ b/build/android/Taskfile.yml @@ -0,0 +1,237 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + APP_ID: '{{.APP_ID | default "com.wails.app"}}' + MIN_SDK: '21' + TARGET_SDK: '34' + NDK_VERSION: 'r26d' + +tasks: + install:deps: + summary: Check and install Android development dependencies + cmds: + - go run build/android/scripts/deps/install_deps.go + env: + TASK_FORCE_YES: '{{if .YES}}true{{else}}false{{end}}' + prompt: This will check and install Android development dependencies. Continue? + + build: + summary: Creates a build of the application for Android + deps: + - task: common:go:mod:tidy + - task: generate:android:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - echo "Building Android app {{.APP_NAME}}..." + - task: compile:go:shared + vars: + ARCH: '{{.ARCH | default "arm64"}}' + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}' + env: + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + compile:go:shared: + summary: Compile Go code to shared library (.so) + cmds: + - | + NDK_ROOT="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/{{.NDK_VERSION}}}" + if [ ! -d "$NDK_ROOT" ]; then + echo "Error: Android NDK not found at $NDK_ROOT" + echo "Please set ANDROID_NDK_HOME or install NDK {{.NDK_VERSION}} via Android Studio" + exit 1 + fi + + # Determine toolchain based on host OS + case "$(uname -s)" in + Darwin) HOST_TAG="darwin-x86_64" ;; + Linux) HOST_TAG="linux-x86_64" ;; + *) echo "Unsupported host OS"; exit 1 ;; + esac + + TOOLCHAIN="$NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG" + + # Set compiler based on architecture + case "{{.ARCH}}" in + arm64) + export CC="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang" + export CXX="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang++" + export GOARCH=arm64 + JNI_DIR="arm64-v8a" + ;; + amd64|x86_64) + export CC="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang" + export CXX="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang++" + export GOARCH=amd64 + JNI_DIR="x86_64" + ;; + *) + echo "Unsupported architecture: {{.ARCH}}" + exit 1 + ;; + esac + + export CGO_ENABLED=1 + export GOOS=android + + mkdir -p {{.BIN_DIR}} + mkdir -p build/android/app/src/main/jniLibs/$JNI_DIR + + go build -buildmode=c-shared {{.BUILD_FLAGS}} \ + -o build/android/app/src/main/jniLibs/$JNI_DIR/libwails.so + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}' + + compile:go:all-archs: + summary: Compile Go code for all Android architectures (fat APK) + cmds: + - task: compile:go:shared + vars: + ARCH: arm64 + - task: compile:go:shared + vars: + ARCH: amd64 + + package: + summary: Packages a production build of the application into an APK + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: assemble:apk + + package:fat: + summary: Packages a production build for all architectures (fat APK) + cmds: + - task: compile:go:all-archs + - task: assemble:apk + + assemble:apk: + summary: Assembles the APK using Gradle + cmds: + - | + cd build/android + ./gradlew assembleDebug + cp app/build/outputs/apk/debug/app-debug.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}.apk" + echo "APK created: {{.BIN_DIR}}/{{.APP_NAME}}.apk" + + assemble:apk:release: + summary: Assembles a release APK using Gradle + cmds: + - | + cd build/android + ./gradlew assembleRelease + cp app/build/outputs/apk/release/app-release-unsigned.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}-release.apk" + echo "Release APK created: {{.BIN_DIR}}/{{.APP_NAME}}-release.apk" + + generate:android:bindings: + internal: true + summary: Generates bindings for Android + sources: + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true + env: + GOOS: android + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default "arm64"}}' + + ensure-emulator: + internal: true + summary: Ensure Android Emulator is running + silent: true + cmds: + - | + # Check if an emulator is already running + if adb devices | grep -q "emulator"; then + echo "Emulator already running" + exit 0 + fi + + # Get first available AVD + AVD_NAME=$(emulator -list-avds | head -1) + if [ -z "$AVD_NAME" ]; then + echo "No Android Virtual Devices found." + echo "Create one using: Android Studio > Tools > Device Manager" + exit 1 + fi + + echo "Starting emulator: $AVD_NAME" + emulator -avd "$AVD_NAME" -no-snapshot-load & + + # Wait for emulator to boot (max 60 seconds) + echo "Waiting for emulator to boot..." + adb wait-for-device + + for i in {1..60}; do + BOOT_COMPLETED=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [ "$BOOT_COMPLETED" = "1" ]; then + echo "Emulator booted successfully" + exit 0 + fi + sleep 1 + done + + echo "Emulator boot timeout" + exit 1 + preconditions: + - sh: command -v adb + msg: "adb not found. Please install Android SDK and add platform-tools to PATH" + - sh: command -v emulator + msg: "emulator not found. Please install Android SDK and add emulator to PATH" + + deploy-emulator: + summary: Deploy to Android Emulator + deps: [package] + cmds: + - adb uninstall {{.APP_ID}} 2>/dev/null || true + - adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk" + - adb shell am start -n {{.APP_ID}}/.MainActivity + + run: + summary: Run the application in Android Emulator + deps: + - task: ensure-emulator + - task: build + vars: + ARCH: x86_64 + cmds: + - task: assemble:apk + - adb uninstall {{.APP_ID}} 2>/dev/null || true + - adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk" + - adb shell am start -n {{.APP_ID}}/.MainActivity + + logs: + summary: Stream Android logcat filtered to this app + cmds: + - adb logcat -v time | grep -E "(Wails|{{.APP_NAME}})" + + logs:all: + summary: Stream all Android logcat (verbose) + cmds: + - adb logcat -v time + + clean: + summary: Clean build artifacts + cmds: + - rm -rf {{.BIN_DIR}} + - rm -rf build/android/app/build + - rm -rf build/android/app/src/main/jniLibs/*/libwails.so + - rm -rf build/android/.gradle diff --git a/build/android/app/build.gradle b/build/android/app/build.gradle new file mode 100644 index 0000000..78fdbf7 --- /dev/null +++ b/build/android/app/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.wails.app' + compileSdk 34 + + buildFeatures { + buildConfig = true + } + + defaultConfig { + applicationId "com.wails.app" + minSdk 21 + targetSdk 34 + versionCode 1 + versionName "1.0" + + // Configure supported ABIs + ndk { + abiFilters 'arm64-v8a', 'x86_64' + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + debuggable true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + // Source sets configuration + sourceSets { + main { + // JNI libraries are in jniLibs folder + jniLibs.srcDirs = ['src/main/jniLibs'] + // Assets for the WebView + assets.srcDirs = ['src/main/assets'] + } + } + + // Packaging options + packagingOptions { + // Don't strip Go symbols in debug builds + doNotStrip '*/arm64-v8a/libwails.so' + doNotStrip '*/x86_64/libwails.so' + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.webkit:webkit:1.9.0' + implementation 'com.google.android.material:material:1.11.0' +} diff --git a/build/android/app/proguard-rules.pro b/build/android/app/proguard-rules.pro new file mode 100644 index 0000000..8b88c3d --- /dev/null +++ b/build/android/app/proguard-rules.pro @@ -0,0 +1,12 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. + +# Keep native methods +-keepclasseswithmembernames class * { + native ; +} + +# Keep Wails bridge classes +-keep class com.wails.app.WailsBridge { *; } +-keep class com.wails.app.WailsJSBridge { *; } diff --git a/build/android/app/src/main/AndroidManifest.xml b/build/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6c7982a --- /dev/null +++ b/build/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/build/android/app/src/main/java/com/wails/app/MainActivity.java b/build/android/app/src/main/java/com/wails/app/MainActivity.java new file mode 100644 index 0000000..3067fee --- /dev/null +++ b/build/android/app/src/main/java/com/wails/app/MainActivity.java @@ -0,0 +1,198 @@ +package com.wails.app; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.util.Log; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.webkit.WebViewAssetLoader; +import com.wails.app.BuildConfig; + +/** + * MainActivity hosts the WebView and manages the Wails application lifecycle. + * It uses WebViewAssetLoader to serve assets from the Go library without + * requiring a network server. + */ +public class MainActivity extends AppCompatActivity { + private static final String TAG = "WailsActivity"; + private static final String WAILS_SCHEME = "https"; + private static final String WAILS_HOST = "wails.localhost"; + + private WebView webView; + private WailsBridge bridge; + private WebViewAssetLoader assetLoader; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Initialize the native Go library + bridge = new WailsBridge(this); + bridge.initialize(); + + // Set up WebView + setupWebView(); + + // Load the application + loadApplication(); + } + + @SuppressLint("SetJavaScriptEnabled") + private void setupWebView() { + webView = findViewById(R.id.webview); + + // Configure WebView settings + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setDatabaseEnabled(true); + settings.setAllowFileAccess(false); + settings.setAllowContentAccess(false); + settings.setMediaPlaybackRequiresUserGesture(false); + settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); + + // Enable debugging in debug builds + if (BuildConfig.DEBUG) { + WebView.setWebContentsDebuggingEnabled(true); + } + + // Set up asset loader for serving local assets + assetLoader = new WebViewAssetLoader.Builder() + .setDomain(WAILS_HOST) + .addPathHandler("/", new WailsPathHandler(bridge)) + .build(); + + // Set up WebView client to intercept requests + webView.setWebViewClient(new WebViewClient() { + @Nullable + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + String url = request.getUrl().toString(); + Log.d(TAG, "Intercepting request: " + url); + + // Handle wails.localhost requests + if (request.getUrl().getHost() != null && + request.getUrl().getHost().equals(WAILS_HOST)) { + + // For wails API calls (runtime, capabilities, etc.), we need to pass the full URL + // including query string because WebViewAssetLoader.PathHandler strips query params + String path = request.getUrl().getPath(); + if (path != null && path.startsWith("/wails/")) { + // Get full path with query string for runtime calls + String fullPath = path; + String query = request.getUrl().getQuery(); + if (query != null && !query.isEmpty()) { + fullPath = path + "?" + query; + } + Log.d(TAG, "Wails API call detected, full path: " + fullPath); + + // Call bridge directly with full path + byte[] data = bridge.serveAsset(fullPath, request.getMethod(), "{}"); + if (data != null && data.length > 0) { + java.io.InputStream inputStream = new java.io.ByteArrayInputStream(data); + java.util.Map headers = new java.util.HashMap<>(); + headers.put("Access-Control-Allow-Origin", "*"); + headers.put("Cache-Control", "no-cache"); + headers.put("Content-Type", "application/json"); + + return new WebResourceResponse( + "application/json", + "UTF-8", + 200, + "OK", + headers, + inputStream + ); + } + // Return error response if data is null + return new WebResourceResponse( + "application/json", + "UTF-8", + 500, + "Internal Error", + new java.util.HashMap<>(), + new java.io.ByteArrayInputStream("{}".getBytes()) + ); + } + + // For regular assets, use the asset loader + return assetLoader.shouldInterceptRequest(request.getUrl()); + } + + return super.shouldInterceptRequest(view, request); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + Log.d(TAG, "Page loaded: " + url); + // Inject Wails runtime + bridge.injectRuntime(webView, url); + } + }); + + // Add JavaScript interface for Go communication + webView.addJavascriptInterface(new WailsJSBridge(bridge, webView), "wails"); + } + + private void loadApplication() { + // Load the main page from the asset server + String url = WAILS_SCHEME + "://" + WAILS_HOST + "/"; + Log.d(TAG, "Loading URL: " + url); + webView.loadUrl(url); + } + + /** + * Execute JavaScript in the WebView from the Go side + */ + public void executeJavaScript(final String js) { + runOnUiThread(() -> { + if (webView != null) { + webView.evaluateJavascript(js, null); + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + if (bridge != null) { + bridge.onResume(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (bridge != null) { + bridge.onPause(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (bridge != null) { + bridge.shutdown(); + } + if (webView != null) { + webView.destroy(); + } + } + + @Override + public void onBackPressed() { + if (webView != null && webView.canGoBack()) { + webView.goBack(); + } else { + super.onBackPressed(); + } + } +} diff --git a/build/android/app/src/main/java/com/wails/app/WailsBridge.java b/build/android/app/src/main/java/com/wails/app/WailsBridge.java new file mode 100644 index 0000000..3dab652 --- /dev/null +++ b/build/android/app/src/main/java/com/wails/app/WailsBridge.java @@ -0,0 +1,214 @@ +package com.wails.app; + +import android.content.Context; +import android.util.Log; +import android.webkit.WebView; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * WailsBridge manages the connection between the Java/Android side and the Go native library. + * It handles: + * - Loading and initializing the native Go library + * - Serving asset requests from Go + * - Passing messages between JavaScript and Go + * - Managing callbacks for async operations + */ +public class WailsBridge { + private static final String TAG = "WailsBridge"; + + static { + // Load the native Go library + System.loadLibrary("wails"); + } + + private final Context context; + private final AtomicInteger callbackIdGenerator = new AtomicInteger(0); + private final ConcurrentHashMap pendingAssetCallbacks = new ConcurrentHashMap<>(); + private final ConcurrentHashMap pendingMessageCallbacks = new ConcurrentHashMap<>(); + private WebView webView; + private volatile boolean initialized = false; + + // Native methods - implemented in Go + private static native void nativeInit(WailsBridge bridge); + private static native void nativeShutdown(); + private static native void nativeOnResume(); + private static native void nativeOnPause(); + private static native void nativeOnPageFinished(String url); + private static native byte[] nativeServeAsset(String path, String method, String headers); + private static native String nativeHandleMessage(String message); + private static native String nativeGetAssetMimeType(String path); + + public WailsBridge(Context context) { + this.context = context; + } + + /** + * Initialize the native Go library + */ + public void initialize() { + if (initialized) { + return; + } + + Log.i(TAG, "Initializing Wails bridge..."); + try { + nativeInit(this); + initialized = true; + Log.i(TAG, "Wails bridge initialized successfully"); + } catch (Exception e) { + Log.e(TAG, "Failed to initialize Wails bridge", e); + } + } + + /** + * Shutdown the native Go library + */ + public void shutdown() { + if (!initialized) { + return; + } + + Log.i(TAG, "Shutting down Wails bridge..."); + try { + nativeShutdown(); + initialized = false; + } catch (Exception e) { + Log.e(TAG, "Error during shutdown", e); + } + } + + /** + * Called when the activity resumes + */ + public void onResume() { + if (initialized) { + nativeOnResume(); + } + } + + /** + * Called when the activity pauses + */ + public void onPause() { + if (initialized) { + nativeOnPause(); + } + } + + /** + * Serve an asset from the Go asset server + * @param path The URL path requested + * @param method The HTTP method + * @param headers The request headers as JSON + * @return The asset data, or null if not found + */ + public byte[] serveAsset(String path, String method, String headers) { + if (!initialized) { + Log.w(TAG, "Bridge not initialized, cannot serve asset: " + path); + return null; + } + + Log.d(TAG, "Serving asset: " + path); + try { + return nativeServeAsset(path, method, headers); + } catch (Exception e) { + Log.e(TAG, "Error serving asset: " + path, e); + return null; + } + } + + /** + * Get the MIME type for an asset + * @param path The asset path + * @return The MIME type string + */ + public String getAssetMimeType(String path) { + if (!initialized) { + return "application/octet-stream"; + } + + try { + String mimeType = nativeGetAssetMimeType(path); + return mimeType != null ? mimeType : "application/octet-stream"; + } catch (Exception e) { + Log.e(TAG, "Error getting MIME type for: " + path, e); + return "application/octet-stream"; + } + } + + /** + * Handle a message from JavaScript + * @param message The message from JavaScript (JSON) + * @return The response to send back to JavaScript (JSON) + */ + public String handleMessage(String message) { + if (!initialized) { + Log.w(TAG, "Bridge not initialized, cannot handle message"); + return "{\"error\":\"Bridge not initialized\"}"; + } + + Log.d(TAG, "Handling message from JS: " + message); + try { + return nativeHandleMessage(message); + } catch (Exception e) { + Log.e(TAG, "Error handling message", e); + return "{\"error\":\"" + e.getMessage() + "\"}"; + } + } + + /** + * Inject the Wails runtime JavaScript into the WebView. + * Called when the page finishes loading. + * @param webView The WebView to inject into + * @param url The URL that finished loading + */ + public void injectRuntime(WebView webView, String url) { + this.webView = webView; + // Notify Go side that page has finished loading so it can inject the runtime + Log.d(TAG, "Page finished loading: " + url + ", notifying Go side"); + if (initialized) { + nativeOnPageFinished(url); + } + } + + /** + * Execute JavaScript in the WebView (called from Go side) + * @param js The JavaScript code to execute + */ + public void executeJavaScript(String js) { + if (webView != null) { + webView.post(() -> webView.evaluateJavascript(js, null)); + } + } + + /** + * Called from Go when an event needs to be emitted to JavaScript + * @param eventName The event name + * @param eventData The event data (JSON) + */ + public void emitEvent(String eventName, String eventData) { + String js = String.format("window.wails && window.wails._emit('%s', %s);", + escapeJsString(eventName), eventData); + executeJavaScript(js); + } + + private String escapeJsString(String str) { + return str.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\r", "\\r"); + } + + // Callback interfaces + public interface AssetCallback { + void onAssetReady(byte[] data, String mimeType); + void onAssetError(String error); + } + + public interface MessageCallback { + void onResponse(String response); + void onError(String error); + } +} diff --git a/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java b/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java new file mode 100644 index 0000000..98ae5b2 --- /dev/null +++ b/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java @@ -0,0 +1,142 @@ +package com.wails.app; + +import android.util.Log; +import android.webkit.JavascriptInterface; +import android.webkit.WebView; +import com.wails.app.BuildConfig; + +/** + * WailsJSBridge provides the JavaScript interface that allows the web frontend + * to communicate with the Go backend. This is exposed to JavaScript as the + * `window.wails` object. + * + * Similar to iOS's WKScriptMessageHandler but using Android's addJavascriptInterface. + */ +public class WailsJSBridge { + private static final String TAG = "WailsJSBridge"; + + private final WailsBridge bridge; + private final WebView webView; + + public WailsJSBridge(WailsBridge bridge, WebView webView) { + this.bridge = bridge; + this.webView = webView; + } + + /** + * Send a message to Go and return the response synchronously. + * Called from JavaScript: wails.invoke(message) + * + * @param message The message to send (JSON string) + * @return The response from Go (JSON string) + */ + @JavascriptInterface + public String invoke(String message) { + Log.d(TAG, "Invoke called: " + message); + return bridge.handleMessage(message); + } + + /** + * Send a message to Go asynchronously. + * The response will be sent back via a callback. + * Called from JavaScript: wails.invokeAsync(callbackId, message) + * + * @param callbackId The callback ID to use for the response + * @param message The message to send (JSON string) + */ + @JavascriptInterface + public void invokeAsync(final String callbackId, final String message) { + Log.d(TAG, "InvokeAsync called: " + message); + + // Handle in background thread to not block JavaScript + new Thread(() -> { + try { + String response = bridge.handleMessage(message); + sendCallback(callbackId, response, null); + } catch (Exception e) { + Log.e(TAG, "Error in async invoke", e); + sendCallback(callbackId, null, e.getMessage()); + } + }).start(); + } + + /** + * Log a message from JavaScript to Android's logcat + * Called from JavaScript: wails.log(level, message) + * + * @param level The log level (debug, info, warn, error) + * @param message The message to log + */ + @JavascriptInterface + public void log(String level, String message) { + switch (level.toLowerCase()) { + case "debug": + Log.d(TAG + "/JS", message); + break; + case "info": + Log.i(TAG + "/JS", message); + break; + case "warn": + Log.w(TAG + "/JS", message); + break; + case "error": + Log.e(TAG + "/JS", message); + break; + default: + Log.v(TAG + "/JS", message); + break; + } + } + + /** + * Get the platform name + * Called from JavaScript: wails.platform() + * + * @return "android" + */ + @JavascriptInterface + public String platform() { + return "android"; + } + + /** + * Check if we're running in debug mode + * Called from JavaScript: wails.isDebug() + * + * @return true if debug build, false otherwise + */ + @JavascriptInterface + public boolean isDebug() { + return BuildConfig.DEBUG; + } + + /** + * Send a callback response to JavaScript + */ + private void sendCallback(String callbackId, String result, String error) { + final String js; + if (error != null) { + js = String.format( + "window.wails && window.wails._callback('%s', null, '%s');", + escapeJsString(callbackId), + escapeJsString(error) + ); + } else { + js = String.format( + "window.wails && window.wails._callback('%s', %s, null);", + escapeJsString(callbackId), + result != null ? result : "null" + ); + } + + webView.post(() -> webView.evaluateJavascript(js, null)); + } + + private String escapeJsString(String str) { + if (str == null) return ""; + return str.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\r", "\\r"); + } +} diff --git a/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java b/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java new file mode 100644 index 0000000..326fa9b --- /dev/null +++ b/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java @@ -0,0 +1,118 @@ +package com.wails.app; + +import android.net.Uri; +import android.util.Log; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewAssetLoader; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * WailsPathHandler implements WebViewAssetLoader.PathHandler to serve assets + * from the Go asset server. This allows the WebView to load assets without + * using a network server, similar to iOS's WKURLSchemeHandler. + */ +public class WailsPathHandler implements WebViewAssetLoader.PathHandler { + private static final String TAG = "WailsPathHandler"; + + private final WailsBridge bridge; + + public WailsPathHandler(WailsBridge bridge) { + this.bridge = bridge; + } + + @Nullable + @Override + public WebResourceResponse handle(@NonNull String path) { + Log.d(TAG, "Handling path: " + path); + + // Normalize path + if (path.isEmpty() || path.equals("/")) { + path = "/index.html"; + } + + // Get asset from Go + byte[] data = bridge.serveAsset(path, "GET", "{}"); + + if (data == null || data.length == 0) { + Log.w(TAG, "Asset not found: " + path); + return null; // Return null to let WebView handle 404 + } + + // Determine MIME type + String mimeType = bridge.getAssetMimeType(path); + Log.d(TAG, "Serving " + path + " with type " + mimeType + " (" + data.length + " bytes)"); + + // Create response + InputStream inputStream = new ByteArrayInputStream(data); + Map headers = new HashMap<>(); + headers.put("Access-Control-Allow-Origin", "*"); + headers.put("Cache-Control", "no-cache"); + + return new WebResourceResponse( + mimeType, + "UTF-8", + 200, + "OK", + headers, + inputStream + ); + } + + /** + * Determine MIME type from file extension + */ + private String getMimeType(String path) { + String lowerPath = path.toLowerCase(); + + if (lowerPath.endsWith(".html") || lowerPath.endsWith(".htm")) { + return "text/html"; + } else if (lowerPath.endsWith(".js") || lowerPath.endsWith(".mjs")) { + return "application/javascript"; + } else if (lowerPath.endsWith(".css")) { + return "text/css"; + } else if (lowerPath.endsWith(".json")) { + return "application/json"; + } else if (lowerPath.endsWith(".png")) { + return "image/png"; + } else if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) { + return "image/jpeg"; + } else if (lowerPath.endsWith(".gif")) { + return "image/gif"; + } else if (lowerPath.endsWith(".svg")) { + return "image/svg+xml"; + } else if (lowerPath.endsWith(".ico")) { + return "image/x-icon"; + } else if (lowerPath.endsWith(".woff")) { + return "font/woff"; + } else if (lowerPath.endsWith(".woff2")) { + return "font/woff2"; + } else if (lowerPath.endsWith(".ttf")) { + return "font/ttf"; + } else if (lowerPath.endsWith(".eot")) { + return "application/vnd.ms-fontobject"; + } else if (lowerPath.endsWith(".xml")) { + return "application/xml"; + } else if (lowerPath.endsWith(".txt")) { + return "text/plain"; + } else if (lowerPath.endsWith(".wasm")) { + return "application/wasm"; + } else if (lowerPath.endsWith(".mp3")) { + return "audio/mpeg"; + } else if (lowerPath.endsWith(".mp4")) { + return "video/mp4"; + } else if (lowerPath.endsWith(".webm")) { + return "video/webm"; + } else if (lowerPath.endsWith(".webp")) { + return "image/webp"; + } + + return "application/octet-stream"; + } +} diff --git a/build/android/app/src/main/res/layout/activity_main.xml b/build/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..f278384 --- /dev/null +++ b/build/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..9409abe Binary files /dev/null and b/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..9409abe Binary files /dev/null and b/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..5b6acc0 Binary files /dev/null and b/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..5b6acc0 Binary files /dev/null and b/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..1c2c664 Binary files /dev/null and b/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..1c2c664 Binary files /dev/null and b/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..be557d8 Binary files /dev/null and b/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..be557d8 Binary files /dev/null and b/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4507f32 Binary files /dev/null and b/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4507f32 Binary files /dev/null and b/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/build/android/app/src/main/res/values/colors.xml b/build/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..dd33f3b --- /dev/null +++ b/build/android/app/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #3574D4 + #2C5FB8 + #1B2636 + #FFFFFFFF + #FF000000 + diff --git a/build/android/app/src/main/res/values/strings.xml b/build/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3ed9e47 --- /dev/null +++ b/build/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Wails App + diff --git a/build/android/app/src/main/res/values/themes.xml b/build/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..be8a282 --- /dev/null +++ b/build/android/app/src/main/res/values/themes.xml @@ -0,0 +1,14 @@ + + + + diff --git a/build/android/build.gradle b/build/android/build.gradle new file mode 100644 index 0000000..d7fbab3 --- /dev/null +++ b/build/android/build.gradle @@ -0,0 +1,4 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '8.7.3' apply false +} diff --git a/build/android/gradle.properties b/build/android/gradle.properties new file mode 100644 index 0000000..b9d4426 --- /dev/null +++ b/build/android/gradle.properties @@ -0,0 +1,26 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/build/optimize-your-build#parallel +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/build/android/gradle/wrapper/gradle-wrapper.jar b/build/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f8e1ee3 Binary files /dev/null and b/build/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/build/android/gradle/wrapper/gradle-wrapper.properties b/build/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..23449a2 --- /dev/null +++ b/build/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/build/android/gradlew b/build/android/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/build/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright ÂĐ 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions ÂŦ$varÂŧ, ÂŦ${var}Âŧ, ÂŦ${var:-default}Âŧ, ÂŦ${var+SET}Âŧ, +# ÂŦ${var#prefix}Âŧ, ÂŦ${var%suffix}Âŧ, and ÂŦ$( cmd )Âŧ; +# * compound commands having a testable exit status, especially ÂŦcaseÂŧ; +# * various built-in commands including ÂŦcommandÂŧ, ÂŦsetÂŧ, and ÂŦulimitÂŧ. +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/build/android/gradlew.bat b/build/android/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/build/android/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/build/android/main_android.go b/build/android/main_android.go new file mode 100644 index 0000000..70a7164 --- /dev/null +++ b/build/android/main_android.go @@ -0,0 +1,11 @@ +//go:build android + +package main + +import "github.com/wailsapp/wails/v3/pkg/application" + +func init() { + // Register main function to be called when the Android app initializes + // This is necessary because in c-shared build mode, main() is not automatically called + application.RegisterAndroidMain(main) +} diff --git a/build/android/scripts/deps/install_deps.go b/build/android/scripts/deps/install_deps.go new file mode 100644 index 0000000..d9dfedf --- /dev/null +++ b/build/android/scripts/deps/install_deps.go @@ -0,0 +1,151 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +func main() { + fmt.Println("Checking Android development dependencies...") + fmt.Println() + + errors := []string{} + + // Check Go + if !checkCommand("go", "version") { + errors = append(errors, "Go is not installed. Install from https://go.dev/dl/") + } else { + fmt.Println("✓ Go is installed") + } + + // Check ANDROID_HOME + androidHome := os.Getenv("ANDROID_HOME") + if androidHome == "" { + androidHome = os.Getenv("ANDROID_SDK_ROOT") + } + if androidHome == "" { + // Try common default locations + home, _ := os.UserHomeDir() + possiblePaths := []string{ + filepath.Join(home, "Android", "Sdk"), + filepath.Join(home, "Library", "Android", "sdk"), + "/usr/local/share/android-sdk", + } + for _, p := range possiblePaths { + if _, err := os.Stat(p); err == nil { + androidHome = p + break + } + } + } + + if androidHome == "" { + errors = append(errors, "ANDROID_HOME not set. Install Android Studio and set ANDROID_HOME environment variable") + } else { + fmt.Printf("✓ ANDROID_HOME: %s\n", androidHome) + } + + // Check adb + if !checkCommand("adb", "version") { + if androidHome != "" { + platformTools := filepath.Join(androidHome, "platform-tools") + errors = append(errors, fmt.Sprintf("adb not found. Add %s to PATH", platformTools)) + } else { + errors = append(errors, "adb not found. Install Android SDK Platform-Tools") + } + } else { + fmt.Println("✓ adb is installed") + } + + // Check emulator + if !checkCommand("emulator", "-list-avds") { + if androidHome != "" { + emulatorPath := filepath.Join(androidHome, "emulator") + errors = append(errors, fmt.Sprintf("emulator not found. Add %s to PATH", emulatorPath)) + } else { + errors = append(errors, "emulator not found. Install Android Emulator via SDK Manager") + } + } else { + fmt.Println("✓ Android Emulator is installed") + } + + // Check NDK + ndkHome := os.Getenv("ANDROID_NDK_HOME") + if ndkHome == "" && androidHome != "" { + // Look for NDK in default location + ndkDir := filepath.Join(androidHome, "ndk") + if entries, err := os.ReadDir(ndkDir); err == nil { + for _, entry := range entries { + if entry.IsDir() { + ndkHome = filepath.Join(ndkDir, entry.Name()) + break + } + } + } + } + + if ndkHome == "" { + errors = append(errors, "Android NDK not found. Install NDK via Android Studio > SDK Manager > SDK Tools > NDK (Side by side)") + } else { + fmt.Printf("✓ Android NDK: %s\n", ndkHome) + } + + // Check Java + if !checkCommand("java", "-version") { + errors = append(errors, "Java not found. Install JDK 11+ (OpenJDK recommended)") + } else { + fmt.Println("✓ Java is installed") + } + + // Check for AVD (Android Virtual Device) + if checkCommand("emulator", "-list-avds") { + cmd := exec.Command("emulator", "-list-avds") + output, err := cmd.Output() + if err == nil && len(strings.TrimSpace(string(output))) > 0 { + avds := strings.Split(strings.TrimSpace(string(output)), "\n") + fmt.Printf("✓ Found %d Android Virtual Device(s)\n", len(avds)) + } else { + fmt.Println("⚠ No Android Virtual Devices found. Create one via Android Studio > Tools > Device Manager") + } + } + + fmt.Println() + + if len(errors) > 0 { + fmt.Println("❌ Missing dependencies:") + for _, err := range errors { + fmt.Printf(" - %s\n", err) + } + fmt.Println() + fmt.Println("Setup instructions:") + fmt.Println("1. Install Android Studio: https://developer.android.com/studio") + fmt.Println("2. Open SDK Manager and install:") + fmt.Println(" - Android SDK Platform (API 34)") + fmt.Println(" - Android SDK Build-Tools") + fmt.Println(" - Android SDK Platform-Tools") + fmt.Println(" - Android Emulator") + fmt.Println(" - NDK (Side by side)") + fmt.Println("3. Set environment variables:") + if runtime.GOOS == "darwin" { + fmt.Println(" export ANDROID_HOME=$HOME/Library/Android/sdk") + } else { + fmt.Println(" export ANDROID_HOME=$HOME/Android/Sdk") + } + fmt.Println(" export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator") + fmt.Println("4. Create an AVD via Android Studio > Tools > Device Manager") + os.Exit(1) + } + + fmt.Println("✓ All Android development dependencies are installed!") +} + +func checkCommand(name string, args ...string) bool { + cmd := exec.Command(name, args...) + cmd.Stdout = nil + cmd.Stderr = nil + return cmd.Run() == nil +} diff --git a/build/android/settings.gradle b/build/android/settings.gradle new file mode 100644 index 0000000..a3f3ec3 --- /dev/null +++ b/build/android/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "WailsApp" +include ':app' diff --git a/build/appicon.icon/Assets/wails_icon_vector.svg b/build/appicon.icon/Assets/wails_icon_vector.svg new file mode 100644 index 0000000..b099222 --- /dev/null +++ b/build/appicon.icon/Assets/wails_icon_vector.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/build/appicon.icon/icon.json b/build/appicon.icon/icon.json new file mode 100644 index 0000000..ecf1849 --- /dev/null +++ b/build/appicon.icon/icon.json @@ -0,0 +1,51 @@ +{ + "fill" : { + "automatic-gradient" : "extended-gray:1.00000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "fill-specializations" : [ + { + "appearance" : "dark", + "value" : { + "solid" : "srgb:0.92143,0.92145,0.92144,1.00000" + } + }, + { + "appearance" : "tinted", + "value" : { + "solid" : "srgb:0.83742,0.83744,0.83743,1.00000" + } + } + ], + "image-name" : "wails_icon_vector.svg", + "name" : "wails_icon_vector", + "position" : { + "scale" : 1.25, + "translation-in-points" : [ + 36.890625, + 4.96875 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "specular" : true, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/build/config.yml b/build/config.yml new file mode 100644 index 0000000..a4d4f52 --- /dev/null +++ b/build/config.yml @@ -0,0 +1,78 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: "3" + +# This information is used to generate the build assets. +info: + companyName: "Vuong" # The name of the company + productName: "DevToolbox" # The name of the application + productIdentifier: "com.vuon9.devtoolbox" # The unique product identifier + description: "DevToolbox" # The application description + copyright: "(c) 2026, Vuong" # Copyright text + comments: "DevToolbox is a set of useful tools for daily development." # Comments + version: "0.0.1" # The application version + # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional) + # # Should match the name of your .icon file without the extension + # # If not set and Assets.car exists, defaults to "appicon" + +# iOS build configuration (uncomment to customise iOS project generation) +# Note: Keys under `ios` OVERRIDE values under `info` when set. +# ios: +# # The iOS bundle identifier used in the generated Xcode project (CFBundleIdentifier) +# bundleID: "com.mycompany.myproduct" +# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName) +# displayName: "My Product" +# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion) +# version: "0.0.1" +# # The company/organisation name for templates and project settings +# company: "My Company" +# # Additional comments to embed in Info.plist metadata +# comments: "Some Product Comments" + +# Dev mode configuration +dev_mode: + root_path: . + log_level: warn + debounce: 1000 + ignore: + dir: + - .git + - node_modules + - frontend + - bin + file: + - .DS_Store + - .gitignore + - .gitkeep + watched_extension: + - "*.go" + - "*.js" # Watch for changes to JS/TS files included using the //wails:include directive. + - "*.ts" # The frontend directory will be excluded entirely by the setting above. + git_ignore: true + executes: + - cmd: wails3 build DEV=true + type: blocking + - cmd: wails3 task common:dev:frontend + type: background + - cmd: wails3 task run + type: primary + +# File Associations +# More information at: https://v3.wails.io/noit/done/yet +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor +# mimeType: image/jpeg # (optional) + +# Other data +other: + - name: My Other Data diff --git a/build/darwin/Assets.car b/build/darwin/Assets.car new file mode 100644 index 0000000..4def9c3 Binary files /dev/null and b/build/darwin/Assets.car differ diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist index 14121ef..2a2d114 100644 --- a/build/darwin/Info.dev.plist +++ b/build/darwin/Info.dev.plist @@ -2,67 +2,33 @@ CFBundlePackageType - APPL + APPL CFBundleName - {{.Info.ProductName}} + My Product CFBundleExecutable - {{.OutputFilename}} + devtoolbox CFBundleIdentifier - com.wails.{{.Name}} + com.example.devtoolbox CFBundleVersion - {{.Info.ProductVersion}} + 0.1.0 CFBundleGetInfoString - {{.Info.Comments}} + This is a comment CFBundleShortVersionString - {{.Info.ProductVersion}} + 0.1.0 CFBundleIconFile - iconfile + icons + CFBundleIconName + appicon LSMinimumSystemVersion - 10.13.0 + 10.15.0 NSHighResolutionCapable - true + 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}} + ÂĐ 2026, My Company NSAppTransportSecurity NSAllowsLocalNetworking - + \ No newline at end of file diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist index d17a747..1e0aef6 100644 --- a/build/darwin/Info.plist +++ b/build/darwin/Info.plist @@ -2,62 +2,28 @@ CFBundlePackageType - APPL + APPL CFBundleName - {{.Info.ProductName}} + My Product CFBundleExecutable - {{.OutputFilename}} + devtoolbox CFBundleIdentifier - com.wails.{{.Name}} + com.example.devtoolbox CFBundleVersion - {{.Info.ProductVersion}} + 0.1.0 CFBundleGetInfoString - {{.Info.Comments}} + This is a comment CFBundleShortVersionString - {{.Info.ProductVersion}} + 0.1.0 CFBundleIconFile - iconfile + icons + CFBundleIconName + appicon LSMinimumSystemVersion - 10.13.0 + 10.15.0 NSHighResolutionCapable - true + 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}} + ÂĐ 2026, My Company - + \ No newline at end of file diff --git a/build/darwin/Taskfile.yml b/build/darwin/Taskfile.yml new file mode 100644 index 0000000..041bd20 --- /dev/null +++ b/build/darwin/Taskfile.yml @@ -0,0 +1,207 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)" + # KEYCHAIN_PROFILE: "my-notarize-profile" + # ENTITLEMENTS: "build/darwin/entitlements.plist" + + # Docker image for cross-compilation (used when building on non-macOS) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application + cmds: + - task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + OUTPUT: '{{.OUTPUT}}' + vars: + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + + build:native: + summary: Builds the application natively on macOS + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: darwin + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + CGO_CFLAGS: "-mmacosx-version-min=10.15" + CGO_LDFLAGS: "-mmacosx-version-min=10.15" + MACOSX_DEPLOYMENT_TARGET: "10.15" + + build:docker: + summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for cross-compilation. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - mkdir -p {{.BIN_DIR}} + - mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" + vars: + DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + # Handles both relative (=> ../) and absolute (=> /) paths + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + build:universal: + summary: Builds darwin universal binary (arm64 + amd64) + deps: + - task: build + vars: + ARCH: amd64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" + - task: build + vars: + ARCH: arm64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + cmds: + - task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}' + + build:universal:lipo:native: + summary: Creates universal binary using native lipo (macOS) + internal: true + cmds: + - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + build:universal:lipo:go: + summary: Creates universal binary using wails3 tool lipo (Linux/Windows) + internal: true + cmds: + - wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + package: + summary: Packages the application into a `.app` bundle + deps: + - task: build + cmds: + - task: create:app:bundle + + package:universal: + summary: Packages darwin universal binary (arm64 + amd64) + deps: + - task: build:universal + cmds: + - task: create:app:bundle + + + create:app:bundle: + summary: Creates an `.app` bundle + cmds: + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + - | + if [ -f build/darwin/Assets.car ]; then + cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + fi + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" + - cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents" + - task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}' + + codesign:adhoc: + summary: Ad-hoc signs the app bundle (macOS only) + internal: true + cmds: + - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app" + + codesign:skip: + summary: Skips codesigning when cross-compiling + internal: true + cmds: + - 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."' + + run: + cmds: + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - | + if [ -f build/darwin/Assets.car ]; then + cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + fi + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" + - cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist" + - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}' + + sign: + summary: Signs the application bundle with Developer ID + desc: | + Signs the .app bundle for distribution. + Configure SIGN_IDENTITY in the vars section at the top of this file. + deps: + - task: package + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_IDENTITY}}" ]' + msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" + + sign:notarize: + summary: Signs and notarizes the application bundle + desc: | + Signs the .app bundle and submits it for notarization. + Configure SIGN_IDENTITY and KEYCHAIN_PROFILE in the vars section at the top of this file. + + Setup (one-time): + wails3 signing credentials --apple-id "you@email.com" --team-id "TEAMID" --password "app-specific-password" --profile "my-profile" + deps: + - task: package + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}} + preconditions: + - sh: '[ -n "{{.SIGN_IDENTITY}}" ]' + msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" + - sh: '[ -n "{{.KEYCHAIN_PROFILE}}" ]' + msg: "KEYCHAIN_PROFILE is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" diff --git a/build/darwin/icons.icns b/build/darwin/icons.icns new file mode 100644 index 0000000..1b5bd4c Binary files /dev/null and b/build/darwin/icons.icns differ diff --git a/build/docker/Dockerfile.cross b/build/docker/Dockerfile.cross new file mode 100644 index 0000000..ddae7aa --- /dev/null +++ b/build/docker/Dockerfile.cross @@ -0,0 +1,198 @@ +# Cross-compile Wails v3 apps to any platform +# +# Darwin: Zig + macOS SDK +# Linux: Native GCC when host matches target, Zig for cross-arch +# Windows: Zig + bundled mingw +# +# Usage: +# docker build -t wails-cross -f Dockerfile.cross . +# docker run --rm -v $(pwd):/app wails-cross darwin arm64 +# docker run --rm -v $(pwd):/app wails-cross darwin amd64 +# docker run --rm -v $(pwd):/app wails-cross linux amd64 +# docker run --rm -v $(pwd):/app wails-cross linux arm64 +# docker run --rm -v $(pwd):/app wails-cross windows amd64 +# docker run --rm -v $(pwd):/app wails-cross windows arm64 + +FROM golang:1.25-bookworm + +ARG TARGETARCH + +# Install base tools, GCC, and GTK/WebKit dev packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl xz-utils nodejs npm pkg-config gcc libc6-dev \ + libgtk-3-dev libwebkit2gtk-4.1-dev \ + libgtk-4-dev libwebkitgtk-6.0-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Zig - automatically selects correct binary for host architecture +ARG ZIG_VERSION=0.14.0 +RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ + curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" \ + | tar -xJ -C /opt \ + && ln -s /opt/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}/zig /usr/local/bin/zig + +# Download macOS SDK (required for darwin targets) +ARG MACOS_SDK_VERSION=14.5 +RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \ + | tar -xJ -C /opt \ + && mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk + +ENV MACOS_SDK_PATH=/opt/macos-sdk + +# Create Zig CC wrappers for cross-compilation targets +# Darwin and Windows use Zig; Linux uses native GCC (run with --platform for cross-arch) + +# Darwin arm64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -mmacosx-version-min=*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-darwin-arm64 + +# Darwin amd64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -mmacosx-version-min=*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-darwin-amd64 + +# Windows amd64 - uses Zig's bundled mingw +COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -Wl,*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -target x86_64-windows-gnu $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-windows-amd64 + +# Windows arm64 - uses Zig's bundled mingw +COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -Wl,*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -target aarch64-windows-gnu $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-windows-arm64 + +# Build script +COPY <<'SCRIPT' /usr/local/bin/build.sh +#!/bin/sh +set -e + +OS=${1:-darwin} +ARCH=${2:-arm64} + +case "${OS}-${ARCH}" in + darwin-arm64|darwin-aarch64) + export CC=zcc-darwin-arm64 + export GOARCH=arm64 + export GOOS=darwin + ;; + darwin-amd64|darwin-x86_64) + export CC=zcc-darwin-amd64 + export GOARCH=amd64 + export GOOS=darwin + ;; + linux-arm64|linux-aarch64) + export CC=gcc + export GOARCH=arm64 + export GOOS=linux + ;; + linux-amd64|linux-x86_64) + export CC=gcc + export GOARCH=amd64 + export GOOS=linux + ;; + windows-arm64|windows-aarch64) + export CC=zcc-windows-arm64 + export GOARCH=arm64 + export GOOS=windows + ;; + windows-amd64|windows-x86_64) + export CC=zcc-windows-amd64 + export GOARCH=amd64 + export GOOS=windows + ;; + *) + echo "Usage: " + echo " os: darwin, linux, windows" + echo " arch: amd64, arm64" + exit 1 + ;; +esac + +export CGO_ENABLED=1 +export CGO_CFLAGS="-w" + +# Build frontend if exists and not already built (host may have built it) +if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then + (cd frontend && npm install --silent && npm run build --silent) +fi + +# Build +APP=${APP_NAME:-$(basename $(pwd))} +mkdir -p bin + +EXT="" +LDFLAGS="-s -w" +if [ "$GOOS" = "windows" ]; then + EXT=".exe" + LDFLAGS="-s -w -H windowsgui" +fi + +go build -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} . +echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}" +SCRIPT +RUN chmod +x /usr/local/bin/build.sh + +WORKDIR /app +ENTRYPOINT ["/usr/local/bin/build.sh"] +CMD ["darwin", "arm64"] diff --git a/build/docker/Dockerfile.server b/build/docker/Dockerfile.server new file mode 100644 index 0000000..58fb64f --- /dev/null +++ b/build/docker/Dockerfile.server @@ -0,0 +1,41 @@ +# Wails Server Mode Dockerfile +# Multi-stage build for minimal image size + +# Build stage +FROM golang:alpine AS builder + +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache git + +# Copy source code +COPY . . + +# Remove local replace directive if present (for production builds) +RUN sed -i '/^replace/d' go.mod || true + +# Download dependencies +RUN go mod tidy + +# Build the server binary +RUN go build -tags server -ldflags="-s -w" -o server . + +# Runtime stage - minimal image +FROM gcr.io/distroless/static-debian12 + +# Copy the binary +COPY --from=builder /app/server /server + +# Copy frontend assets +COPY --from=builder /app/frontend/dist /frontend/dist + +# Expose the default port +EXPOSE 8080 + +# Bind to all interfaces (required for Docker) +# Can be overridden at runtime with -e WAILS_SERVER_HOST=... +ENV WAILS_SERVER_HOST=0.0.0.0 + +# Run the server +ENTRYPOINT ["/server"] diff --git a/build/ios/Assets.xcassets b/build/ios/Assets.xcassets new file mode 100644 index 0000000..46fbb87 --- /dev/null +++ b/build/ios/Assets.xcassets @@ -0,0 +1,116 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "images" : [ + { + "filename" : "icon-20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon-20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "icon-29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "icon-40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon-40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "icon-60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "icon-60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "icon-20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "icon-20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon-29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "icon-29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "icon-40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon-76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "icon-76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "icon-83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "icon-1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ] +} \ No newline at end of file diff --git a/build/ios/Info.dev.plist b/build/ios/Info.dev.plist new file mode 100644 index 0000000..deb801c --- /dev/null +++ b/build/ios/Info.dev.plist @@ -0,0 +1,62 @@ + + + + + CFBundleExecutable + devtoolbox + CFBundleIdentifier + com.example.devtoolbox.dev + CFBundleName + My Product (Dev) + CFBundleDisplayName + My Product (Dev) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0-dev + CFBundleVersion + 0.1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 15.0 + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + + WailsDevelopmentMode + + + NSHumanReadableCopyright + ÂĐ 2026, My Company + + + CFBundleGetInfoString + This is a comment + + + \ No newline at end of file diff --git a/build/ios/Info.plist b/build/ios/Info.plist new file mode 100644 index 0000000..274c588 --- /dev/null +++ b/build/ios/Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleExecutable + devtoolbox + CFBundleIdentifier + com.example.devtoolbox + CFBundleName + My Product + CFBundleDisplayName + My Product + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 0.1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 15.0 + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + + NSHumanReadableCopyright + ÂĐ 2026, My Company + + + CFBundleGetInfoString + This is a comment + + + \ No newline at end of file diff --git a/build/ios/LaunchScreen.storyboard b/build/ios/LaunchScreen.storyboard new file mode 100644 index 0000000..4edaeb3 --- /dev/null +++ b/build/ios/LaunchScreen.storyboard @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/ios/Taskfile.yml b/build/ios/Taskfile.yml new file mode 100644 index 0000000..8c27f08 --- /dev/null +++ b/build/ios/Taskfile.yml @@ -0,0 +1,293 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + BUNDLE_ID: '{{.BUNDLE_ID | default "com.wails.app"}}' + # SDK_PATH is computed lazily at task-level to avoid errors on non-macOS systems + # Each task that needs it defines SDK_PATH in its own vars section + +tasks: + install:deps: + summary: Check and install iOS development dependencies + cmds: + - go run build/ios/scripts/deps/install_deps.go + env: + TASK_FORCE_YES: '{{if .YES}}true{{else}}false{{end}}' + prompt: This will check and install iOS development dependencies. Continue? + + # Note: Bindings generation may show CGO warnings for iOS C imports. + # These warnings are harmless and don't affect the generated bindings, + # as the generator only needs to parse Go types, not C implementations. + build: + summary: Creates a build of the application for iOS + deps: + - task: generate:ios:overlay + - task: generate:ios:xcode + - task: common:go:mod:tidy + - task: generate:ios:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - echo "Building iOS app {{.APP_NAME}}..." + - go build -buildmode=c-archive -overlay build/ios/xcode/overlay.json {{.BUILD_FLAGS}} -o {{.OUTPUT}}.a + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,ios -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags ios,debug -buildvcs=false -gcflags=all="-l"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + env: + GOOS: ios + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default "arm64"}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + CGO_CFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0' + CGO_LDFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator' + + compile:objc: + summary: Compile Objective-C iOS wrapper + vars: + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + cmds: + - xcrun -sdk iphonesimulator clang -target arm64-apple-ios15.0-simulator -isysroot {{.SDK_PATH}} -framework Foundation -framework UIKit -framework WebKit -o {{.BIN_DIR}}/{{.APP_NAME}} build/ios/main.m + - codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}" + + package: + summary: Packages a production build of the application into a `.app` bundle + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:app:bundle + + create:app:bundle: + summary: Creates an iOS `.app` bundle + cmds: + - rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.app" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app" + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/" + - cp build/ios/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/" + - | + # Compile asset catalog and embed icons in the app bundle + APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.app" + AC_IN="build/ios/xcode/main/Assets.xcassets" + if [ -d "$AC_IN" ]; then + TMP_AC=$(mktemp -d) + xcrun actool \ + --compile "$TMP_AC" \ + --app-icon AppIcon \ + --platform iphonesimulator \ + --minimum-deployment-target 15.0 \ + --product-type com.apple.product-type.application \ + --target-device iphone \ + --target-device ipad \ + --output-partial-info-plist "$APP_BUNDLE/assetcatalog_generated_info.plist" \ + "$AC_IN" + if [ -f "$TMP_AC/Assets.car" ]; then + cp -f "$TMP_AC/Assets.car" "$APP_BUNDLE/Assets.car" + fi + rm -rf "$TMP_AC" + if [ -f "$APP_BUNDLE/assetcatalog_generated_info.plist" ]; then + /usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true + fi + fi + - codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app" + + deploy-simulator: + summary: Deploy to iOS Simulator + deps: [package] + cmds: + - xcrun simctl terminate booted {{.BUNDLE_ID}} 2>/dev/null || true + - xcrun simctl uninstall booted {{.BUNDLE_ID}} 2>/dev/null || true + - xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.app" + - xcrun simctl launch booted {{.BUNDLE_ID}} + + compile:ios: + summary: Compile the iOS executable from Go archive and main.m + deps: + - task: build + vars: + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + cmds: + - | + MAIN_M=build/ios/xcode/main/main.m + if [ ! -f "$MAIN_M" ]; then + MAIN_M=build/ios/main.m + fi + xcrun -sdk iphonesimulator clang \ + -target arm64-apple-ios15.0-simulator \ + -isysroot {{.SDK_PATH}} \ + -framework Foundation -framework UIKit -framework WebKit \ + -framework Security -framework CoreFoundation \ + -lresolv \ + -o "{{.BIN_DIR}}/{{.APP_NAME | lower}}" \ + "$MAIN_M" "{{.BIN_DIR}}/{{.APP_NAME}}.a" + + generate:ios:bindings: + internal: true + summary: Generates bindings for iOS with proper CGO flags + sources: + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + vars: + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true + env: + GOOS: ios + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default "arm64"}}' + CGO_CFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0' + CGO_LDFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator' + + ensure-simulator: + internal: true + summary: Ensure iOS Simulator is running and booted + silent: true + cmds: + - | + if ! xcrun simctl list devices booted | grep -q "Booted"; then + echo "Starting iOS Simulator..." + # Get first available iPhone device + DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone" | head -1 | grep -o "[A-F0-9-]\{36\}" || true) + if [ -z "$DEVICE_ID" ]; then + echo "No iPhone simulator found. Creating one..." + RUNTIME=$(xcrun simctl list runtimes | grep iOS | tail -1 | awk '{print $NF}') + DEVICE_ID=$(xcrun simctl create "iPhone 15 Pro" "iPhone 15 Pro" "$RUNTIME") + fi + # Boot the device + echo "Booting device $DEVICE_ID..." + xcrun simctl boot "$DEVICE_ID" 2>/dev/null || true + # Open Simulator app + open -a Simulator + # Wait for boot (max 30 seconds) + for i in {1..30}; do + if xcrun simctl list devices booted | grep -q "Booted"; then + echo "Simulator booted successfully" + break + fi + sleep 1 + done + # Final check + if ! xcrun simctl list devices booted | grep -q "Booted"; then + echo "Failed to boot simulator after 30 seconds" + exit 1 + fi + fi + preconditions: + - sh: command -v xcrun + msg: "xcrun not found. Please run 'wails3 task ios:install:deps' to install iOS development dependencies" + + generate:ios:overlay: + internal: true + summary: Generate Go build overlay and iOS shim + sources: + - build/config.yml + generates: + - build/ios/xcode/overlay.json + - build/ios/xcode/gen/main_ios.gen.go + cmds: + - wails3 ios overlay:gen -out build/ios/xcode/overlay.json -config build/config.yml + + generate:ios:xcode: + internal: true + summary: Generate iOS Xcode project structure and assets + sources: + - build/config.yml + - build/appicon.png + generates: + - build/ios/xcode/main/main.m + - build/ios/xcode/main/Assets.xcassets/**/* + - build/ios/xcode/project.pbxproj + cmds: + - wails3 ios xcode:gen -outdir build/ios/xcode -config build/config.yml + + run: + summary: Run the application in iOS Simulator + deps: + - task: ensure-simulator + - task: compile:ios + cmds: + - rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - cp "{{.BIN_DIR}}/{{.APP_NAME | lower}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/{{.APP_NAME | lower}}" + - cp build/ios/Info.dev.plist "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Info.plist" + - | + # Compile asset catalog and embed icons for dev bundle + APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + AC_IN="build/ios/xcode/main/Assets.xcassets" + if [ -d "$AC_IN" ]; then + TMP_AC=$(mktemp -d) + xcrun actool \ + --compile "$TMP_AC" \ + --app-icon AppIcon \ + --platform iphonesimulator \ + --minimum-deployment-target 15.0 \ + --product-type com.apple.product-type.application \ + --target-device iphone \ + --target-device ipad \ + --output-partial-info-plist "$APP_BUNDLE/assetcatalog_generated_info.plist" \ + "$AC_IN" + if [ -f "$TMP_AC/Assets.car" ]; then + cp -f "$TMP_AC/Assets.car" "$APP_BUNDLE/Assets.car" + fi + rm -rf "$TMP_AC" + if [ -f "$APP_BUNDLE/assetcatalog_generated_info.plist" ]; then + /usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true + fi + fi + - codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - xcrun simctl terminate booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true + - xcrun simctl uninstall booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true + - xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - xcrun simctl launch booted "com.wails.{{.APP_NAME | lower}}.dev" + + xcode: + summary: Open the generated Xcode project for this app + cmds: + - task: generate:ios:xcode + - open build/ios/xcode/main.xcodeproj + + logs: + summary: Stream iOS Simulator logs filtered to this app + cmds: + - | + xcrun simctl spawn booted log stream \ + --level debug \ + --style compact \ + --predicate 'senderImagePath CONTAINS[c] "{{.APP_NAME | lower}}.app/" OR composedMessage CONTAINS[c] "{{.APP_NAME | lower}}" OR eventMessage CONTAINS[c] "{{.APP_NAME | lower}}" OR process == "{{.APP_NAME | lower}}" OR category CONTAINS[c] "{{.APP_NAME | lower}}"' + + logs:dev: + summary: Stream logs for the dev bundle (used by `task ios:run`) + cmds: + - | + xcrun simctl spawn booted log stream \ + --level debug \ + --style compact \ + --predicate 'senderImagePath CONTAINS[c] ".dev.app/" OR subsystem == "com.wails.{{.APP_NAME | lower}}.dev" OR process == "{{.APP_NAME | lower}}"' + + logs:wide: + summary: Wide log stream to help discover the exact process/bundle identifiers + cmds: + - | + xcrun simctl spawn booted log stream \ + --level debug \ + --style compact \ + --predicate 'senderImagePath CONTAINS[c] ".app/"' \ No newline at end of file diff --git a/build/ios/app_options_default.go b/build/ios/app_options_default.go new file mode 100644 index 0000000..04e4f1b --- /dev/null +++ b/build/ios/app_options_default.go @@ -0,0 +1,10 @@ +//go:build !ios + +package main + +import "github.com/wailsapp/wails/v3/pkg/application" + +// modifyOptionsForIOS is a no-op on non-iOS platforms +func modifyOptionsForIOS(opts *application.Options) { + // No modifications needed for non-iOS platforms +} \ No newline at end of file diff --git a/build/ios/app_options_ios.go b/build/ios/app_options_ios.go new file mode 100644 index 0000000..8f6ac31 --- /dev/null +++ b/build/ios/app_options_ios.go @@ -0,0 +1,11 @@ +//go:build ios + +package main + +import "github.com/wailsapp/wails/v3/pkg/application" + +// modifyOptionsForIOS adjusts the application options for iOS +func modifyOptionsForIOS(opts *application.Options) { + // Disable signal handlers on iOS to prevent crashes + opts.DisableDefaultSignalHandler = true +} \ No newline at end of file diff --git a/build/ios/build.sh b/build/ios/build.sh new file mode 100644 index 0000000..1adb7a9 --- /dev/null +++ b/build/ios/build.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -e + +# Build configuration +APP_NAME="devtoolbox" +BUNDLE_ID="com.example.devtoolbox" +VERSION="0.1.0" +BUILD_NUMBER="0.1.0" +BUILD_DIR="build/ios" +TARGET="simulator" + +echo "Building iOS app: $APP_NAME" +echo "Bundle ID: $BUNDLE_ID" +echo "Version: $VERSION ($BUILD_NUMBER)" +echo "Target: $TARGET" + +# Ensure build directory exists +mkdir -p "$BUILD_DIR" + +# Determine SDK and target architecture +if [ "$TARGET" = "simulator" ]; then + SDK="iphonesimulator" + ARCH="arm64-apple-ios15.0-simulator" +elif [ "$TARGET" = "device" ]; then + SDK="iphoneos" + ARCH="arm64-apple-ios15.0" +else + echo "Unknown target: $TARGET" + exit 1 +fi + +# Get SDK path +SDK_PATH=$(xcrun --sdk $SDK --show-sdk-path) + +# Compile the application +echo "Compiling with SDK: $SDK" +xcrun -sdk $SDK clang \ + -target $ARCH \ + -isysroot "$SDK_PATH" \ + -framework Foundation \ + -framework UIKit \ + -framework WebKit \ + -framework CoreGraphics \ + -o "$BUILD_DIR/$APP_NAME" \ + "$BUILD_DIR/main.m" + +# Create app bundle +echo "Creating app bundle..." +APP_BUNDLE="$BUILD_DIR/$APP_NAME.app" +rm -rf "$APP_BUNDLE" +mkdir -p "$APP_BUNDLE" + +# Move executable +mv "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/" + +# Copy Info.plist +cp "$BUILD_DIR/Info.plist" "$APP_BUNDLE/" + +# Sign the app +echo "Signing app..." +codesign --force --sign - "$APP_BUNDLE" + +echo "Build complete: $APP_BUNDLE" + +# Deploy to simulator if requested +if [ "$TARGET" = "simulator" ]; then + echo "Deploying to simulator..." + xcrun simctl terminate booted "$BUNDLE_ID" 2>/dev/null || true + xcrun simctl install booted "$APP_BUNDLE" + xcrun simctl launch booted "$BUNDLE_ID" + echo "App launched on simulator" +fi \ No newline at end of file diff --git a/build/ios/entitlements.plist b/build/ios/entitlements.plist new file mode 100644 index 0000000..cc5d958 --- /dev/null +++ b/build/ios/entitlements.plist @@ -0,0 +1,21 @@ + + + + + + get-task-allow + + + + com.apple.security.app-sandbox + + + + com.apple.security.network.client + + + + com.apple.security.files.user-selected.read-only + + + \ No newline at end of file diff --git a/build/ios/icon.png b/build/ios/icon.png new file mode 100644 index 0000000..be7d591 --- /dev/null +++ b/build/ios/icon.png @@ -0,0 +1,3 @@ +# iOS Icon Placeholder +# This file should be replaced with the actual app icon (1024x1024 PNG) +# The build process will generate all required icon sizes from this base icon \ No newline at end of file diff --git a/build/ios/main.m b/build/ios/main.m new file mode 100644 index 0000000..366767a --- /dev/null +++ b/build/ios/main.m @@ -0,0 +1,23 @@ +//go:build ios +// Minimal bootstrap: delegate comes from Go archive (WailsAppDelegate) +#import +#include + +// External Go initialization function from the c-archive (declare before use) +extern void WailsIOSMain(); + +int main(int argc, char * argv[]) { + @autoreleasepool { + // Disable buffering so stdout/stderr from Go log.Printf flush immediately + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + + // Start Go runtime on a background queue to avoid blocking main thread/UI + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + WailsIOSMain(); + }); + + // Run UIApplicationMain using WailsAppDelegate provided by the Go archive + return UIApplicationMain(argc, argv, nil, @"WailsAppDelegate"); + } +} \ No newline at end of file diff --git a/build/ios/main_ios.go b/build/ios/main_ios.go new file mode 100644 index 0000000..b75a403 --- /dev/null +++ b/build/ios/main_ios.go @@ -0,0 +1,24 @@ +//go:build ios + +package main + +import ( + "C" +) + +// For iOS builds, we need to export a function that can be called from Objective-C +// This wrapper allows us to keep the original main.go unmodified + +//export WailsIOSMain +func WailsIOSMain() { + // DO NOT lock the goroutine to the current OS thread on iOS! + // This causes signal handling issues: + // "signal 16 received on thread with no signal stack" + // "fatal error: non-Go code disabled sigaltstack" + // iOS apps run in a sandboxed environment where the Go runtime's + // signal handling doesn't work the same way as desktop platforms. + + // Call the actual main function from main.go + // This ensures all the user's code is executed + main() +} \ No newline at end of file diff --git a/build/ios/project.pbxproj b/build/ios/project.pbxproj new file mode 100644 index 0000000..3a20d6d --- /dev/null +++ b/build/ios/project.pbxproj @@ -0,0 +1,222 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = {}; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + C0DEBEEF0000000000000001 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000002 /* main.m */; }; + C0DEBEEF00000000000000F1 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000101 /* UIKit.framework */; }; + C0DEBEEF00000000000000F2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000102 /* Foundation.framework */; }; + C0DEBEEF00000000000000F3 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000103 /* WebKit.framework */; }; + C0DEBEEF00000000000000F4 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000104 /* Security.framework */; }; + C0DEBEEF00000000000000F5 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000105 /* CoreFoundation.framework */; }; + C0DEBEEF00000000000000F6 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000106 /* libresolv.tbd */; }; + C0DEBEEF00000000000000F7 /* My Product.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000107 /* My Product.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + C0DEBEEF0000000000000002 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + C0DEBEEF0000000000000003 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C0DEBEEF0000000000000004 /* My Product.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "My Product.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + C0DEBEEF0000000000000101 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000102 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000103 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000104 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000105 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000106 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.text-based-dylib-definition; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000107 /* My Product.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "My Product.a"; path = ../../../bin/My Product.a; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + C0DEBEEF0000000000000010 = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000020 /* Products */, + C0DEBEEF0000000000000045 /* Frameworks */, + C0DEBEEF0000000000000030 /* main */, + ); + sourceTree = ""; + }; + C0DEBEEF0000000000000020 /* Products */ = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000004 /* My Product.app */, + ); + name = Products; + sourceTree = ""; + }; + C0DEBEEF0000000000000030 /* main */ = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000002 /* main.m */, + C0DEBEEF0000000000000003 /* Info.plist */, + ); + path = main; + sourceTree = SOURCE_ROOT; + }; + C0DEBEEF0000000000000045 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000101 /* UIKit.framework */, + C0DEBEEF0000000000000102 /* Foundation.framework */, + C0DEBEEF0000000000000103 /* WebKit.framework */, + C0DEBEEF0000000000000104 /* Security.framework */, + C0DEBEEF0000000000000105 /* CoreFoundation.framework */, + C0DEBEEF0000000000000106 /* libresolv.tbd */, + C0DEBEEF0000000000000107 /* My Product.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + C0DEBEEF0000000000000040 /* My Product */ = { + isa = PBXNativeTarget; + buildConfigurationList = C0DEBEEF0000000000000070 /* Build configuration list for PBXNativeTarget "My Product" */; + buildPhases = ( + C0DEBEEF0000000000000055 /* Prebuild: Wails Go Archive */, + C0DEBEEF0000000000000050 /* Sources */, + C0DEBEEF0000000000000056 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "My Product"; + productName = "My Product"; + productReference = C0DEBEEF0000000000000004 /* My Product.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + C0DEBEEF0000000000000060 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1500; + ORGANIZATIONNAME = "My Company"; + TargetAttributes = { + C0DEBEEF0000000000000040 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = C0DEBEEF0000000000000080 /* Build configuration list for PBXProject "main" */; + compatibilityVersion = "Xcode 15.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = C0DEBEEF0000000000000010; + productRefGroup = C0DEBEEF0000000000000020 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C0DEBEEF0000000000000040 /* My Product */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXFrameworksBuildPhase section */ + C0DEBEEF0000000000000056 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C0DEBEEF00000000000000F7 /* My Product.a in Frameworks */, + C0DEBEEF00000000000000F1 /* UIKit.framework in Frameworks */, + C0DEBEEF00000000000000F2 /* Foundation.framework in Frameworks */, + C0DEBEEF00000000000000F3 /* WebKit.framework in Frameworks */, + C0DEBEEF00000000000000F4 /* Security.framework in Frameworks */, + C0DEBEEF00000000000000F5 /* CoreFoundation.framework in Frameworks */, + C0DEBEEF00000000000000F6 /* libresolv.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + C0DEBEEF0000000000000055 /* Prebuild: Wails Go Archive */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Prebuild: Wails Go Archive"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\nAPP_ROOT=\"${PROJECT_DIR}/../../..\"\nSDK_PATH=$(xcrun --sdk iphonesimulator --show-sdk-path)\nexport GOOS=ios\nexport GOARCH=arm64\nexport CGO_ENABLED=1\nexport CGO_CFLAGS=\"-isysroot ${SDK_PATH} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0\"\nexport CGO_LDFLAGS=\"-isysroot ${SDK_PATH} -target arm64-apple-ios15.0-simulator\"\ncd \"${APP_ROOT}\"\n# Ensure overlay exists\nif [ ! -f build/ios/xcode/overlay.json ]; then\n wails3 ios overlay:gen -out build/ios/xcode/overlay.json -config build/config.yml || true\nfi\n# Build Go c-archive if missing or older than sources\nif [ ! -f bin/My Product.a ]; then\n echo \"Building Go c-archive...\"\n go build -buildmode=c-archive -overlay build/ios/xcode/overlay.json -o bin/My Product.a\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C0DEBEEF0000000000000050 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C0DEBEEF0000000000000001 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + C0DEBEEF0000000000000090 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = main/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.devtoolbox"; + PRODUCT_NAME = "My Product"; + CODE_SIGNING_ALLOWED = NO; + SDKROOT = iphonesimulator; + }; + name = Debug; + }; + C0DEBEEF00000000000000A0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = main/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.devtoolbox"; + PRODUCT_NAME = "My Product"; + CODE_SIGNING_ALLOWED = NO; + SDKROOT = iphonesimulator; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C0DEBEEF0000000000000070 /* Build configuration list for PBXNativeTarget "My Product" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C0DEBEEF0000000000000090 /* Debug */, + C0DEBEEF00000000000000A0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + C0DEBEEF0000000000000080 /* Build configuration list for PBXProject "main" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C0DEBEEF0000000000000090 /* Debug */, + C0DEBEEF00000000000000A0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = C0DEBEEF0000000000000060 /* Project object */; +} diff --git a/build/ios/scripts/deps/install_deps.go b/build/ios/scripts/deps/install_deps.go new file mode 100644 index 0000000..88ed47a --- /dev/null +++ b/build/ios/scripts/deps/install_deps.go @@ -0,0 +1,319 @@ +// install_deps.go - iOS development dependency checker +// This script checks for required iOS development tools. +// It's designed to be portable across different shells by using Go instead of shell scripts. +// +// Usage: +// go run install_deps.go # Interactive mode +// TASK_FORCE_YES=true go run install_deps.go # Auto-accept prompts +// CI=true go run install_deps.go # CI mode (auto-accept) + +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strings" +) + +type Dependency struct { + Name string + CheckFunc func() (bool, string) // Returns (success, details) + Required bool + InstallCmd []string + InstallMsg string + SuccessMsg string + FailureMsg string +} + +func main() { + fmt.Println("Checking iOS development dependencies...") + fmt.Println("=" + strings.Repeat("=", 50)) + fmt.Println() + + hasErrors := false + dependencies := []Dependency{ + { + Name: "Xcode", + CheckFunc: func() (bool, string) { + // Check if xcodebuild exists + if !checkCommand([]string{"xcodebuild", "-version"}) { + return false, "" + } + // Get version info + out, err := exec.Command("xcodebuild", "-version").Output() + if err != nil { + return false, "" + } + lines := strings.Split(string(out), "\n") + if len(lines) > 0 { + return true, strings.TrimSpace(lines[0]) + } + return true, "" + }, + Required: true, + InstallMsg: "Please install Xcode from the Mac App Store:\n https://apps.apple.com/app/xcode/id497799835\n Xcode is REQUIRED for iOS development (includes iOS SDKs, simulators, and frameworks)", + SuccessMsg: "✅ Xcode found", + FailureMsg: "❌ Xcode not found (REQUIRED)", + }, + { + Name: "Xcode Developer Path", + CheckFunc: func() (bool, string) { + // Check if xcode-select points to a valid Xcode path + out, err := exec.Command("xcode-select", "-p").Output() + if err != nil { + return false, "xcode-select not configured" + } + path := strings.TrimSpace(string(out)) + + // Check if path exists and is in Xcode.app + if _, err := os.Stat(path); err != nil { + return false, "Invalid Xcode path" + } + + // Verify it's pointing to Xcode.app (not just Command Line Tools) + if !strings.Contains(path, "Xcode.app") { + return false, fmt.Sprintf("Points to %s (should be Xcode.app)", path) + } + + return true, path + }, + Required: true, + InstallCmd: []string{"sudo", "xcode-select", "-s", "/Applications/Xcode.app/Contents/Developer"}, + InstallMsg: "Xcode developer path needs to be configured", + SuccessMsg: "✅ Xcode developer path configured", + FailureMsg: "❌ Xcode developer path not configured correctly", + }, + { + Name: "iOS SDK", + CheckFunc: func() (bool, string) { + // Get the iOS Simulator SDK path + cmd := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path") + output, err := cmd.Output() + if err != nil { + return false, "Cannot find iOS SDK" + } + sdkPath := strings.TrimSpace(string(output)) + + // Check if the SDK path exists + if _, err := os.Stat(sdkPath); err != nil { + return false, "iOS SDK path not found" + } + + // Check for UIKit framework (essential for iOS development) + uikitPath := fmt.Sprintf("%s/System/Library/Frameworks/UIKit.framework", sdkPath) + if _, err := os.Stat(uikitPath); err != nil { + return false, "UIKit.framework not found" + } + + // Get SDK version + versionCmd := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-version") + versionOut, _ := versionCmd.Output() + version := strings.TrimSpace(string(versionOut)) + + return true, fmt.Sprintf("iOS %s SDK", version) + }, + Required: true, + InstallMsg: "iOS SDK comes with Xcode. Please ensure Xcode is properly installed.", + SuccessMsg: "✅ iOS SDK found with UIKit framework", + FailureMsg: "❌ iOS SDK not found or incomplete", + }, + { + Name: "iOS Simulator Runtime", + CheckFunc: func() (bool, string) { + if !checkCommand([]string{"xcrun", "simctl", "help"}) { + return false, "" + } + // Check if we can list runtimes + out, err := exec.Command("xcrun", "simctl", "list", "runtimes").Output() + if err != nil { + return false, "Cannot access simulator" + } + // Count iOS runtimes + lines := strings.Split(string(out), "\n") + count := 0 + var versions []string + for _, line := range lines { + if strings.Contains(line, "iOS") && !strings.Contains(line, "unavailable") { + count++ + // Extract version number + if parts := strings.Fields(line); len(parts) > 2 { + for _, part := range parts { + if strings.HasPrefix(part, "(") && strings.HasSuffix(part, ")") { + versions = append(versions, strings.Trim(part, "()")) + break + } + } + } + } + } + if count > 0 { + return true, fmt.Sprintf("%d runtime(s): %s", count, strings.Join(versions, ", ")) + } + return false, "No iOS runtimes installed" + }, + Required: true, + InstallMsg: "iOS Simulator runtimes come with Xcode. You may need to download them:\n Xcode → Settings → Platforms → iOS", + SuccessMsg: "✅ iOS Simulator runtime available", + FailureMsg: "❌ iOS Simulator runtime not available", + }, + } + + // Check each dependency + for _, dep := range dependencies { + success, details := dep.CheckFunc() + if success { + msg := dep.SuccessMsg + if details != "" { + msg = fmt.Sprintf("%s (%s)", dep.SuccessMsg, details) + } + fmt.Println(msg) + } else { + fmt.Println(dep.FailureMsg) + if details != "" { + fmt.Printf(" Details: %s\n", details) + } + if dep.Required { + hasErrors = true + if len(dep.InstallCmd) > 0 { + fmt.Println() + fmt.Println(" " + dep.InstallMsg) + fmt.Printf(" Fix command: %s\n", strings.Join(dep.InstallCmd, " ")) + if promptUser("Do you want to run this command?") { + fmt.Println("Running command...") + cmd := exec.Command(dep.InstallCmd[0], dep.InstallCmd[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + if err := cmd.Run(); err != nil { + fmt.Printf("Command failed: %v\n", err) + os.Exit(1) + } + fmt.Println("✅ Command completed. Please run this check again.") + } else { + fmt.Printf(" Please run manually: %s\n", strings.Join(dep.InstallCmd, " ")) + } + } else { + fmt.Println(" " + dep.InstallMsg) + } + } + } + } + + // Check for iPhone simulators + fmt.Println() + fmt.Println("Checking for iPhone simulator devices...") + if !checkCommand([]string{"xcrun", "simctl", "list", "devices"}) { + fmt.Println("❌ Cannot check for iPhone simulators") + hasErrors = true + } else { + out, err := exec.Command("xcrun", "simctl", "list", "devices").Output() + if err != nil { + fmt.Println("❌ Failed to list simulator devices") + hasErrors = true + } else if !strings.Contains(string(out), "iPhone") { + fmt.Println("⚠ïļ No iPhone simulator devices found") + fmt.Println() + + // Get the latest iOS runtime + runtimeOut, err := exec.Command("xcrun", "simctl", "list", "runtimes").Output() + if err != nil { + fmt.Println(" Failed to get iOS runtimes:", err) + } else { + lines := strings.Split(string(runtimeOut), "\n") + var latestRuntime string + for _, line := range lines { + if strings.Contains(line, "iOS") && !strings.Contains(line, "unavailable") { + // Extract runtime identifier + parts := strings.Fields(line) + if len(parts) > 0 { + latestRuntime = parts[len(parts)-1] + } + } + } + + if latestRuntime == "" { + fmt.Println(" No iOS runtime found. Please install iOS simulators in Xcode:") + fmt.Println(" Xcode → Settings → Platforms → iOS") + } else { + fmt.Println(" Would you like to create an iPhone 15 Pro simulator?") + createCmd := []string{"xcrun", "simctl", "create", "iPhone 15 Pro", "iPhone 15 Pro", latestRuntime} + fmt.Printf(" Command: %s\n", strings.Join(createCmd, " ")) + if promptUser("Create simulator?") { + cmd := exec.Command(createCmd[0], createCmd[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Printf(" Failed to create simulator: %v\n", err) + } else { + fmt.Println(" ✅ iPhone 15 Pro simulator created") + } + } else { + fmt.Println(" Skipping simulator creation") + fmt.Printf(" Create manually: %s\n", strings.Join(createCmd, " ")) + } + } + } + } else { + // Count iPhone devices + count := 0 + lines := strings.Split(string(out), "\n") + for _, line := range lines { + if strings.Contains(line, "iPhone") && !strings.Contains(line, "unavailable") { + count++ + } + } + fmt.Printf("✅ %d iPhone simulator device(s) available\n", count) + } + } + + // Final summary + fmt.Println() + fmt.Println("=" + strings.Repeat("=", 50)) + if hasErrors { + fmt.Println("❌ Some required dependencies are missing or misconfigured.") + fmt.Println() + fmt.Println("Quick setup guide:") + fmt.Println("1. Install Xcode from Mac App Store (if not installed)") + fmt.Println("2. Open Xcode once and agree to the license") + fmt.Println("3. Install additional components when prompted") + fmt.Println("4. Run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer") + fmt.Println("5. Download iOS simulators: Xcode → Settings → Platforms → iOS") + fmt.Println("6. Run this check again") + os.Exit(1) + } else { + fmt.Println("✅ All required dependencies are installed!") + fmt.Println(" You're ready for iOS development with Wails!") + } +} + +func checkCommand(args []string) bool { + if len(args) == 0 { + return false + } + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdout = nil + cmd.Stderr = nil + err := cmd.Run() + return err == nil +} + +func promptUser(question string) bool { + // Check if we're in a non-interactive environment + if os.Getenv("CI") != "" || os.Getenv("TASK_FORCE_YES") == "true" { + fmt.Printf("%s [y/N]: y (auto-accepted)\n", question) + return true + } + + reader := bufio.NewReader(os.Stdin) + fmt.Printf("%s [y/N]: ", question) + + response, err := reader.ReadString('\n') + if err != nil { + return false + } + + response = strings.ToLower(strings.TrimSpace(response)) + return response == "y" || response == "yes" +} \ No newline at end of file diff --git a/build/linux/Taskfile.yml b/build/linux/Taskfile.yml new file mode 100644 index 0000000..1285484 --- /dev/null +++ b/build/linux/Taskfile.yml @@ -0,0 +1,225 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # PGP_KEY: "path/to/signing-key.asc" + # SIGN_ROLE: "builder" # Options: origin, maint, archive, builder + # + # Password is stored securely in system keychain. Run: wails3 setup signing + + # Docker image for cross-compilation (used when building on non-Linux or no CC available) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application for Linux + cmds: + # Linux requires CGO - use Docker when: + # 1. Cross-compiling from non-Linux, OR + # 2. No C compiler is available, OR + # 3. Target architecture differs from host architecture (cross-arch compilation) + - task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + OUTPUT: '{{.OUTPUT}}' + vars: + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + # Determine target architecture (defaults to host ARCH if not specified) + TARGET_ARCH: '{{.ARCH | default ARCH}}' + # Check if a C compiler is available (gcc or clang) + HAS_CC: + sh: '(command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1) && echo "true" || echo "false"' + + build:native: + summary: Builds the application natively on Linux + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + - task: generate:dotdesktop + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: linux + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + + build:docker: + summary: Builds for Linux using Docker (for non-Linux hosts or when no C compiler available) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + - task: generate:dotdesktop + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for cross-compilation to Linux. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - mkdir -p {{.BIN_DIR}} + - mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" + vars: + DOCKER_ARCH: '{{.ARCH | default "amd64"}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + package: + summary: Packages the application for Linux + deps: + - task: build + cmds: + - task: create:appimage + - task: create:deb + - task: create:rpm + - task: create:aur + + create:appimage: + summary: Creates an AppImage + dir: build/linux/appimage + deps: + - task: build + - task: generate:dotdesktop + cmds: + - cp "{{.APP_BINARY}}" "{{.APP_NAME}}" + - cp ../../appicon.png "{{.APP_NAME}}.png" + - wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build + vars: + APP_NAME: '{{.APP_NAME}}' + APP_BINARY: '../../../bin/{{.APP_NAME}}' + ICON: '{{.APP_NAME}}.png' + DESKTOP_FILE: '../{{.APP_NAME}}.desktop' + OUTPUT_DIR: '../../../bin' + + create:deb: + summary: Creates a deb package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:deb + + create:rpm: + summary: Creates a rpm package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:rpm + + create:aur: + summary: Creates a arch linux packager package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:aur + + generate:deb: + summary: Creates a deb package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:rpm: + summary: Creates a rpm package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:aur: + summary: Creates a arch linux packager package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:dotdesktop: + summary: Generates a `.desktop` file + dir: build + cmds: + - mkdir -p {{.ROOT_DIR}}/build/linux/appimage + - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}" + vars: + APP_NAME: '{{.APP_NAME}}' + EXEC: '{{.APP_NAME}}' + ICON: '{{.APP_NAME}}' + CATEGORIES: 'Development;' + OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}' + + sign:deb: + summary: Signs the DEB package + desc: | + Signs the .deb package with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:deb + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}} + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" + + sign:rpm: + summary: Signs the RPM package + desc: | + Signs the .rpm package with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:rpm + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" --pgp-key {{.PGP_KEY}} + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" + + sign:packages: + summary: Signs all Linux packages (DEB and RPM) + desc: | + Signs both .deb and .rpm packages with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + cmds: + - task: sign:deb + - task: sign:rpm + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" diff --git a/build/linux/appimage/build.sh b/build/linux/appimage/build.sh new file mode 100644 index 0000000..85901c3 --- /dev/null +++ b/build/linux/appimage/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) 2018-Present Lea Anthony +# SPDX-License-Identifier: MIT + +# Fail script on any error +set -euxo pipefail + +# Define variables +APP_DIR="${APP_NAME}.AppDir" + +# Create AppDir structure +mkdir -p "${APP_DIR}/usr/bin" +cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" +cp "${ICON_PATH}" "${APP_DIR}/" +cp "${DESKTOP_FILE}" "${APP_DIR}/" + +if [[ $(uname -m) == *x86_64* ]]; then + # Download linuxdeploy and make it executable + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + + # Run linuxdeploy to bundle the application + ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage +else + # Download linuxdeploy and make it executable (arm64) + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage + chmod +x linuxdeploy-aarch64.AppImage + + # Run linuxdeploy to bundle the application (arm64) + ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage +fi + +# Rename the generated AppImage +mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage" + diff --git a/build/linux/desktop b/build/linux/desktop new file mode 100644 index 0000000..3cbe74a --- /dev/null +++ b/build/linux/desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.0 +Name=My Product +Comment=A DevToolbox application +# The Exec line includes %u to pass the URL to the application +Exec=/usr/local/bin/devtoolbox %u +Terminal=false +Type=Application +Icon=devtoolbox +Categories=Utility; +StartupWMClass=devtoolbox + + diff --git a/build/linux/nfpm/nfpm.yaml b/build/linux/nfpm/nfpm.yaml new file mode 100644 index 0000000..5fe3946 --- /dev/null +++ b/build/linux/nfpm/nfpm.yaml @@ -0,0 +1,67 @@ +# Feel free to remove those if you don't want/need to use them. +# Make sure to check the documentation at https://nfpm.goreleaser.com +# +# The lines below are called `modelines`. See `:help modeline` + +name: "devtoolbox" +arch: ${GOARCH} +platform: "linux" +version: "0.1.0" +section: "default" +priority: "extra" +maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> +description: "A DevToolbox application" +vendor: "My Company" +homepage: "https://wails.io" +license: "MIT" +release: "1" + +contents: + - src: "./bin/devtoolbox" + dst: "/usr/local/bin/devtoolbox" + - src: "./build/appicon.png" + dst: "/usr/share/icons/hicolor/128x128/apps/devtoolbox.png" + - src: "./build/linux/devtoolbox.desktop" + dst: "/usr/share/applications/devtoolbox.desktop" + +# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1 +depends: + - libgtk-3-0 + - libwebkit2gtk-4.1-0 + +# Distribution-specific overrides for different package formats and WebKit versions +overrides: + # RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0) + rpm: + depends: + - gtk3 + - webkit2gtk4.1 + + # Arch Linux packages (WebKit 4.1) + archlinux: + depends: + - gtk3 + - webkit2gtk-4.1 + +# scripts section to ensure desktop database is updated after install +scripts: + postinstall: "./build/linux/nfpm/scripts/postinstall.sh" + # You can also add preremove, postremove if needed + # preremove: "./build/linux/nfpm/scripts/preremove.sh" + # postremove: "./build/linux/nfpm/scripts/postremove.sh" + +# replaces: +# - foobar +# provides: +# - bar +# depends: +# - gtk3 +# - libwebkit2gtk +# recommends: +# - whatever +# suggests: +# - something-else +# conflicts: +# - not-foo +# - not-bar +# changelog: "changelog.yaml" diff --git a/build/linux/nfpm/scripts/postinstall.sh b/build/linux/nfpm/scripts/postinstall.sh new file mode 100644 index 0000000..4bbb815 --- /dev/null +++ b/build/linux/nfpm/scripts/postinstall.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# Update desktop database for .desktop file changes +# This makes the application appear in application menus and registers its capabilities. +if command -v update-desktop-database >/dev/null 2>&1; then + echo "Updating desktop database..." + update-desktop-database -q /usr/share/applications +else + echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2 +fi + +# Update MIME database for custom URL schemes (x-scheme-handler) +# This ensures the system knows how to handle your custom protocols. +if command -v update-mime-database >/dev/null 2>&1; then + echo "Updating MIME database..." + update-mime-database -n /usr/share/mime +else + echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2 +fi + +exit 0 diff --git a/build/linux/nfpm/scripts/postremove.sh b/build/linux/nfpm/scripts/postremove.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/build/linux/nfpm/scripts/postremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/build/linux/nfpm/scripts/preinstall.sh b/build/linux/nfpm/scripts/preinstall.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/build/linux/nfpm/scripts/preinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/build/linux/nfpm/scripts/preremove.sh b/build/linux/nfpm/scripts/preremove.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/build/linux/nfpm/scripts/preremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/build/windows/Taskfile.yml b/build/windows/Taskfile.yml new file mode 100644 index 0000000..77b620b --- /dev/null +++ b/build/windows/Taskfile.yml @@ -0,0 +1,183 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # SIGN_CERTIFICATE: "path/to/certificate.pfx" + # SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE + # TIMESTAMP_SERVER: "http://timestamp.digicert.com" + # + # Password is stored securely in system keychain. Run: wails3 setup signing + + # Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application for Windows + cmds: + # Auto-detect CGO: if CGO_ENABLED=1, use Docker; otherwise use native Go cross-compile + - task: '{{if and (ne OS "windows") (eq .CGO_ENABLED "1")}}build:docker{{else}}build:native{{end}}' + vars: + ARCH: '{{.ARCH}}' + DEV: '{{.DEV}}' + vars: + # Default to CGO_ENABLED=0 if not explicitly set + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + + build:native: + summary: Builds the application using native Go cross-compilation + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + cmds: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe" + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}' + env: + GOOS: windows + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + GOARCH: '{{.ARCH | default ARCH}}' + + build:docker: + summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for CGO cross-compilation. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - task: generate:syso + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - rm -f *.syso + vars: + DOCKER_ARCH: '{{.ARCH | default "amd64"}}' + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + package: + summary: Packages the application + cmds: + - task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}' + vars: + FORMAT: '{{.FORMAT | default "nsis"}}' + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: '{{.ARCH | default ARCH}}' + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - | + {{if eq OS "windows"}} + makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi + {{else}} + makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + {{end}} + vars: + ARCH: '{{.ARCH | default ARCH}}' + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + + create:msix:package: + summary: Creates an MSIX package + deps: + - task: build + cmds: + - |- + wails3 tool msix \ + --config "{{.ROOT_DIR}}/wails.json" \ + --name "{{.APP_NAME}}" \ + --executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \ + --arch "{{.ARCH}}" \ + --out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \ + {{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \ + {{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \ + {{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}} + vars: + ARCH: '{{.ARCH | default ARCH}}' + CERT_PATH: '{{.CERT_PATH | default ""}}' + PUBLISHER: '{{.PUBLISHER | default ""}}' + USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}' + + install:msix:tools: + summary: Installs tools required for MSIX packaging + cmds: + - wails3 tool msix-install-tools + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}.exe' + + sign: + summary: Signs the Windows executable + desc: | + Signs the .exe with an Authenticode certificate. + Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: build + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]' + msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml" + + sign:installer: + summary: Signs the NSIS installer + desc: | + Creates and signs the NSIS installer. + Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:nsis:installer + cmds: + - wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]' + msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml" diff --git a/build/windows/icon.ico b/build/windows/icon.ico index f334798..bfa0690 100644 Binary files a/build/windows/icon.ico and b/build/windows/icon.ico differ diff --git a/build/windows/info.json b/build/windows/info.json index 9727946..6ef6a97 100644 --- a/build/windows/info.json +++ b/build/windows/info.json @@ -1,15 +1,15 @@ { "fixed": { - "file_version": "{{.Info.ProductVersion}}" + "file_version": "0.1.0" }, "info": { "0000": { - "ProductVersion": "{{.Info.ProductVersion}}", - "CompanyName": "{{.Info.CompanyName}}", - "FileDescription": "{{.Info.ProductName}}", - "LegalCopyright": "{{.Info.Copyright}}", - "ProductName": "{{.Info.ProductName}}", - "Comments": "{{.Info.Comments}}" + "ProductVersion": "0.1.0", + "CompanyName": "My Company", + "FileDescription": "A DevToolbox application", + "LegalCopyright": "ÂĐ 2026, My Company", + "ProductName": "My Product", + "Comments": "This is a comment" } } } \ No newline at end of file diff --git a/build/windows/msix/app_manifest.xml b/build/windows/msix/app_manifest.xml new file mode 100644 index 0000000..a4cc148 --- /dev/null +++ b/build/windows/msix/app_manifest.xml @@ -0,0 +1,55 @@ + + + + + + + My Product + My Company + A DevToolbox application + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/windows/msix/template.xml b/build/windows/msix/template.xml new file mode 100644 index 0000000..d4c1f31 --- /dev/null +++ b/build/windows/msix/template.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + false + My Product + My Company + A DevToolbox application + Assets\AppIcon.png + + + + + + + diff --git a/build/windows/installer/project.nsi b/build/windows/nsis/project.nsi similarity index 84% rename from build/windows/installer/project.nsi rename to build/windows/nsis/project.nsi index 654ae2e..5f28f11 100644 --- a/build/windows/installer/project.nsi +++ b/build/windows/nsis/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. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually ## from outside of Wails for debugging and development of the installer. -## +## ## For development first make a wails nsis build to populate the "wails_tools.nsh": ## > wails build --target windows/amd64 --nsis ## Then you can call makensis on this file with specifying the path to your binary: @@ -17,13 +17,13 @@ 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 wails_tools.nsh 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 INFO_PROJECTNAME "my-project" # Default "DevToolbox" +## !define INFO_COMPANYNAME "My Company" # Default "My Company" +## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "ÂĐ 2026, My Company" ### ## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" ## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" @@ -62,7 +62,7 @@ ManifestDPIAware true !insertmacro MUI_PAGE_INSTFILES # Installing page. !insertmacro MUI_PAGE_FINISH # Finished installation page. -!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page +!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page !insertmacro MUI_LANGUAGE "English" # Set the Language of the installer @@ -71,7 +71,7 @@ ManifestDPIAware true #!finalize 'signtool --file "%1"' Name "${INFO_PRODUCTNAME}" -OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +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. @@ -85,7 +85,7 @@ Section !insertmacro wails.webview2runtime SetOutPath $INSTDIR - + !insertmacro wails.files CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" @@ -93,11 +93,11 @@ Section !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 diff --git a/build/windows/installer/wails_tools.nsh b/build/windows/nsis/wails_tools.nsh similarity index 87% rename from build/windows/installer/wails_tools.nsh rename to build/windows/nsis/wails_tools.nsh index 2f6d321..9d15737 100644 --- a/build/windows/installer/wails_tools.nsh +++ b/build/windows/nsis/wails_tools.nsh @@ -5,19 +5,19 @@ !include "FileFunc.nsh" !ifndef INFO_PROJECTNAME - !define INFO_PROJECTNAME "{{.Name}}" + !define INFO_PROJECTNAME "DevToolbox" !endif !ifndef INFO_COMPANYNAME - !define INFO_COMPANYNAME "{{.Info.CompanyName}}" + !define INFO_COMPANYNAME "My Company" !endif !ifndef INFO_PRODUCTNAME - !define INFO_PRODUCTNAME "{{.Info.ProductName}}" + !define INFO_PRODUCTNAME "My Product" !endif !ifndef INFO_PRODUCTVERSION - !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" + !define INFO_PRODUCTVERSION "0.1.0" !endif !ifndef INFO_COPYRIGHT - !define INFO_COPYRIGHT "{{.Info.Copyright}}" + !define INFO_COPYRIGHT "ÂĐ 2026, My Company" !endif !ifndef PRODUCT_EXECUTABLE !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" @@ -163,17 +163,17 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" Goto ok ${EndIf} ${EndIf} - + SetDetailsPrint both DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" SetDetailsPrint listonly - + InitPluginsDir CreateDirectory "$pluginsdir\webview2bootstrapper" SetOutPath "$pluginsdir\webview2bootstrapper" - File "tmp\MicrosoftEdgeWebview2Setup.exe" + File "MicrosoftEdgeWebview2Setup.exe" ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' - + SetDetailsPrint both ok: !macroend @@ -203,20 +203,12 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" !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 @@ -235,15 +227,10 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" !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 + +!macroend \ No newline at end of file diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest index 17e1a23..426c2d5 100644 --- a/build/windows/wails.exe.manifest +++ b/build/windows/wails.exe.manifest @@ -1,6 +1,6 @@ - + @@ -12,4 +12,11 @@ permonitorv2,permonitor + + + + + + + \ No newline at end of file diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js new file mode 100644 index 0000000..458def9 --- /dev/null +++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js @@ -0,0 +1,14 @@ +//@ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH  MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +function configure() { + Object.freeze(Object.assign($Create.Events, { + })); +} + +configure(); diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts new file mode 100644 index 0000000..793ee3e --- /dev/null +++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -0,0 +1,14 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH  MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import type { Events } from "@wailsio/runtime"; + +declare module "@wailsio/runtime" { + namespace Events { + interface CustomEvents { + "time": string; + } + } +} diff --git a/bun.lock b/frontend/bun.lock similarity index 99% rename from bun.lock rename to frontend/bun.lock index e1cd49d..69877bd 100644 --- a/bun.lock +++ b/frontend/bun.lock @@ -7,6 +7,7 @@ "@carbon/icons-react": "^11.71.0", "@carbon/react": "^1.97.0", "@carbon/styles": "^1.96.0", + "@wailsio/runtime": "^3.0.0-alpha.79", "cronstrue": "^3.9.0", "diff": "^8.0.2", "js-beautify": "^1.15.4", @@ -290,6 +291,8 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "@wailsio/runtime": ["@wailsio/runtime@3.0.0-alpha.79", "", {}, "sha512-NITzxKmJsMEruc39L166lbPJVECxzcbdqpHVqOOF7Cu/7Zqk/e3B/gNpkUjhNyo5rVb3V1wpS8oEgLUmpu1cwA=="], + "abbrev": ["abbrev@2.0.0", "", {}, "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..dc6ba24 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + + DevToolbox + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/frontend/package.json similarity index 84% rename from package.json rename to frontend/package.json index d8f5294..6c8e795 100644 --- a/package.json +++ b/frontend/package.json @@ -5,13 +5,15 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build:dev": "vite build --minify false --mode development", + "build": "vite build --mode production", "preview": "vite preview" }, "dependencies": { "@carbon/icons-react": "^11.71.0", "@carbon/react": "^1.97.0", "@carbon/styles": "^1.96.0", + "@wailsio/runtime": "^3.0.0-alpha.79", "cronstrue": "^3.9.0", "diff": "^8.0.2", "js-beautify": "^1.15.4", @@ -34,4 +36,4 @@ "@vitejs/plugin-react": "^4.0.0", "vite": "^5.4.21" } -} +} \ No newline at end of file diff --git a/package.json.md5 b/frontend/package.json.md5 similarity index 100% rename from package.json.md5 rename to frontend/package.json.md5 diff --git a/src/App.css b/frontend/src/App.css similarity index 100% rename from src/App.css rename to frontend/src/App.css diff --git a/src/App.jsx b/frontend/src/App.jsx similarity index 98% rename from src/App.jsx rename to frontend/src/App.jsx index 69d6174..f9cee85 100644 --- a/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,7 +5,7 @@ import { Theme, IconButton, OverflowMenu, OverflowMenuItem } from '@carbon/react import { Settings } from '@carbon/icons-react'; // Tools Imports -import UnixTimeConverter from './pages/UnixTimeConverter'; +import DateTimeConverter from './pages/DateTimeConverter'; import JwtDebugger from './pages/JwtDebugger'; import RegExpTester from './pages/RegExpTester'; import CronJobParser from './pages/CronJobParser'; @@ -101,7 +101,7 @@ function App() { switch (activeTool) { case 'text-converter': return ; case 'string-utilities': return ; - case 'unix-time': return ; + case 'datetime-converter': return ; case 'jwt': return ; case 'barcode': return ; case 'data-generator': return ; diff --git a/src/assets/fonts/OFL.txt b/frontend/src/assets/fonts/OFL.txt similarity index 100% rename from src/assets/fonts/OFL.txt rename to frontend/src/assets/fonts/OFL.txt diff --git a/src/assets/fonts/nunito-v16-latin-regular.woff2 b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 similarity index 100% rename from src/assets/fonts/nunito-v16-latin-regular.woff2 rename to frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 diff --git a/src/assets/images/logo-universal.png b/frontend/src/assets/images/logo-universal.png similarity index 100% rename from src/assets/images/logo-universal.png rename to frontend/src/assets/images/logo-universal.png diff --git a/frontend/src/components/AnalogClockWidget.jsx b/frontend/src/components/AnalogClockWidget.jsx new file mode 100644 index 0000000..ed25b69 --- /dev/null +++ b/frontend/src/components/AnalogClockWidget.jsx @@ -0,0 +1,183 @@ +import React from 'react'; + +/** + * Analog Clock Widget Component + * Displays an SVG-based analog clock face + */ +export default function AnalogClockWidget({ date }) { + if (!date) return null; + + const currentDate = new Date(date); + const hours = currentDate.getHours(); + const minutes = currentDate.getMinutes(); + const seconds = currentDate.getSeconds(); + + // Calculate angles + const hourAngle = ((hours % 12) + minutes / 60) * 30; // 360 / 12 = 30 + const minuteAngle = (minutes + seconds / 60) * 6; // 360 / 60 = 6 + const secondAngle = seconds * 6; + + const size = 200; + const center = size / 2; + const radius = size / 2 - 10; + + // Generate hour markers + const hourMarkers = []; + for (let i = 0; i < 12; i++) { + const angle = (i * 30 - 90) * (Math.PI / 180); + const x1 = center + (radius - 15) * Math.cos(angle); + const y1 = center + (radius - 15) * Math.sin(angle); + const x2 = center + (radius - 5) * Math.cos(angle); + const y2 = center + (radius - 5) * Math.sin(angle); + + hourMarkers.push( + + ); + } + + // Generate minute markers + const minuteMarkers = []; + for (let i = 0; i < 60; i++) { + if (i % 5 === 0) continue; // Skip hour positions + const angle = (i * 6 - 90) * (Math.PI / 180); + const x1 = center + (radius - 8) * Math.cos(angle); + const y1 = center + (radius - 8) * Math.sin(angle); + const x2 = center + (radius - 5) * Math.cos(angle); + const y2 = center + (radius - 5) * Math.sin(angle); + + minuteMarkers.push( + + ); + } + + // Calculate hand positions + const hourHandLength = radius * 0.5; + const minuteHandLength = radius * 0.7; + const secondHandLength = radius * 0.85; + + const hourX = center + hourHandLength * Math.cos((hourAngle - 90) * (Math.PI / 180)); + const hourY = center + hourHandLength * Math.sin((hourAngle - 90) * (Math.PI / 180)); + + const minuteX = center + minuteHandLength * Math.cos((minuteAngle - 90) * (Math.PI / 180)); + const minuteY = center + minuteHandLength * Math.sin((minuteAngle - 90) * (Math.PI / 180)); + + const secondX = center + secondHandLength * Math.cos((secondAngle - 90) * (Math.PI / 180)); + const secondY = center + secondHandLength * Math.sin((secondAngle - 90) * (Math.PI / 180)); + + // Format digital time + const ampm = hours >= 12 ? 'PM' : 'AM'; + const displayHours = hours % 12 || 12; + const displayMinutes = minutes.toString().padStart(2, '0'); + const displaySeconds = seconds.toString().padStart(2, '0'); + + return ( +
+ + {/* Clock face */} + + + {/* Hour markers */} + {hourMarkers} + + {/* Minute markers */} + {minuteMarkers} + + {/* Hour hand */} + + + {/* Minute hand */} + + + {/* Second hand */} + + + {/* Center dot */} + + + + {/* Digital display */} +
+
+ {displayHours}:{displayMinutes}:{displaySeconds} +
+
+ {ampm} +
+
+
+ ); +} diff --git a/frontend/src/components/CalendarWidget.jsx b/frontend/src/components/CalendarWidget.jsx new file mode 100644 index 0000000..5432e00 --- /dev/null +++ b/frontend/src/components/CalendarWidget.jsx @@ -0,0 +1,218 @@ +import React from 'react'; + +/** + * Calendar Widget Component + * Displays a mini calendar with the selected date highlighted + */ +export default function CalendarWidget({ date, onDateSelect }) { + if (!date) return null; + + const currentDate = new Date(date); + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + const day = currentDate.getDate(); + + // Get first day of month and number of days + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const daysInPrevMonth = new Date(year, month, 0).getDate(); + + // Month names + const monthNames = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + + // Day names + const dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; + + // Generate calendar days + const days = []; + + // Previous month days + for (let i = firstDay - 1; i >= 0; i--) { + days.push({ + day: daysInPrevMonth - i, + currentMonth: false, + isToday: false, + isSelected: false, + }); + } + + // Current month days + const today = new Date(); + for (let i = 1; i <= daysInMonth; i++) { + const isToday = i === today.getDate() && + month === today.getMonth() && + year === today.getFullYear(); + const isSelected = i === day; + + days.push({ + day: i, + currentMonth: true, + isToday, + isSelected, + }); + } + + // Next month days to fill the grid + const remainingCells = 42 - days.length; // 6 rows * 7 columns + for (let i = 1; i <= remainingCells; i++) { + days.push({ + day: i, + currentMonth: false, + isToday: false, + isSelected: false, + }); + } + + const handleDayClick = (dayInfo, index) => { + if (!dayInfo.currentMonth || !onDateSelect) return; + + // Calculate the actual date + const clickedDate = new Date(year, month, dayInfo.day); + onDateSelect(clickedDate); + }; + + const goToPrevMonth = () => { + if (onDateSelect) { + const newDate = new Date(year, month - 1, day); + onDateSelect(newDate); + } + }; + + const goToNextMonth = () => { + if (onDateSelect) { + const newDate = new Date(year, month + 1, day); + onDateSelect(newDate); + } + }; + + return ( +
+ {/* Header */} +
+ + +
+ {monthNames[month]} {year} +
+ + +
+ + {/* Day headers */} +
+ {dayNames.map((name) => ( +
+ {name} +
+ ))} +
+ + {/* Calendar grid */} +
+ {days.map((dayInfo, index) => ( + + ))} +
+ + {/* Selected date display */} +
+ Selected: {currentDate.toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + })} +
+
+ ); +} diff --git a/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx similarity index 97% rename from src/components/Sidebar.jsx rename to frontend/src/components/Sidebar.jsx index d0318e0..a55c28e 100644 --- a/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import logo from '../assets/images/logo-universal.png'; export function Sidebar({ activeTool, setActiveTool, isVisible, toggleSidebar }) { @@ -17,7 +17,7 @@ export function Sidebar({ activeTool, setActiveTool, isVisible, toggleSidebar }) { id: 'text-converter', name: 'Text Converter', icon: '🔄' }, { id: 'string-utilities', name: 'String Utilities', icon: 'ðŸ§ĩ' }, { id: 'number-converter', name: 'Number Converter', icon: 'ðŸ”Ē' }, - { id: 'unix-time', name: 'Unix Time Converter', icon: '🕒' }, + { id: 'datetime-converter', name: 'DateTime Converter', icon: '🕒' }, { id: 'jwt', name: 'JWT Debugger', icon: 'ðŸ›Ąïļ' }, { id: 'barcode', name: 'Barcode / QR Code', icon: 'â–Ģ' }, { id: 'data-generator', name: 'Data Generator', icon: '📊' }, diff --git a/src/components/ToolUI.jsx b/frontend/src/components/ToolUI.jsx similarity index 100% rename from src/components/ToolUI.jsx rename to frontend/src/components/ToolUI.jsx diff --git a/src/components/index.js b/frontend/src/components/index.js similarity index 100% rename from src/components/index.js rename to frontend/src/components/index.js diff --git a/src/components/inputs/ToolCopyButton.jsx b/frontend/src/components/inputs/ToolCopyButton.jsx similarity index 100% rename from src/components/inputs/ToolCopyButton.jsx rename to frontend/src/components/inputs/ToolCopyButton.jsx diff --git a/src/components/inputs/ToolInput.jsx b/frontend/src/components/inputs/ToolInput.jsx similarity index 100% rename from src/components/inputs/ToolInput.jsx rename to frontend/src/components/inputs/ToolInput.jsx diff --git a/src/components/inputs/ToolInputGroup.jsx b/frontend/src/components/inputs/ToolInputGroup.jsx similarity index 100% rename from src/components/inputs/ToolInputGroup.jsx rename to frontend/src/components/inputs/ToolInputGroup.jsx diff --git a/src/components/inputs/ToolTabBar.jsx b/frontend/src/components/inputs/ToolTabBar.jsx similarity index 100% rename from src/components/inputs/ToolTabBar.jsx rename to frontend/src/components/inputs/ToolTabBar.jsx diff --git a/src/components/inputs/ToolTextArea.jsx b/frontend/src/components/inputs/ToolTextArea.jsx similarity index 100% rename from src/components/inputs/ToolTextArea.jsx rename to frontend/src/components/inputs/ToolTextArea.jsx diff --git a/src/components/inputs/index.js b/frontend/src/components/inputs/index.js similarity index 100% rename from src/components/inputs/index.js rename to frontend/src/components/inputs/index.js diff --git a/src/components/layout/ToolLayout.jsx b/frontend/src/components/layout/ToolLayout.jsx similarity index 100% rename from src/components/layout/ToolLayout.jsx rename to frontend/src/components/layout/ToolLayout.jsx diff --git a/src/components/layout/ToolLayoutToggle.jsx b/frontend/src/components/layout/ToolLayoutToggle.jsx similarity index 100% rename from src/components/layout/ToolLayoutToggle.jsx rename to frontend/src/components/layout/ToolLayoutToggle.jsx diff --git a/src/components/layout/ToolVerticalSplit.jsx b/frontend/src/components/layout/ToolVerticalSplit.jsx similarity index 100% rename from src/components/layout/ToolVerticalSplit.jsx rename to frontend/src/components/layout/ToolVerticalSplit.jsx diff --git a/src/components/layout/constants.js b/frontend/src/components/layout/constants.js similarity index 100% rename from src/components/layout/constants.js rename to frontend/src/components/layout/constants.js diff --git a/src/components/layout/index.js b/frontend/src/components/layout/index.js similarity index 100% rename from src/components/layout/index.js rename to frontend/src/components/layout/index.js diff --git a/src/hooks/useLayoutToggle.js b/frontend/src/hooks/useLayoutToggle.js similarity index 100% rename from src/hooks/useLayoutToggle.js rename to frontend/src/hooks/useLayoutToggle.js diff --git a/src/index.scss b/frontend/src/index.scss similarity index 100% rename from src/index.scss rename to frontend/src/index.scss diff --git a/src/main.jsx b/frontend/src/main.jsx similarity index 100% rename from src/main.jsx rename to frontend/src/main.jsx diff --git a/src/pages/BarcodeGenerator.jsx b/frontend/src/pages/BarcodeGenerator.jsx similarity index 98% rename from src/pages/BarcodeGenerator.jsx rename to frontend/src/pages/BarcodeGenerator.jsx index e929c86..39ea939 100644 --- a/src/pages/BarcodeGenerator.jsx +++ b/frontend/src/pages/BarcodeGenerator.jsx @@ -3,7 +3,7 @@ import { Button, Dropdown, InlineLoading } from '@carbon/react'; import { Renew, Download } from '@carbon/icons-react'; import { ToolHeader, ToolPane, ToolSplitPane, ToolLayoutToggle } from '../components/ToolUI'; import useLayoutToggle from '../hooks/useLayoutToggle'; -import { Backend } from '../utils/backendBridge'; +import { BarcodeService } from '../../bindings/devtoolbox/service'; const BARCODE_STANDARDS = [ { value: 'QR', label: 'QR Code (2D)' }, @@ -157,7 +157,7 @@ export default function BarcodeGenerator() { setError(''); try { - const response = await Backend.BarcodeService.GenerateBarcode({ + const response = await BarcodeService.GenerateBarcode({ content: content.trim(), standard, size, diff --git a/src/pages/CodeFormatter/index.jsx b/frontend/src/pages/CodeFormatter/index.jsx similarity index 97% rename from src/pages/CodeFormatter/index.jsx rename to frontend/src/pages/CodeFormatter/index.jsx index 02393d6..e8886af 100644 --- a/src/pages/CodeFormatter/index.jsx +++ b/frontend/src/pages/CodeFormatter/index.jsx @@ -3,7 +3,7 @@ import { Button, Select, SelectItem, TextInput, IconButton } from '@carbon/react import { Code, TrashCan, Close } from '@carbon/icons-react'; import { ToolHeader, ToolControls, ToolPane, ToolSplitPane, ToolLayoutToggle } from '../../components/ToolUI'; import useLayoutToggle from '../../hooks/useLayoutToggle'; -import { Backend } from '../../utils/backendBridge'; +import { CodeFormatterService } from '../../../bindings/devtoolbox/service'; const FORMATTERS = [ { id: 'json', name: 'JSON', supportsFilter: true, filterPlaceholder: '.users[] | select(.age > 18) | .name' }, @@ -67,7 +67,7 @@ export default function CodeFormatter() { } try { - const result = await Backend.CodeFormatterService.Format({ + const result = await CodeFormatterService.Format({ input, formatType, filter: undefined, @@ -100,7 +100,7 @@ export default function CodeFormatter() { } try { - const result = await Backend.CodeFormatterService.Format({ + const result = await CodeFormatterService.Format({ input, formatType, filter: undefined, @@ -135,7 +135,7 @@ export default function CodeFormatter() { } try { - const result = await Backend.CodeFormatterService.Format({ + const result = await CodeFormatterService.Format({ input: formattedOutput, // Always use formatted output as source formatType, filter: filter.trim(), diff --git a/src/pages/ColorConverter.jsx b/frontend/src/pages/ColorConverter.jsx similarity index 93% rename from src/pages/ColorConverter.jsx rename to frontend/src/pages/ColorConverter.jsx index 1191c43..313d14e 100644 --- a/src/pages/ColorConverter.jsx +++ b/frontend/src/pages/ColorConverter.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback, useReducer, useMemo, useRef } from 'react'; -import { Button, TextInput, Tile, Tabs, TabList, Tab, TabPanels, TabPanel } from '@carbon/react'; +import { Button, TextInput, Tile, Tabs, TabList, Tab, TabPanels, TabPanel, NumberInput } from '@carbon/react'; import { Eyedropper, Copy, ColorPalette, TrashCan } from '@carbon/icons-react'; import { ToolHeader, ToolControls, ToolLayoutToggle } from '../components/ToolUI'; import useLayoutToggle from '../hooks/useLayoutToggle'; @@ -7,14 +7,14 @@ import useLayoutToggle from '../hooks/useLayoutToggle'; // Color utility functions const hexToRgb = (hex) => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex) || - /^#?([a-f\d])([a-f\d])([a-f\d])$/i.exec(hex); + /^#?([a-f\d])([a-f\d])([a-f\d])$/i.exec(hex); if (!result) return null; - + const r = parseInt(result[1].length === 1 ? result[1] + result[1] : result[1], 16); const g = parseInt(result[2].length === 1 ? result[2] + result[2] : result[2], 16); const b = parseInt(result[3].length === 1 ? result[3] + result[3] : result[3], 16); const a = result[4] ? parseInt(result[4].length === 1 ? result[4] + result[4] : result[4], 16) / 255 : 1; - + return { r, g, b, a }; }; @@ -31,11 +31,11 @@ const rgbToHsl = (r, g, b) => { r /= 255; g /= 255; b /= 255; - + const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h, s, l = (max + min) / 2; - + if (max === min) { h = s = 0; } else { @@ -48,7 +48,7 @@ const rgbToHsl = (r, g, b) => { } h /= 6; } - + return { h: Math.round(h * 360), s: Math.round(s * 100), @@ -60,14 +60,14 @@ const rgbToHsv = (r, g, b) => { r /= 255; g /= 255; b /= 255; - + const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h, s, v = max; - + const d = max - min; s = max === 0 ? 0 : d / max; - + if (max === min) { h = 0; } else { @@ -78,7 +78,7 @@ const rgbToHsv = (r, g, b) => { } h /= 6; } - + return { h: Math.round(h * 360), s: Math.round(s * 100), @@ -90,12 +90,12 @@ const rgbToCmyk = (r, g, b) => { r /= 255; g /= 255; b /= 255; - + const k = 1 - Math.max(r, g, b); const c = k === 1 ? 0 : (1 - r - k) / (1 - k); const m = k === 1 ? 0 : (1 - g - k) / (1 - k); const y = k === 1 ? 0 : (1 - b - k) / (1 - k); - + return { c: Math.round(c * 100), m: Math.round(m * 100), @@ -113,7 +113,7 @@ const generateCodeSnippets = (r, g, b, a, hsl, hsv) => { const hue = hsl.h; const sat = hsv.s; const bright = hsv.v; - + return { css: [ { name: 'RGB', code: `rgb(${r} ${g} ${b})` }, @@ -124,25 +124,29 @@ const generateCodeSnippets = (r, g, b, a, hsl, hsv) => { { name: 'CSS Variable', code: `--color-primary: ${rgbToHex(r, g, b, a)};` } ], swift: [ - { name: 'NSColor RGB', code: `NSColor( + { + name: 'NSColor RGB', code: `NSColor( calibratedRed: ${rf}, green: ${gf}, blue: ${bf}, alpha: ${af} )` }, - { name: 'NSColor HSB', code: `NSColor( + { + name: 'NSColor HSB', code: `NSColor( calibratedHue: ${hue / 360}, saturation: ${(sat / 100).toFixed(3)}, brightness: ${(bright / 100).toFixed(3)}, alpha: ${af} )` }, - { name: 'UIColor RGB', code: `UIColor( + { + name: 'UIColor RGB', code: `UIColor( red: ${rf}, green: ${gf}, blue: ${bf}, alpha: ${af} )` }, - { name: 'UIColor HSB', code: `UIColor( + { + name: 'UIColor HSB', code: `UIColor( hue: ${hue / 360}, saturation: ${(sat / 100).toFixed(3)}, brightness: ${(bright / 100).toFixed(3)}, @@ -189,7 +193,8 @@ const generateCodeSnippets = (r, g, b, a, hsl, hsv) => { { name: 'Hex String', code: `ColorUtility.TryParseHtmlString("${rgbToHex(r, g, b)}", out Color color)` } ], reactnative: [ - { name: 'StyleSheet', code: `const styles = StyleSheet.create({ + { + name: 'StyleSheet', code: `const styles = StyleSheet.create({ container: { backgroundColor: '${rgbToHex(r, g, b)}', }, @@ -277,7 +282,7 @@ export default function ColorConverter() { }, []); // Generate code snippets when color changes - const codeSnippets = useMemo(() => + const codeSnippets = useMemo(() => generateCodeSnippets( state.rgb.r, state.rgb.g, state.rgb.b, state.rgb.a, state.hsl, state.hsv @@ -306,7 +311,7 @@ export default function ColorConverter() { const hsl = rgbToHsl(r, g, b); const hsv = rgbToHsv(r, g, b); const cmyk = rgbToCmyk(r, g, b); - + dispatch({ type: 'SET_COLOR', payload: { hex, rgb: { r, g, b, a }, hsl, hsv, cmyk } @@ -314,7 +319,7 @@ export default function ColorConverter() { setHexInput(hex); setRgbInputs({ r, g, b, a }); setHslInputs(hsl); - + // Debounce history recording debouncedAddToHistory(hex, { r, g, b, a }); }, [debouncedAddToHistory]); @@ -361,33 +366,33 @@ export default function ColorConverter() { const numValue = parseInt(value, 10) || 0; const newHsl = { ...hslInputs, [key]: numValue }; setHslInputs(newHsl); - + // Convert HSL to RGB const h = newHsl.h / 360; const s = newHsl.s / 100; const l = newHsl.l / 100; - + let r, g, b; - + if (s === 0) { r = g = b = l; } else { const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; - if (t < 1/6) return p + (q - p) * 6 * t; - if (t < 1/2) return q; - if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; - + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; - r = hue2rgb(p, q, h + 1/3); + r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); - b = hue2rgb(p, q, h - 1/3); + b = hue2rgb(p, q, h - 1 / 3); } - + updateFromRgb(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), state.rgb.a); }, [hslInputs, state.rgb.a, updateFromRgb]); @@ -399,7 +404,7 @@ export default function ColorConverter() { // EyeDropper functionality const openEyeDropper = useCallback(async () => { if (!eyedropperSupported) return; - + setIsPicking(true); try { const eyeDropper = new window.EyeDropper(); @@ -451,9 +456,9 @@ export default function ColorConverter() { return (
- @@ -494,7 +499,7 @@ export default function ColorConverter() { Random
- + {eyedropperSupported && ( + ))} + +
+ setInput(e.target.value)} + placeholder="e.g., 1738412345, 2026-02-01T12:24:05Z, 02/01/2026..." + style={{ fontFamily: "'IBM Plex Mono', monospace", minWidth: '350px' }} + /> + + item ? item.label : ''} + selectedItem={TIMEZONES.find(t => t.id === timezone)} + onChange={({ selectedItem }) => selectedItem && setTimezone(selectedItem.id)} + style={{ minWidth: '250px' }} + /> + + +
+ + + + {/* Settings */} + + item ? item.label : ''} + selectedItem={OUTPUT_FORMATS.find(f => f.id === outputFormat)} + onChange={({ selectedItem }) => selectedItem && setOutputFormat(selectedItem.id)} + style={{ minWidth: '250px' }} + /> + item ? item.label : ''} + selectedItem={TIMEZONES.find(t => t.id === timezone)} + onChange={({ selectedItem }) => selectedItem && setOutputTimezone(selectedItem.id)} + style={{ minWidth: '250px' }} + defaultValue={'local'} + /> + + + {/* Error */} + {error && {error}} + + {/* Main Result */} + {parsedDate && ( + +
+
+ + {formatDate(parsedDate, outputFormat, outputTimezone)} + + copyToClipboard(formatDate(parsedDate, outputFormat, outputTimezone))} size="sm" /> +
+ +
+ {getRelativeTime(parsedDate)} + Unix: {Math.floor(parsedDate.getTime() / 1000)} + â€Ē + Unix (ms): {parsedDate.getTime()} +
+
+
+ )} + + {/* All Formats */} + {parsedDate && ( +
+ {OUTPUT_FORMATS.map((fmt) => ( + +
+ {fmt.label} +
+
+ + {formatDate(parsedDate, fmt.id, timezone)} + + copyToClipboard(formatDate(parsedDate, fmt.id, timezone))} size="sm" /> +
+
+ ))} +
+ )} + + ); +} diff --git a/src/pages/JwtDebugger/components/JwtDecode.jsx b/frontend/src/pages/JwtDebugger/components/JwtDecode.jsx similarity index 96% rename from src/pages/JwtDebugger/components/JwtDecode.jsx rename to frontend/src/pages/JwtDebugger/components/JwtDecode.jsx index a90e373..132661a 100644 --- a/src/pages/JwtDebugger/components/JwtDecode.jsx +++ b/frontend/src/pages/JwtDebugger/components/JwtDecode.jsx @@ -1,3 +1,4 @@ +import React, { useCallback } from 'react'; import { ToolLayout, ToolTextArea, ToolInputGroup } from '../../../components/ToolUI'; import { actions } from '../jwtReducer'; import { ErrorMessage, SuccessMessage } from './StatusMessages'; @@ -5,8 +6,7 @@ import SignatureVerification from './SignatureVerification'; import { Button } from '@carbon/react'; import { MagicWand } from '@carbon/icons-react'; import { EXAMPLE_SECRET } from '../jwtUtils'; -import { useCallback } from 'react'; -import { Backend } from '../../../utils/backendBridge'; +import { JWTService } from '../../../../bindings/devtoolbox/service'; export default function JwtDecode({ state, dispatch, layout, verifySignature }) { // Tab change handlers @@ -23,7 +23,7 @@ export default function JwtDecode({ state, dispatch, layout, verifySignature }) }; try { - const response = await Backend.JWTService.Encode( + const response = await JWTService.Encode( JSON.stringify(header), JSON.stringify(payload), 'HS256', diff --git a/src/pages/JwtDebugger/components/JwtEncode.jsx b/frontend/src/pages/JwtDebugger/components/JwtEncode.jsx similarity index 100% rename from src/pages/JwtDebugger/components/JwtEncode.jsx rename to frontend/src/pages/JwtDebugger/components/JwtEncode.jsx diff --git a/src/pages/JwtDebugger/components/ModeTabBar.jsx b/frontend/src/pages/JwtDebugger/components/ModeTabBar.jsx similarity index 100% rename from src/pages/JwtDebugger/components/ModeTabBar.jsx rename to frontend/src/pages/JwtDebugger/components/ModeTabBar.jsx diff --git a/src/pages/JwtDebugger/components/SignatureVerification.jsx b/frontend/src/pages/JwtDebugger/components/SignatureVerification.jsx similarity index 100% rename from src/pages/JwtDebugger/components/SignatureVerification.jsx rename to frontend/src/pages/JwtDebugger/components/SignatureVerification.jsx diff --git a/src/pages/JwtDebugger/components/StatusMessages.jsx b/frontend/src/pages/JwtDebugger/components/StatusMessages.jsx similarity index 100% rename from src/pages/JwtDebugger/components/StatusMessages.jsx rename to frontend/src/pages/JwtDebugger/components/StatusMessages.jsx diff --git a/src/pages/JwtDebugger/index.jsx b/frontend/src/pages/JwtDebugger/index.jsx similarity index 93% rename from src/pages/JwtDebugger/index.jsx rename to frontend/src/pages/JwtDebugger/index.jsx index d66e4e8..b848c43 100644 --- a/src/pages/JwtDebugger/index.jsx +++ b/frontend/src/pages/JwtDebugger/index.jsx @@ -1,4 +1,4 @@ -import React, { useReducer, useCallback } from 'react'; +import React, { useEffect, useReducer, useCallback } from 'react'; import { ToolHeader } from '../../components/ToolUI'; import useLayoutToggle from '../../hooks/useLayoutToggle'; import ToolLayoutToggle from '../../components/layout/ToolLayoutToggle'; @@ -6,7 +6,7 @@ import { jwtReducer, initialState, actions } from './jwtReducer'; import ModeTabBar from './components/ModeTabBar'; import JwtDecode from './components/JwtDecode'; import JwtEncode from './components/JwtEncode'; -import { Backend } from '../../utils/backendBridge'; +import { JWTService } from '../../../bindings/devtoolbox/service'; export default function JwtDebugger() { const [state, dispatch] = useReducer(jwtReducer, initialState); @@ -20,7 +20,7 @@ export default function JwtDebugger() { }); // Decode JWT when token changes (using Go backend) - React.useEffect(() => { + useEffect(() => { if (!state.token.trim()) { dispatch(actions.setDecoded({ header: null, payload: null, signature: '', error: '', isValid: null })); return; @@ -29,7 +29,7 @@ export default function JwtDebugger() { // Call Go backend for decoding const decodeToken = async () => { try { - const response = await Backend.JWTService.Decode(state.token); + const response = await JWTService.Decode(state.token); dispatch(actions.setDecoded({ header: response.header, @@ -66,7 +66,7 @@ export default function JwtDebugger() { try { // Call Go backend for verification - const response = await Backend.JWTService.Verify(state.token, state.secret, state.encoding); + const response = await JWTService.Verify(state.token, state.secret, state.encoding); dispatch(actions.setValidation( response.error ? response.error : response.validationMessage, @@ -85,7 +85,7 @@ export default function JwtDebugger() { } try { - const response = await Backend.JWTService.Encode( + const response = await JWTService.Encode( state.headerInput, state.payloadInput, state.algorithm, diff --git a/src/pages/JwtDebugger/jwtReducer.js b/frontend/src/pages/JwtDebugger/jwtReducer.js similarity index 100% rename from src/pages/JwtDebugger/jwtReducer.js rename to frontend/src/pages/JwtDebugger/jwtReducer.js diff --git a/src/pages/JwtDebugger/jwtUtils.js b/frontend/src/pages/JwtDebugger/jwtUtils.js similarity index 100% rename from src/pages/JwtDebugger/jwtUtils.js rename to frontend/src/pages/JwtDebugger/jwtUtils.js diff --git a/src/pages/NumberConverter/index.jsx b/frontend/src/pages/NumberConverter/index.jsx similarity index 100% rename from src/pages/NumberConverter/index.jsx rename to frontend/src/pages/NumberConverter/index.jsx diff --git a/src/pages/RegExpTester.jsx b/frontend/src/pages/RegExpTester.jsx similarity index 100% rename from src/pages/RegExpTester.jsx rename to frontend/src/pages/RegExpTester.jsx diff --git a/src/pages/StringUtilities/components/CaseConverterPane.jsx b/frontend/src/pages/StringUtilities/components/CaseConverterPane.jsx similarity index 100% rename from src/pages/StringUtilities/components/CaseConverterPane.jsx rename to frontend/src/pages/StringUtilities/components/CaseConverterPane.jsx diff --git a/src/pages/StringUtilities/components/InspectorPane.jsx b/frontend/src/pages/StringUtilities/components/InspectorPane.jsx similarity index 100% rename from src/pages/StringUtilities/components/InspectorPane.jsx rename to frontend/src/pages/StringUtilities/components/InspectorPane.jsx diff --git a/src/pages/StringUtilities/components/ModeTabBar.jsx b/frontend/src/pages/StringUtilities/components/ModeTabBar.jsx similarity index 100% rename from src/pages/StringUtilities/components/ModeTabBar.jsx rename to frontend/src/pages/StringUtilities/components/ModeTabBar.jsx diff --git a/src/pages/StringUtilities/components/SortDedupePane.jsx b/frontend/src/pages/StringUtilities/components/SortDedupePane.jsx similarity index 100% rename from src/pages/StringUtilities/components/SortDedupePane.jsx rename to frontend/src/pages/StringUtilities/components/SortDedupePane.jsx diff --git a/src/pages/StringUtilities/index.jsx b/frontend/src/pages/StringUtilities/index.jsx similarity index 100% rename from src/pages/StringUtilities/index.jsx rename to frontend/src/pages/StringUtilities/index.jsx diff --git a/src/pages/StringUtilities/strings.js b/frontend/src/pages/StringUtilities/strings.js similarity index 100% rename from src/pages/StringUtilities/strings.js rename to frontend/src/pages/StringUtilities/strings.js diff --git a/src/pages/TextConverter/components/CommonTags.jsx b/frontend/src/pages/TextConverter/components/CommonTags.jsx similarity index 100% rename from src/pages/TextConverter/components/CommonTags.jsx rename to frontend/src/pages/TextConverter/components/CommonTags.jsx diff --git a/src/pages/TextConverter/components/ConfigurationPane.jsx b/frontend/src/pages/TextConverter/components/ConfigurationPane.jsx similarity index 100% rename from src/pages/TextConverter/components/ConfigurationPane.jsx rename to frontend/src/pages/TextConverter/components/ConfigurationPane.jsx diff --git a/src/pages/TextConverter/components/ConversionControls.jsx b/frontend/src/pages/TextConverter/components/ConversionControls.jsx similarity index 99% rename from src/pages/TextConverter/components/ConversionControls.jsx rename to frontend/src/pages/TextConverter/components/ConversionControls.jsx index 7b717a2..f107dd0 100644 --- a/src/pages/TextConverter/components/ConversionControls.jsx +++ b/frontend/src/pages/TextConverter/components/ConversionControls.jsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Button, Dropdown, Toggle, RadioButtonGroup, RadioButton } from '@carbon/react'; import { ToolLayoutToggle } from '../../../components/ToolUI'; import { ArrowsHorizontal, Play, Add, Checkmark } from '@carbon/icons-react'; diff --git a/src/pages/TextConverter/components/ImageOutput.jsx b/frontend/src/pages/TextConverter/components/ImageOutput.jsx similarity index 100% rename from src/pages/TextConverter/components/ImageOutput.jsx rename to frontend/src/pages/TextConverter/components/ImageOutput.jsx diff --git a/src/pages/TextConverter/components/MultiHashOutput.jsx b/frontend/src/pages/TextConverter/components/MultiHashOutput.jsx similarity index 100% rename from src/pages/TextConverter/components/MultiHashOutput.jsx rename to frontend/src/pages/TextConverter/components/MultiHashOutput.jsx diff --git a/src/pages/TextConverter/constants.js b/frontend/src/pages/TextConverter/constants.js similarity index 100% rename from src/pages/TextConverter/constants.js rename to frontend/src/pages/TextConverter/constants.js diff --git a/src/pages/TextConverter/index.jsx b/frontend/src/pages/TextConverter/index.jsx similarity index 97% rename from src/pages/TextConverter/index.jsx rename to frontend/src/pages/TextConverter/index.jsx index 4fef414..d3e48d2 100644 --- a/src/pages/TextConverter/index.jsx +++ b/frontend/src/pages/TextConverter/index.jsx @@ -1,9 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { Button } from '@carbon/react'; -import { ArrowsHorizontal } from '@carbon/icons-react'; import { ToolHeader, ToolPane, ToolSplitPane } from '../../components/ToolUI'; import useLayoutToggle from '../../hooks/useLayoutToggle'; -import { Backend } from '../../utils/backendBridge'; import ConversionControls from './components/ConversionControls'; import ConfigurationPane from './components/ConfigurationPane'; import MultiHashOutput from './components/MultiHashOutput'; @@ -20,6 +17,7 @@ import { PLACEHOLDERS, LAYOUT } from './strings'; +import { ConversionService } from '../../../bindings/devtoolbox/service'; export default function TextBasedConverter() { // Persistent state initialization @@ -93,14 +91,14 @@ export default function TextBasedConverter() { // Add current selection to quick actions const addCurrentToQuickActions = useCallback(() => { if (isCurrentInQuickActions()) return; - + const newTag = { id: `${category}-${method}`.toLowerCase().replace(/[^a-z0-9]/g, '-'), category, method, label: `${category} - ${method}` }; - + setCustomTags(prev => [...prev, newTag]); }, [category, method, isCurrentInQuickActions]); @@ -128,7 +126,7 @@ export default function TextBasedConverter() { try { // Include subMode in backend request const backendConfig = { ...cfg, subMode: sub }; - const result = await Backend.ConversionService.Convert(text, cat, meth, backendConfig); + const result = await ConversionService.Convert(text, cat, meth, backendConfig); setOutput(result); setError(''); } catch (err) { diff --git a/src/pages/TextConverter/strings.js b/frontend/src/pages/TextConverter/strings.js similarity index 100% rename from src/pages/TextConverter/strings.js rename to frontend/src/pages/TextConverter/strings.js diff --git a/src/pages/TextDiffChecker.jsx b/frontend/src/pages/TextDiffChecker.jsx similarity index 100% rename from src/pages/TextDiffChecker.jsx rename to frontend/src/pages/TextDiffChecker.jsx diff --git a/src/style.css b/frontend/src/style.css similarity index 100% rename from src/style.css rename to frontend/src/style.css diff --git a/src/utils/inputUtils.js b/frontend/src/utils/inputUtils.js similarity index 100% rename from src/utils/inputUtils.js rename to frontend/src/utils/inputUtils.js diff --git a/src/utils/layoutUtils.js b/frontend/src/utils/layoutUtils.js similarity index 100% rename from src/utils/layoutUtils.js rename to frontend/src/utils/layoutUtils.js diff --git a/go.mod b/go.mod index 9c44fae..7633a9e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module dev-toolbox +module devtoolbox -go 1.24.0 +go 1.25 require ( github.com/boombuler/barcode v1.1.0 @@ -11,37 +11,77 @@ require ( github.com/itchyny/gojq v0.12.18 github.com/pelletier/go-toml/v2 v2.2.4 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/wailsapp/wails/v2 v2.11.0 + github.com/wailsapp/wails/v3 v3.0.0-alpha.68 golang.org/x/crypto v0.47.0 golang.org/x/net v0.49.0 gopkg.in/yaml.v3 v3.0.1 ) +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/coder/websocket v1.8.14 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.7.0 // indirect + github.com/go-git/go-git/v5 v5.16.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lmittmann/tint v1.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/tools v0.41.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) + require ( github.com/bep/debounce v1.2.1 // indirect + github.com/gin-gonic/gin v1.11.0 github.com/go-ole/go-ole v1.3.0 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/itchyny/timefmt-go v0.1.7 // 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/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // 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-colorable v0.1.14 // 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 + github.com/samber/lo v1.52.0 // indirect + github.com/wailsapp/go-webview2 v1.0.23 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect ) diff --git a/go.sum b/go.sum index eccf171..f905058 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,17 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= @@ -15,124 +28,232 @@ github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVa github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +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.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= +github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= +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.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= +github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= 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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc= github.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg= github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA= github.com/itchyny/timefmt-go v0.1.7/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= -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/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-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= +github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= 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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= -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= -github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= -github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= +github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/wails/v3 v3.0.0-alpha.68 h1:CBSP9rOISKiFv6hmqVj2HsU6f4bSMQmsmuSzPQMUxSE= +github.com/wailsapp/wails/v3 v3.0.0-alpha.68/go.mod h1:zvgNL/mlFcX8aRGu6KOz9AHrMmTBD+4hJRQIONqF/Yw= +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= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/index.html b/index.html deleted file mode 100644 index 1b78207..0000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - dev-toolbox - - -
- - - - diff --git a/internal/wails/barcode_service.go b/internal/barcode/service.go similarity index 84% rename from internal/wails/barcode_service.go rename to internal/barcode/service.go index 0b0de2f..a1f5772 100644 --- a/internal/wails/barcode_service.go +++ b/internal/barcode/service.go @@ -1,8 +1,7 @@ -package wails +package barcode import ( "bytes" - "context" "encoding/base64" "fmt" "image/png" @@ -14,16 +13,10 @@ import ( "github.com/skip2/go-qrcode" ) -type BarcodeService struct { - ctx context.Context -} - -func NewBarcodeService() *BarcodeService { - return &BarcodeService{} -} +type BarcodeService struct{} -func (s *BarcodeService) Startup(ctx context.Context) { - s.ctx = ctx +func NewBarcodeService() BarcodeService { + return BarcodeService{} } // GenerateBarcodeRequest represents the request to generate a barcode @@ -89,21 +82,28 @@ func (s *BarcodeService) GenerateBarcode(req GenerateBarcodeRequest) GenerateBar // For 1D barcodes, we maintain aspect ratio if standard != "QR" { bounds := img.Bounds() - width := bounds.Dx() - height := bounds.Dy() + origWidth := bounds.Dx() + origHeight := bounds.Dy() - // Scale width to size, maintain aspect ratio for height - scaleFactor := float64(size) / float64(width) - newHeight := int(float64(height) * scaleFactor) + // Calculate target dimensions while maintaining aspect ratio + scaleFactor := float64(size) / float64(origWidth) + targetWidth := size + targetHeight := int(float64(origHeight) * scaleFactor) - // Ensure minimum height for visibility - if newHeight < 100 { - newHeight = 100 + // Ensure minimum dimensions for visibility and library requirements + if targetWidth < 259 { + targetWidth = 259 + } + if targetHeight < 100 { + targetHeight = 100 } - img, err = barcode.Scale(img, size, newHeight) - if err != nil { - return GenerateBarcodeResponse{Error: fmt.Sprintf("Failed to scale barcode: %v", err)} + // Only scale if the target is different from original + if targetWidth != origWidth || targetHeight != origHeight { + img, err = barcode.Scale(img, targetWidth, targetHeight) + if err != nil { + return GenerateBarcodeResponse{Error: fmt.Sprintf("Failed to scale barcode: %v", err)} + } } } @@ -187,14 +187,27 @@ func (s *BarcodeService) GetBarcodeSizes() []map[string]interface{} { } // calculateEANChecksum calculates the EAN checksum digit +// For EAN-13: weights alternate starting with 1 (1,3,1,3...) +// For EAN-8: weights alternate starting with 3 (3,1,3,1...) func calculateEANChecksum(code string) int { sum := 0 + isEAN8 := len(code) == 7 for i, c := range code { digit := int(c - '0') - if i%2 == 0 { - sum += digit * 1 + if isEAN8 { + // EAN-8: weights start with 3 at position 0 + if i%2 == 0 { + sum += digit * 3 + } else { + sum += digit * 1 + } } else { - sum += digit * 3 + // EAN-13: weights start with 1 at position 0 + if i%2 == 0 { + sum += digit * 1 + } else { + sum += digit * 3 + } } } checksum := (10 - (sum % 10)) % 10 diff --git a/internal/barcode/service_test.go b/internal/barcode/service_test.go new file mode 100644 index 0000000..c75bbc9 --- /dev/null +++ b/internal/barcode/service_test.go @@ -0,0 +1,473 @@ +package barcode + +import ( + "strings" + "testing" +) + +func TestGenerateBarcode(t *testing.T) { + service := NewBarcodeService() + + tests := []struct { + name string + content string + standard string + size int + level string + expectErr bool + errorMsg string + }{ + // QR Code tests + { + name: "QR code - valid content", + content: "https://example.com", + standard: "QR", + size: 256, + level: "M", + expectErr: false, + }, + { + name: "QR code - empty content should error", + content: "", + standard: "QR", + size: 256, + level: "M", + expectErr: true, + errorMsg: "Content cannot be empty", + }, + { + name: "QR code - custom error level H", + content: "QR test", + standard: "QR", + size: 128, + level: "H", + expectErr: false, + }, + { + name: "QR code - default size (less than 64)", + content: "test", + standard: "QR", + size: 32, + level: "M", + expectErr: false, + }, + + // EAN-13 tests + { + name: "EAN-13 - 12 digits (will be auto-calculated)", + content: "123456789012", + standard: "EAN-13", + size: 256, + expectErr: false, + }, + { + name: "EAN-13 - 13 digits with correct checksum", + content: "1234567890128", // Last digit 8 is checksum for 123456789012 + standard: "EAN-13", + size: 256, + expectErr: false, + }, + { + name: "EAN-13 - invalid length", + content: "12345", + standard: "EAN-13", + size: 256, + expectErr: true, + }, + + // EAN-8 tests + { + name: "EAN-8 - 7 digits (will be auto-calculated)", + content: "1234567", + standard: "EAN-8", + size: 256, + expectErr: false, + }, + { + name: "EAN-8 - 8 digits with correct checksum", + content: "96385074", // Checksum 4 for 9638507 + standard: "EAN-8", + size: 256, + expectErr: false, + }, + + // Code128 tests + { + name: "Code128 - alphanumeric content", + content: "ABC123", + standard: "Code128", + size: 256, + expectErr: false, + }, + { + name: "Code128 - special characters", + content: "Test-123_456", + standard: "Code128", + size: 256, + expectErr: false, + }, + + // Code39 tests + { + name: "Code39 - valid alphanumeric", + content: "ABC123", + standard: "Code39", + size: 256, + expectErr: false, + }, + { + name: "Code39 - with spaces and valid special chars", + content: "TEST-123 $/+%", + standard: "Code39", + size: 256, + expectErr: false, + }, + + // Edge cases + { + name: "Default standard (empty should default to QR)", + content: "default test", + standard: "", + size: 256, + level: "M", + expectErr: false, + }, + { + name: "Invalid standard", + content: "test", + standard: "INVALID", + size: 256, + expectErr: true, + errorMsg: "Unsupported barcode standard", + }, + { + name: "Large size (should cap at 1024)", + content: "test", + standard: "QR", + size: 2048, + level: "M", + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := GenerateBarcodeRequest{ + Content: tt.content, + Standard: tt.standard, + Size: tt.size, + Level: tt.level, + Format: "base64", + } + + resp := service.GenerateBarcode(req) + + if tt.expectErr { + if resp.Error == "" { + t.Errorf("Expected error but got none") + } + if tt.errorMsg != "" && !strings.Contains(resp.Error, tt.errorMsg) { + t.Errorf("Expected error containing '%s', got '%s'", tt.errorMsg, resp.Error) + } + return + } + + if resp.Error != "" { + t.Errorf("Unexpected error: %s", resp.Error) + return + } + + if resp.DataURL == "" { + t.Error("Expected DataURL but got empty") + return + } + + // Verify it's a valid data URL + if !strings.HasPrefix(resp.DataURL, "data:image/png;base64,") { + t.Errorf("Expected data URL starting with 'data:image/png;base64,', got: %s", resp.DataURL[:50]) + } + + // Verify base64 data is present + base64Data := strings.TrimPrefix(resp.DataURL, "data:image/png;base64,") + if len(base64Data) == 0 { + t.Error("Base64 data is empty") + } + }) + } +} + +func TestGetBarcodeStandards(t *testing.T) { + service := NewBarcodeService() + standards := service.GetBarcodeStandards() + + expectedStandards := []struct { + value string + label string + }{ + {"QR", "QR Code (2D)"}, + {"EAN-13", "EAN-13 (Retail - 13 digits)"}, + {"EAN-8", "EAN-8 (Small Retail - 8 digits)"}, + {"Code128", "Code 128 (High Density)"}, + {"Code39", "Code 39 (Alphanumeric)"}, + } + + if len(standards) != len(expectedStandards) { + t.Errorf("Expected %d standards, got %d", len(expectedStandards), len(standards)) + } + + for i, expected := range expectedStandards { + if i >= len(standards) { + break + } + if standards[i]["value"] != expected.value { + t.Errorf("Standard %d: expected value '%s', got '%s'", i, expected.value, standards[i]["value"]) + } + if standards[i]["label"] != expected.label { + t.Errorf("Standard %d: expected label '%s', got '%s'", i, expected.label, standards[i]["label"]) + } + } +} + +func TestGetQRErrorLevels(t *testing.T) { + service := NewBarcodeService() + levels := service.GetQRErrorLevels() + + expectedLevels := []struct { + value string + label string + }{ + {"L", "Low (~7%)"}, + {"M", "Medium (~15%)"}, + {"Q", "Quartile (~25%)"}, + {"H", "High (~30%)"}, + } + + if len(levels) != len(expectedLevels) { + t.Errorf("Expected %d error levels, got %d", len(expectedLevels), len(levels)) + } + + for i, expected := range expectedLevels { + if i >= len(levels) { + break + } + if levels[i]["value"] != expected.value { + t.Errorf("Level %d: expected value '%s', got '%s'", i, expected.value, levels[i]["value"]) + } + if levels[i]["label"] != expected.label { + t.Errorf("Level %d: expected label '%s', got '%s'", i, expected.label, levels[i]["label"]) + } + } +} + +func TestGetBarcodeSizes(t *testing.T) { + service := NewBarcodeService() + sizes := service.GetBarcodeSizes() + + expectedSizes := []struct { + value int + label string + }{ + {128, "Small (128px)"}, + {256, "Medium (256px)"}, + {512, "Large (512px)"}, + {1024, "Extra Large (1024px)"}, + } + + if len(sizes) != len(expectedSizes) { + t.Errorf("Expected %d size options, got %d", len(expectedSizes), len(sizes)) + } + + for i, expected := range expectedSizes { + if i >= len(sizes) { + break + } + // Type assertion for interface{} + value, ok := sizes[i]["value"].(int) + if !ok { + t.Errorf("Size %d: value is not an int", i) + continue + } + if value != expected.value { + t.Errorf("Size %d: expected value %d, got %d", i, expected.value, value) + } + label, ok := sizes[i]["label"].(string) + if !ok { + t.Errorf("Size %d: label is not a string", i) + continue + } + if label != expected.label { + t.Errorf("Size %d: expected label '%s', got '%s'", i, expected.label, label) + } + } +} + +func TestValidateContent(t *testing.T) { + service := NewBarcodeService() + + tests := []struct { + name string + content string + standard string + wantValid bool + wantMsg string + }{ + // EAN-13 validation tests + { + name: "EAN-13 valid 12 digits", + content: "123456789012", + standard: "EAN-13", + wantValid: true, + }, + { + name: "EAN-13 valid 13 digits with correct checksum", + content: "1234567890128", // Checksum for 123456789012 is 8 + standard: "EAN-13", + wantValid: true, + }, + { + name: "EAN-13 invalid length", + content: "12345", + standard: "EAN-13", + wantValid: false, + wantMsg: "EAN-13 requires 12 or 13 digits", + }, + { + name: "EAN-13 contains non-digits", + content: "12345678901A", + standard: "EAN-13", + wantValid: false, + wantMsg: "EAN-13 can only contain digits", + }, + { + name: "EAN-13 13 digits with wrong checksum", + content: "1234567890129", // Wrong checksum (should be 8) + standard: "EAN-13", + wantValid: false, + wantMsg: "Invalid checksum", + }, + + // EAN-8 validation tests + { + name: "EAN-8 valid 7 digits", + content: "1234567", + standard: "EAN-8", + wantValid: true, + }, + { + name: "EAN-8 valid 8 digits with correct checksum", + content: "96385074", // Checksum for 9638507 is 4 + standard: "EAN-8", + wantValid: true, + }, + { + name: "EAN-8 invalid length", + content: "12345", + standard: "EAN-8", + wantValid: false, + wantMsg: "EAN-8 requires 7 or 8 digits", + }, + { + name: "EAN-8 contains non-digits", + content: "12345A7", + standard: "EAN-8", + wantValid: false, + wantMsg: "EAN-8 can only contain digits", + }, + + // Code39 validation tests + { + name: "Code39 valid alphanumeric", + content: "ABC123", + standard: "Code39", + wantValid: true, + }, + { + name: "Code39 valid with special chars", + content: "TEST-123 $/+%", + standard: "Code39", + wantValid: true, + }, + { + name: "Code39 invalid character (lowercase)", + content: "abc123", + standard: "Code39", + wantValid: false, + wantMsg: "Code 39 only supports: 0-9, A-Z, and - . $ / + % space", + }, + { + name: "Code39 invalid character (unsupported)", + content: "TEST@123", + standard: "Code39", + wantValid: false, + wantMsg: "Code 39 only supports: 0-9, A-Z, and - . $ / + % space", + }, + + // Other standards (should return valid by default) + { + name: "QR code always valid", + content: "any content", + standard: "QR", + wantValid: true, + }, + { + name: "Code128 always valid", + content: "any content", + standard: "Code128", + wantValid: true, + }, + { + name: "Unknown standard", + content: "test", + standard: "UNKNOWN", + wantValid: true, // Unknown standards don't have validation + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := service.ValidateContent(tt.content, tt.standard) + + valid, ok := result["valid"].(bool) + if !ok { + t.Fatal("Result 'valid' field is not a bool") + } + + message, ok := result["message"].(string) + if !ok { + t.Fatal("Result 'message' field is not a string") + } + + if valid != tt.wantValid { + t.Errorf("Expected valid=%v, got valid=%v", tt.wantValid, valid) + } + + if tt.wantMsg != "" && !strings.Contains(message, tt.wantMsg) { + t.Errorf("Expected message containing '%s', got '%s'", tt.wantMsg, message) + } + }) + } +} + +func TestCalculateEANChecksum(t *testing.T) { + tests := []struct { + name string + code string + expected int + }{ + {"EAN-13 example 1", "123456789012", 8}, // Known checksum + {"EAN-13 example 2", "400638133393", 1}, // Known checksum + {"EAN-8 example 1", "1234567", 0}, // 1*3+2*1+3*3+4*1+5*3+6*1+7*3=60, 60%10=0, (10-0)%10=0 + {"EAN-8 example 2", "9638507", 4}, // Known checksum + {"All zeros", "000000000000", 0}, // Edge case + {"All ones", "111111111111", 6}, // Sum=24, 24%10=4, (10-4)%10=6 + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculateEANChecksum(tt.code) + if result != tt.expected { + t.Errorf("Expected checksum %d for code %s, got %d", tt.expected, tt.code, result) + } + }) + } +} diff --git a/internal/datetimeconverter/dto.go b/internal/datetimeconverter/dto.go new file mode 100644 index 0000000..9076b1e --- /dev/null +++ b/internal/datetimeconverter/dto.go @@ -0,0 +1,92 @@ +package datetimeconverter + +// ConvertRequest represents a conversion request from the frontend +type ConvertRequest struct { + Input string `json:"input"` + Precision string `json:"precision"` // "auto", "seconds", "millis", "micros", "nanos" + Timezone string `json:"timezone"` // "local", "UTC", or IANA timezone name + OutputFormat string `json:"outputFormat"` // "iso", "rfc2822", "sql", "us", "eu", "compact", "custom" + CustomFormat string `json:"customFormat"` +} + +// ConvertResponse represents the conversion response +type ConvertResponse struct { + Result *TimeResult `json:"result,omitempty"` + DetectedType string `json:"detectedType"` // "timestamp", "iso", "date", "unknown" + DetectedPrec string `json:"detectedPrec"` // "seconds", "millis", "micros", "nanos" + Error string `json:"error,omitempty"` +} + +// PresetsResponse returns all available presets +type PresetsResponse struct { + Presets []Preset `json:"presets"` +} + +// DeltaRequest represents a time delta calculation request +type DeltaRequest struct { + DateA string `json:"dateA"` + DateB string `json:"dateB"` +} + +// DeltaResponse represents a time delta calculation response +type DeltaResponse struct { + Delta *TimeDelta `json:"delta,omitempty"` + Error string `json:"error,omitempty"` +} + +// ArithmeticRequest represents a date arithmetic request +type ArithmeticRequest struct { + BaseDate string `json:"baseDate"` + Operation string `json:"operation"` // "add" or "subtract" + Value int `json:"value"` + Unit string `json:"unit"` // "seconds", "minutes", "hours", "days", "weeks", "months", "years" +} + +// ArithmeticResponse represents a date arithmetic response +type ArithmeticResponse struct { + Result *TimeResult `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +// BatchRequest represents a batch conversion request +type BatchRequest struct { + Inputs []string `json:"inputs"` + Timezone string `json:"timezone"` +} + +// BatchResponse represents a batch conversion response +type BatchResponse struct { + Results []BatchResult `json:"results"` +} + +// TimezoneComparisonRequest represents a timezone comparison request +type TimezoneComparisonRequest struct { + Timestamp int64 `json:"timestamp"` + Timezones []string `json:"timezones"` +} + +// TimezoneComparisonResponse represents a timezone comparison response +type TimezoneComparisonResponse struct { + Timezones []TimezoneInfo `json:"timezones"` + Error string `json:"error,omitempty"` +} + +type AvailableTimezonesResponse struct { + Timezones []TimezoneInfo `json:"timezones"` +} + +// FromTimeResult creates a ConvertResponse from a TimeResult +func FromTimeResult(result *TimeResult, detectedType, detectedPrec string) ConvertResponse { + return ConvertResponse{ + Result: result, + DetectedType: detectedType, + DetectedPrec: detectedPrec, + } +} + +// ErrorResponse creates an error response +func ErrorResponse(err error) ConvertResponse { + return ConvertResponse{ + Error: err.Error(), + } +} diff --git a/internal/datetimeconverter/errors.go b/internal/datetimeconverter/errors.go new file mode 100644 index 0000000..63e6625 --- /dev/null +++ b/internal/datetimeconverter/errors.go @@ -0,0 +1,12 @@ +package datetimeconverter + +import "fmt" + +// Domain errors for unixtime package +var ( + ErrInvalidTimestamp = fmt.Errorf("invalid timestamp") + ErrInvalidDate = fmt.Errorf("invalid date format") + ErrInvalidPrecision = fmt.Errorf("invalid precision") + ErrInvalidTimezone = fmt.Errorf("invalid timezone") + ErrInvalidUnit = fmt.Errorf("invalid time unit") +) diff --git a/internal/datetimeconverter/models.go b/internal/datetimeconverter/models.go new file mode 100644 index 0000000..c935a61 --- /dev/null +++ b/internal/datetimeconverter/models.go @@ -0,0 +1,302 @@ +package datetimeconverter + +import ( + "fmt" + "time" +) + +// Precision represents the timestamp precision type +type Precision string + +const ( + PrecisionAuto Precision = "auto" + PrecisionSeconds Precision = "seconds" + PrecisionMillis Precision = "millis" + PrecisionMicros Precision = "micros" + PrecisionNanos Precision = "nanos" +) + +// InputType represents the detected input type +type InputType string + +const ( + InputTypeTimestamp InputType = "timestamp" + InputTypeISO InputType = "iso" + InputTypeDate InputType = "date" + InputTypeUnknown InputType = "unknown" +) + +// TimeResult contains all conversion results for a given time +type TimeResult struct { + UnixSeconds int64 `json:"unixSeconds"` + UnixMillis int64 `json:"unixMillis"` + UnixMicros int64 `json:"unixMicros"` + UnixNanos int64 `json:"unixNanos"` + UTC string `json:"utc"` + Local string `json:"local"` + Relative string `json:"relative"` + RelativeDetails RelativeBreakdown `json:"relativeDetails"` +} + +// RelativeBreakdown provides detailed relative time information +type RelativeBreakdown struct { + Days int `json:"days"` + Hours int `json:"hours"` + Minutes int `json:"minutes"` + Seconds int `json:"seconds"` + TotalHours int `json:"totalHours"` + TotalMinutes int `json:"totalMinutes"` + TotalSeconds int `json:"totalSeconds"` + DaysSinceEpoch int `json:"daysSinceEpoch"` +} + +// TimeDelta represents the difference between two times +type TimeDelta struct { + Days int `json:"days"` + Hours int `json:"hours"` + Minutes int `json:"minutes"` + Seconds int `json:"seconds"` + TotalHours float64 `json:"totalHours"` + TotalMinutes float64 `json:"totalMinutes"` + TotalSeconds float64 `json:"totalSeconds"` + BusinessDays int `json:"businessDays"` + IsFuture bool `json:"isFuture"` +} + +// BatchResult represents a single batch conversion result +type BatchResult struct { + Input string `json:"input"` + Success bool `json:"success"` + Error string `json:"error,omitempty"` + Result *TimeResult `json:"result,omitempty"` +} + +// TimezoneInfo represents time in a specific timezone +type TimezoneInfo struct { + Label string `json:"label"` + Timezone string `json:"timezone"` +} + +// Preset represents a quick preset option +type Preset struct { + ID string `json:"id"` + Label string `json:"label"` + Description string `json:"description"` + Timestamp int64 `json:"timestamp"` +} + +// FormatType represents output format types +type FormatType string + +const ( + FormatISO FormatType = "iso" + FormatRFC2822 FormatType = "rfc2822" + FormatRFC3339 FormatType = "rfc3339" + FormatSQL FormatType = "sql" + FormatUS FormatType = "us" + FormatEU FormatType = "eu" + FormatCompact FormatType = "compact" + FormatCustom FormatType = "custom" +) + +// TimeUnit represents time units for arithmetic operations +type TimeUnit string + +const ( + UnitSeconds TimeUnit = "seconds" + UnitMinutes TimeUnit = "minutes" + UnitHours TimeUnit = "hours" + UnitDays TimeUnit = "days" + UnitWeeks TimeUnit = "weeks" + UnitMonths TimeUnit = "months" + UnitYears TimeUnit = "years" +) + +// DetectPrecision determines the precision from timestamp string length +func DetectPrecision(input string) Precision { + if len(input) == 0 { + return PrecisionAuto + } + + // Remove any non-digit characters for length calculation + clean := "" + for _, r := range input { + if r >= '0' && r <= '9' { + clean += string(r) + } + } + + switch len(clean) { + case 10: + return PrecisionSeconds + case 13: + return PrecisionMillis + case 16: + return PrecisionMicros + case 19: + return PrecisionNanos + default: + return PrecisionAuto + } +} + +// DetectInputType determines the type of input +func DetectInputType(input string) InputType { + if len(input) == 0 { + return InputTypeUnknown + } + + // Check for ISO format (contains T or Z) + if len(input) >= 10 && (contains(input, "T") || contains(input, "Z")) { + return InputTypeISO + } + + // Check if it's a pure number (timestamp) + isNumber := true + for _, r := range input { + if r < '0' || r > '9' { + isNumber = false + break + } + } + + if isNumber && len(input) >= 10 { + return InputTypeTimestamp + } + + // Check for date separators + if contains(input, "-") || contains(input, "/") { + return InputTypeDate + } + + return InputTypeUnknown +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr)) +} + +func containsHelper(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +// UnixEpoch returns the Unix epoch time (January 1, 1970) +func UnixEpoch() time.Time { + return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) +} + +// ParseTimestamp parses a timestamp string with given precision +func ParseTimestamp(input string, precision Precision) (time.Time, error) { + if precision == PrecisionAuto { + precision = DetectPrecision(input) + } + + // Extract numeric part + var numericStr string + for _, r := range input { + if r >= '0' && r <= '9' { + numericStr += string(r) + } + } + + if len(numericStr) == 0 { + return time.Time{}, ErrInvalidTimestamp + } + + var ts int64 + _, err := fmt.Sscanf(numericStr, "%d", &ts) + if err != nil { + return time.Time{}, ErrInvalidTimestamp + } + + var nanos int64 + switch precision { + case PrecisionSeconds: + nanos = ts * 1e9 + case PrecisionMillis: + nanos = ts * 1e6 + case PrecisionMicros: + nanos = ts * 1e3 + case PrecisionNanos: + nanos = ts + default: + // Default to seconds if unknown + nanos = ts * 1e9 + } + + return time.Unix(0, nanos).UTC(), nil +} + +// FormatTime formats a time according to the specified format +func FormatTime(t time.Time, format FormatType, customFormat string) string { + switch format { + case FormatISO: + return t.Format(time.RFC3339Nano) + case FormatRFC2822: + return t.Format(time.RFC1123) + case FormatRFC3339: + return t.Format(time.RFC3339) + case FormatSQL: + return t.Format("2006-01-02 15:04:05") + case FormatUS: + return t.Format("01/02/2006 15:04:05") + case FormatEU: + return t.Format("02/01/2006 15:04:05") + case FormatCompact: + return t.Format("20060102-150405") + case FormatCustom: + if customFormat != "" { + return formatCustom(t, customFormat) + } + return t.Format(time.RFC3339) + default: + return t.Format(time.RFC3339) + } +} + +// formatCustom applies custom format tokens +func formatCustom(t time.Time, format string) string { + replacements := map[string]string{ + "YYYY": t.Format("2006"), + "MM": t.Format("01"), + "DD": t.Format("02"), + "HH": t.Format("15"), + "hh": t.Format("03"), + "mm": t.Format("04"), + "ss": t.Format("05"), + "sss": fmt.Sprintf("%03d", t.Nanosecond()/1e6), + "A": t.Format("PM"), + "ddd": t.Format("Mon"), + "dddd": t.Format("Monday"), + "ZZ": t.Format("-0700"), + } + + result := format + for token, value := range replacements { + result = replaceAll(result, token, value) + } + return result +} + +func replaceAll(s, old, new string) string { + result := s + for { + idx := -1 + for i := 0; i <= len(result)-len(old); i++ { + if result[i:i+len(old)] == old { + idx = i + break + } + } + if idx == -1 { + break + } + result = result[:idx] + new + result[idx+len(old):] + } + return result +} diff --git a/internal/datetimeconverter/service.go b/internal/datetimeconverter/service.go new file mode 100644 index 0000000..f6c7222 --- /dev/null +++ b/internal/datetimeconverter/service.go @@ -0,0 +1,459 @@ +package datetimeconverter + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// Service defines the Unix Time service interface +type Service interface { + Convert(req ConvertRequest) ConvertResponse + GetPresets() PresetsResponse + CalculateDelta(req DeltaRequest) DeltaResponse + GetAvailableTimezones() AvailableTimezonesResponse +} + +// service implements the Service interface +type service struct{} + +// NewService creates a new Unix Time service +func NewService() Service { + return &service{} +} + +// Convert converts a timestamp or date string to all formats +func (s *service) Convert(req ConvertRequest) ConvertResponse { + if req.Input == "" { + return ErrorResponse(ErrInvalidTimestamp) + } + + // Detect input type + inputType := DetectInputType(req.Input) + + var t time.Time + var err error + + switch inputType { + case InputTypeTimestamp: + precision := Precision(req.Precision) + if precision == PrecisionAuto { + precision = DetectPrecision(req.Input) + } + t, err = ParseTimestamp(req.Input, precision) + if err != nil { + return ErrorResponse(err) + } + + case InputTypeISO, InputTypeDate: + t, err = parseDateString(req.Input) + if err != nil { + return ErrorResponse(err) + } + + default: + // Try to parse as timestamp anyway + t, err = ParseTimestamp(req.Input, PrecisionAuto) + if err != nil { + return ErrorResponse(ErrInvalidTimestamp) + } + inputType = InputTypeTimestamp + } + + // Apply timezone if specified + if req.Timezone != "" && req.Timezone != "local" && req.Timezone != "UTC" { + loc, err := time.LoadLocation(req.Timezone) + if err == nil { + t = t.In(loc) + } + } + + // Build result + result := buildTimeResult(t) + + // Format the output + format := FormatType(req.OutputFormat) + if format == "" { + format = FormatISO + } + result.Local = FormatTime(t, format, req.CustomFormat) + + detectedPrec := string(DetectPrecision(req.Input)) + if detectedPrec == "auto" { + detectedPrec = "seconds" + } + + return FromTimeResult(result, string(inputType), detectedPrec) +} + +// GetPresets returns all available quick presets +func (s *service) GetPresets() PresetsResponse { + now := time.Now() + + presets := []Preset{ + { + ID: "now", + Label: "Now", + Description: "Current timestamp", + Timestamp: now.Unix(), + }, + { + ID: "plus1hour", + Label: "+1 Hour", + Description: "One hour from now", + Timestamp: now.Add(time.Hour).Unix(), + }, + { + ID: "plus1day", + Label: "+1 Day", + Description: "One day from now", + Timestamp: now.Add(24 * time.Hour).Unix(), + }, + { + ID: "tomorrow9am", + Label: "Tomorrow 9am", + Description: "Tomorrow at 9:00 AM", + Timestamp: time.Date(now.Year(), now.Month(), now.Day()+1, 9, 0, 0, 0, now.Location()).Unix(), + }, + { + ID: "nextweek", + Label: "Next Week", + Description: "Same time next week", + Timestamp: now.Add(7 * 24 * time.Hour).Unix(), + }, + { + ID: "startofday", + Label: "Start of Day", + Description: "Today at 00:00:00", + Timestamp: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix(), + }, + { + ID: "endofday", + Label: "End of Day", + Description: "Today at 23:59:59", + Timestamp: time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).Unix(), + }, + { + ID: "startofweek", + Label: "Start of Week", + Description: "Monday 00:00:00", + Timestamp: getStartOfWeek(now).Unix(), + }, + { + ID: "endofweek", + Label: "End of Week", + Description: "Sunday 23:59:59", + Timestamp: getEndOfWeek(now).Unix(), + }, + { + ID: "epoch", + Label: "Unix Epoch", + Description: "January 1, 1970 00:00:00 UTC", + Timestamp: 0, + }, + } + + return PresetsResponse{Presets: presets} +} + +// CalculateDelta calculates the difference between two dates +func (s *service) CalculateDelta(req DeltaRequest) DeltaResponse { + dateA, err := parseFlexibleDate(req.DateA) + if err != nil { + return DeltaResponse{Error: err.Error()} + } + + dateB, err := parseFlexibleDate(req.DateB) + if err != nil { + return DeltaResponse{Error: err.Error()} + } + + diff := dateB.Sub(dateA) + absDiff := diff + if absDiff < 0 { + absDiff = -absDiff + } + + days := int(absDiff.Hours() / 24) + hours := int(absDiff.Hours()) % 24 + minutes := int(absDiff.Minutes()) % 60 + seconds := int(absDiff.Seconds()) % 60 + + delta := &TimeDelta{ + Days: days, + Hours: hours, + Minutes: minutes, + Seconds: seconds, + TotalHours: absDiff.Hours(), + TotalMinutes: absDiff.Minutes(), + TotalSeconds: absDiff.Seconds(), + IsFuture: diff > 0, + } + + return DeltaResponse{Delta: delta} +} + +// AddTime performs date arithmetic +func (s *service) AddTime(req ArithmeticRequest) ArithmeticResponse { + baseDate, err := parseFlexibleDate(req.BaseDate) + if err != nil { + return ArithmeticResponse{Error: err.Error()} + } + + unit := TimeUnit(req.Unit) + duration := time.Duration(req.Value) + + switch unit { + case UnitSeconds: + duration = duration * time.Second + case UnitMinutes: + duration = duration * time.Minute + case UnitHours: + duration = duration * time.Hour + case UnitDays: + duration = duration * 24 * time.Hour + case UnitWeeks: + duration = duration * 7 * 24 * time.Hour + case UnitMonths: + // Add months manually + baseDate = addMonths(baseDate, req.Value) + duration = 0 + case UnitYears: + // Add years manually + baseDate = addYears(baseDate, req.Value) + duration = 0 + default: + return ArithmeticResponse{Error: ErrInvalidUnit.Error()} + } + + if req.Operation == "subtract" { + duration = -duration + } + + result := baseDate.Add(duration) + + return ArithmeticResponse{Result: buildTimeResult(result)} +} + +func (s *service) GetAvailableTimezones() AvailableTimezonesResponse { + timezones := make([]TimezoneInfo, 0) + for _, zoneDir = range zoneDirs { + readTimezonesFromFile("", &timezones) + } + + return AvailableTimezonesResponse{ + Timezones: timezones, + } +} + +// Helper functions + +func parseDateString(input string) (time.Time, error) { + // Try various date formats + formats := []string{ + time.RFC3339, + time.RFC3339Nano, + time.RFC1123, + time.RFC1123Z, + time.RFC822, + time.RFC822Z, + "2006-01-02 15:04:05", + "2006-01-02 15:04", + "2006-01-02", + "01/02/2006 15:04:05", + "01/02/2006", + "02/01/2006 15:04:05", + "02/01/2006", + "20060102-150405", + "20060102", + } + + for _, format := range formats { + if t, err := time.Parse(format, input); err == nil { + return t, nil + } + } + + return time.Time{}, ErrInvalidDate +} + +func parseFlexibleDate(input string) (time.Time, error) { + // Try parsing as timestamp first + if ts, err := strconv.ParseInt(input, 10, 64); err == nil { + if ts > 1e12 { + return time.Unix(0, ts*1e6), nil // Milliseconds + } + return time.Unix(ts, 0), nil + } + + // Try parsing as date string + return parseDateString(input) +} + +func buildTimeResult(t time.Time) *TimeResult { + nanos := t.UnixNano() + + // Calculate relative time + now := time.Now() + diff := t.Sub(now) + relative := formatRelativeTime(diff) + relativeDetails := buildRelativeBreakdown(diff, t) + + return &TimeResult{ + UnixSeconds: t.Unix(), + UnixMillis: nanos / 1e6, + UnixMicros: nanos / 1e3, + UnixNanos: nanos, + UTC: t.UTC().Format(time.RFC3339), + Local: t.Local().Format("2006-01-02 15:04:05"), + Relative: relative, + RelativeDetails: relativeDetails, + } +} + +func formatRelativeTime(diff time.Duration) string { + if diff == 0 { + return "now" + } + + absDiff := diff + isFuture := diff > 0 + if absDiff < 0 { + absDiff = -absDiff + } + + days := int(absDiff.Hours() / 24) + hours := int(absDiff.Hours()) % 24 + minutes := int(absDiff.Minutes()) % 60 + seconds := int(absDiff.Seconds()) % 60 + + var parts []string + if days > 0 { + parts = append(parts, fmt.Sprintf("%d day%s", days, plural(days))) + } + if hours > 0 { + parts = append(parts, fmt.Sprintf("%d hour%s", hours, plural(hours))) + } + if minutes > 0 && days == 0 { + parts = append(parts, fmt.Sprintf("%d minute%s", minutes, plural(minutes))) + } + if seconds > 0 && days == 0 && hours == 0 { + parts = append(parts, fmt.Sprintf("%d second%s", seconds, plural(seconds))) + } + + result := strings.Join(parts, ", ") + if isFuture { + return "in " + result + } + return result + " ago" +} + +func buildRelativeBreakdown(diff time.Duration, t time.Time) RelativeBreakdown { + absDiff := diff + if absDiff < 0 { + absDiff = -absDiff + } + + days := int(absDiff.Hours() / 24) + hours := int(absDiff.Hours()) % 24 + minutes := int(absDiff.Minutes()) % 60 + seconds := int(absDiff.Seconds()) % 60 + + // Days since epoch + epoch := UnixEpoch() + daysSinceEpoch := int(t.Sub(epoch).Hours() / 24) + + return RelativeBreakdown{ + Days: days, + Hours: hours, + Minutes: minutes, + Seconds: seconds, + TotalHours: int(absDiff.Hours()), + TotalMinutes: int(absDiff.Minutes()), + TotalSeconds: int(absDiff.Seconds()), + DaysSinceEpoch: daysSinceEpoch, + } +} + +func plural(n int) string { + if n == 1 { + return "" + } + return "s" +} + +func getStartOfWeek(t time.Time) time.Time { + // Monday is start of week + weekday := int(t.Weekday()) + if weekday == 0 { + weekday = 7 + } + daysSinceMonday := weekday - 1 + return time.Date(t.Year(), t.Month(), t.Day()-daysSinceMonday, 0, 0, 0, 0, t.Location()) +} + +func getEndOfWeek(t time.Time) time.Time { + // Sunday is end of week + weekday := int(t.Weekday()) + if weekday == 0 { + return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location()) + } + daysUntilSunday := 7 - weekday + return time.Date(t.Year(), t.Month(), t.Day()+daysUntilSunday, 23, 59, 59, 0, t.Location()) +} + +func addMonths(t time.Time, months int) time.Time { + newMonth := t.Month() + time.Month(months) + newYear := t.Year() + + for newMonth > 12 { + newMonth -= 12 + newYear++ + } + for newMonth < 1 { + newMonth += 12 + newYear-- + } + + // Handle day overflow (e.g., Jan 31 + 1 month = Feb 28/29) + daysInMonth := daysIn(newMonth, newYear) + newDay := t.Day() + if newDay > daysInMonth { + newDay = daysInMonth + } + + return time.Date(newYear, newMonth, newDay, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()) +} + +func addYears(t time.Time, years int) time.Time { + newYear := t.Year() + years + + // Handle Feb 29 on non-leap years + if t.Month() == time.February && t.Day() == 29 { + if !isLeapYear(newYear) { + return time.Date(newYear, time.February, 28, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()) + } + } + + return time.Date(newYear, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()) +} + +func daysIn(m time.Month, year int) int { + switch m { + case time.January, time.March, time.May, time.July, time.August, time.October, time.December: + return 31 + case time.April, time.June, time.September, time.November: + return 30 + case time.February: + if isLeapYear(year) { + return 29 + } + return 28 + } + return 0 +} + +func isLeapYear(year int) bool { + return year%4 == 0 && (year%100 != 0 || year%400 == 0) +} diff --git a/internal/datetimeconverter/service_test.go b/internal/datetimeconverter/service_test.go new file mode 100644 index 0000000..4b655b4 --- /dev/null +++ b/internal/datetimeconverter/service_test.go @@ -0,0 +1,322 @@ +package datetimeconverter + +import ( + "testing" + "time" +) + +func TestDetectPrecision(t *testing.T) { + tests := []struct { + name string + input string + expected Precision + }{ + {"Seconds - 10 digits", "1738412345", PrecisionSeconds}, + {"Millis - 13 digits", "1738412345000", PrecisionMillis}, + {"Micros - 16 digits", "1738412345000000", PrecisionMicros}, + {"Nanos - 19 digits", "1738412345000000000", PrecisionNanos}, + {"Empty string", "", PrecisionAuto}, + {"Short number", "12345", PrecisionAuto}, + {"With non-digits", "1738412345abc", PrecisionSeconds}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := DetectPrecision(tt.input) + if result != tt.expected { + t.Errorf("DetectPrecision(%q) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} + +func TestDetectInputType(t *testing.T) { + tests := []struct { + name string + input string + expected InputType + }{ + {"ISO with T", "2026-02-01T12:24:05Z", InputTypeISO}, + {"ISO with Z", "2026-02-01Z", InputTypeISO}, + {"Timestamp seconds", "1738412345", InputTypeTimestamp}, + {"Timestamp millis", "1738412345000", InputTypeTimestamp}, + {"Date with dash", "2026-02-01", InputTypeDate}, + {"Date with slash", "02/01/2026", InputTypeDate}, + {"Empty string", "", InputTypeUnknown}, + {"Mixed text", "hello world", InputTypeUnknown}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := DetectInputType(tt.input) + if result != tt.expected { + t.Errorf("DetectInputType(%q) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} + +func TestParseTimestamp(t *testing.T) { + tests := []struct { + name string + input string + precision Precision + wantErr bool + }{ + {"Valid seconds", "1738412345", PrecisionSeconds, false}, + {"Valid millis", "1738412345000", PrecisionMillis, false}, + {"Valid micros", "1738412345000000", PrecisionMicros, false}, + {"Valid nanos", "1738412345000000000", PrecisionNanos, false}, + {"Auto detect seconds", "1738412345", PrecisionAuto, false}, + {"Auto detect millis", "1738412345000", PrecisionAuto, false}, + {"Empty string", "", PrecisionAuto, true}, + {"Invalid input", "abc", PrecisionAuto, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ParseTimestamp(tt.input, tt.precision) + if tt.wantErr { + if err == nil { + t.Errorf("ParseTimestamp(%q, %v) expected error, got nil", tt.input, tt.precision) + } + return + } + if err != nil { + t.Errorf("ParseTimestamp(%q, %v) unexpected error: %v", tt.input, tt.precision, err) + return + } + if result.IsZero() { + t.Errorf("ParseTimestamp(%q, %v) returned zero time", tt.input, tt.precision) + } + }) + } +} + +func TestFormatTime(t *testing.T) { + testTime := time.Date(2026, 2, 1, 12, 24, 5, 0, time.UTC) + + tests := []struct { + name string + format FormatType + customFormat string + wantContains string + }{ + {"ISO format", FormatISO, "", "2026-02-01"}, + {"RFC2822 format", FormatRFC2822, "", "Feb"}, + {"RFC3339 format", FormatRFC3339, "", "2026-02-01"}, + {"SQL format", FormatSQL, "", "2026-02-01 12:24:05"}, + {"US format", FormatUS, "", "02/01/2026"}, + {"EU format", FormatEU, "", "01/02/2026"}, + {"Compact format", FormatCompact, "", "20260201-122405"}, + {"Custom format", FormatCustom, "YYYY-MM-DD", "2026-02-01"}, + {"Default format", "", "", "2026-02-01"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatTime(testTime, tt.format, tt.customFormat) + if result == "" { + t.Errorf("FormatTime() returned empty string") + } + if tt.wantContains != "" && !containsStr(result, tt.wantContains) { + t.Errorf("FormatTime() = %q, want to contain %q", result, tt.wantContains) + } + }) + } +} + +func TestServiceConvert(t *testing.T) { + svc := NewService() + + tests := []struct { + name string + req ConvertRequest + wantErr bool + }{ + { + name: "Convert timestamp seconds", + req: ConvertRequest{ + Input: "1738412345", + Precision: "auto", + }, + wantErr: false, + }, + { + name: "Convert timestamp millis", + req: ConvertRequest{ + Input: "1738412345000", + Precision: "auto", + }, + wantErr: false, + }, + { + name: "Convert ISO date", + req: ConvertRequest{ + Input: "2026-02-01T12:24:05Z", + Precision: "auto", + }, + wantErr: false, + }, + { + name: "Convert SQL date", + req: ConvertRequest{ + Input: "2026-02-01 12:24:05", + Precision: "auto", + }, + wantErr: false, + }, + { + name: "Empty input", + req: ConvertRequest{ + Input: "", + Precision: "auto", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp := svc.Convert(tt.req) + if tt.wantErr { + if resp.Error == "" { + t.Errorf("Convert() expected error, got none") + } + return + } + if resp.Error != "" { + t.Errorf("Convert() unexpected error: %s", resp.Error) + return + } + if resp.Result == nil { + t.Errorf("Convert() result is nil") + return + } + if resp.Result.UnixSeconds == 0 { + t.Errorf("Convert() UnixSeconds is 0") + } + }) + } +} + +func TestServiceGetPresets(t *testing.T) { + svc := NewService() + resp := svc.GetPresets() + + if len(resp.Presets) == 0 { + t.Errorf("GetPresets() returned empty presets") + } + + expectedPresets := []string{"now", "plus1hour", "plus1day", "tomorrow9am", "nextweek", "startofday", "endofday", "startofweek", "endofweek", "epoch"} + for _, expected := range expectedPresets { + found := false + for _, preset := range resp.Presets { + if preset.ID == expected { + found = true + break + } + } + if !found { + t.Errorf("GetPresets() missing preset: %s", expected) + } + } +} + +func TestServiceCalculateDelta(t *testing.T) { + svc := NewService() + + tests := []struct { + name string + req DeltaRequest + wantErr bool + }{ + { + name: "Calculate delta between timestamps", + req: DeltaRequest{ + DateA: "1738412345", + DateB: "1738498745", + }, + wantErr: false, + }, + { + name: "Calculate delta between ISO dates", + req: DeltaRequest{ + DateA: "2026-02-01T12:00:00Z", + DateB: "2026-02-02T12:00:00Z", + }, + wantErr: false, + }, + { + name: "Invalid date A", + req: DeltaRequest{ + DateA: "invalid", + DateB: "1738412345", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp := svc.CalculateDelta(tt.req) + if tt.wantErr { + if resp.Error == "" { + t.Errorf("CalculateDelta() expected error, got none") + } + return + } + if resp.Error != "" { + t.Errorf("CalculateDelta() unexpected error: %s", resp.Error) + return + } + if resp.Delta == nil { + t.Errorf("CalculateDelta() delta is nil") + } + }) + } +} + +func TestFormatRelativeTime(t *testing.T) { + tests := []struct { + name string + diff time.Duration + expected string + }{ + {"Now", 0, "now"}, + {"1 second ago", -1 * time.Second, "1 second ago"}, + {"2 seconds ago", -2 * time.Second, "2 seconds ago"}, + {"1 minute ago", -1 * time.Minute, "1 minute ago"}, + {"1 hour ago", -1 * time.Hour, "1 hour ago"}, + {"1 day ago", -24 * time.Hour, "1 day ago"}, + {"In 1 second", 1 * time.Second, "in 1 second"}, + {"In 2 seconds", 2 * time.Second, "in 2 seconds"}, + {"In 1 minute", 1 * time.Minute, "in 1 minute"}, + {"In 1 hour", 1 * time.Hour, "in 1 hour"}, + {"In 1 day", 24 * time.Hour, "in 1 day"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := formatRelativeTime(tt.diff) + if result == "" { + t.Errorf("formatRelativeTime(%v) returned empty string", tt.diff) + } + // Just check it doesn't panic and returns something + t.Logf("formatRelativeTime(%v) = %q", tt.diff, result) + }) + } +} + +func containsStr(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || containsHelperStr(s, substr)) +} + +func containsHelperStr(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/internal/datetimeconverter/timezone.go b/internal/datetimeconverter/timezone.go new file mode 100644 index 0000000..f6e9001 --- /dev/null +++ b/internal/datetimeconverter/timezone.go @@ -0,0 +1,48 @@ +package datetimeconverter + +import ( + "fmt" + "os" + "strings" +) + +var zoneDirs = []string{ + "/usr/share/zoneinfo/", + "/usr/share/lib/zoneinfo/", + "/usr/lib/locale/TZ/", +} + +var excludeTzAbbrs = map[string]struct{}{ + "+VERSION": {}, +} + +var zoneDir string + +func readTimezonesFromFile(path string, timezones *[]TimezoneInfo) { + files, _ := os.ReadDir(zoneDir + path) + for _, f := range files { + if f.Name() != strings.ToUpper(f.Name()[:1])+f.Name()[1:] { + continue + } + + if f.IsDir() { + readTimezonesFromFile(path+"/"+f.Name(), timezones) + } else { + // Exclude tz abbreviation if it's matched to any of the given list + if _, ok := excludeTzAbbrs[f.Name()]; ok { + continue + } + + tzAbbr := (path + "/" + f.Name())[1:] + label := tzAbbr + if strings.Contains(tzAbbr, "/") { + label = fmt.Sprintf("%s (%s)", f.Name(), tzAbbr) + } + + *timezones = append(*timezones, TimezoneInfo{ + Label: label, + Timezone: tzAbbr, + }) + } + } +} diff --git a/internal/jwt/errors.go b/internal/jwt/errors.go index ed2a0a4..aaaf5c0 100644 --- a/internal/jwt/errors.go +++ b/internal/jwt/errors.go @@ -1,7 +1,7 @@ package jwt import ( - sharedErrors "dev-toolbox/pkg/errors" + sharedErrors "devtoolbox/pkg/errors" ) // JWTError represents a JWT-specific error diff --git a/internal/jwt/parser.go b/internal/jwt/parser.go index af0581f..6b46f02 100644 --- a/internal/jwt/parser.go +++ b/internal/jwt/parser.go @@ -1,10 +1,11 @@ package jwt import ( - "dev-toolbox/pkg/encoding" - "dev-toolbox/pkg/validation" "strings" + "devtoolbox/pkg/encoding" + "devtoolbox/pkg/validation" + "github.com/golang-jwt/jwt/v5" ) diff --git a/main.go b/main.go index 38cbd28..4af1459 100644 --- a/main.go +++ b/main.go @@ -1,62 +1,124 @@ package main import ( - "context" + "devtoolbox/service" "embed" + "log" + "net/http" + "strings" + "time" - "dev-toolbox/internal/wails" - - wails_runtime "github.com/wailsapp/wails/v2" - "github.com/wailsapp/wails/v2/pkg/options" - "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/gin-gonic/gin" + "github.com/wailsapp/wails/v3/pkg/application" ) -//go:embed all:dist +//go:embed all:frontend/dist var assets embed.FS +func init() { + // Register a custom event whose associated data type is string. + // This is not required, but the binding generator will pick up registered events + // and provide a strongly typed JS/TS API for them. + application.RegisterEvent[string]("time") +} + func main() { - // Create instances of the app structures - app := NewApp() - jwtService := wails.NewJWTService() - conversionService := wails.NewConversionService() - barcodeService := wails.NewBarcodeService() - dataGeneratorService := wails.NewDataGeneratorService() - codeFormatterService := wails.NewCodeFormatterService() - - // Start HTTP server for Web Mode (port 8081) - go func() { - server := NewServer(jwtService, conversionService, barcodeService, dataGeneratorService, codeFormatterService) - server.Start(8081) - }() + ginEngine := gin.New() + ginEngine.Use(gin.Recovery()) + ginEngine.Use(LoggingMiddleware()) + + ginEngine.StaticFS("/static", http.FS(assets)) + ginEngine.GET("/", func(c *gin.Context) { + file, _ := assets.ReadFile("static/index.html") + c.Data(http.StatusOK, "text/html; charset=utf-8", file) + }) + + ginEngine.GET("/api/hello", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "Hello from Gin API!", + "time": time.Now().Format(time.RFC3339), + }) + }) // Create application with options - err := wails_runtime.Run(&options.App{ - Title: "dev-toolbox", + app := application.New(application.Options{ + Name: "DevToolbox", + Description: "Set of tools for daily development", + Services: []application.Service{ + application.NewService(&GreetService{}), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + Assets: application.AssetOptions{ + // Handler: ginEngine, + // Middleware: GinMiddleware(ginEngine), + Handler: application.AssetFileServerFS(assets), + }, + }) + + // Register app services + app.RegisterService(application.NewService(service.NewJWTService(app))) + app.RegisterService(application.NewService(service.NewDateTimeService(app))) + app.RegisterService(application.NewService(service.NewConversionService(app))) + app.RegisterService(application.NewService(service.NewBarcodeService(app))) + app.RegisterService(application.NewService(service.NewDataGeneratorService(app))) + app.RegisterService(application.NewService(service.NewCodeFormatterService(app))) + + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "DevToolbox", Width: 1024, Height: 768, - AssetServer: &assetserver.Options{ - Assets: assets, - }, - BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, - OnStartup: func(ctx context.Context) { - app.startup(ctx) - jwtService.Startup(ctx) - conversionService.Startup(ctx) - barcodeService.Startup(ctx) - dataGeneratorService.Startup(ctx) - codeFormatterService.Startup(ctx) + BackgroundColour: application.RGBA{ + Red: 27, + Green: 38, + Blue: 54, + Alpha: 1, }, - Bind: []interface{}{ - app, - jwtService, - conversionService, - barcodeService, - dataGeneratorService, - codeFormatterService, + Mac: application.MacWindow{ + InvisibleTitleBarHeight: 50, + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInset, }, + URL: "/", }) - if err != nil { - println("Error:", err.Error()) + if err := app.Run(); err != nil { + panic(err) + } +} + +func GinMiddleware(ginEngine *gin.Engine) application.Middleware { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/wails") { + next.ServeHTTP(w, r) + return + } + + ginEngine.ServeHTTP(w, r) + }) + } +} + +func LoggingMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Start timer + startTime := time.Now() + + // Process request + c.Next() + + // Calculate latency + latency := time.Since(startTime) + + // Log request details + log.Printf("[GIN] %s | %s | %s | %d | %s", + c.Request.Method, + c.Request.URL.Path, + c.ClientIP(), + c.Writer.Status(), + latency, + ) } } diff --git a/server.go b/server.go index aa9da98..8eb88e0 100644 --- a/server.go +++ b/server.go @@ -1,34 +1,46 @@ package main import ( + "devtoolbox/internal/barcode" + "devtoolbox/internal/codeformatter" + "devtoolbox/internal/datagenerator" + "devtoolbox/internal/datetimeconverter" + "devtoolbox/service" "encoding/json" "fmt" "log" "net/http" "strings" - - "dev-toolbox/internal/codeformatter" - "dev-toolbox/internal/datagenerator" - "dev-toolbox/internal/wails" ) +// TODO: Think of a way to expose this by Gin, so i can test it via Http client + // Server represents the HTTP server for Web Mode type Server struct { - jwtService *wails.JWTService - conversionService *wails.ConversionService - barcodeService *wails.BarcodeService - dataGeneratorService *wails.DataGeneratorService - codeFormatterService *wails.CodeFormatterService + jwtService *service.JWTService + conversionService *service.ConversionService + barcodeService *service.BarcodeService + dataGeneratorService *service.DataGeneratorService + codeFormatterService *service.CodeFormatterService + dateTimeService *service.DateTimeService } // NewServer creates a new Server instance -func NewServer(jwtService *wails.JWTService, conversionService *wails.ConversionService, barcodeService *wails.BarcodeService, dataGeneratorService *wails.DataGeneratorService, codeFormatterService *wails.CodeFormatterService) *Server { +func NewServer( + jwtService *service.JWTService, + conversionService *service.ConversionService, + barcodeService *service.BarcodeService, + dataGeneratorService *service.DataGeneratorService, + codeFormatterService *service.CodeFormatterService, + dateTimeService *service.DateTimeService, +) *Server { return &Server{ jwtService: jwtService, conversionService: conversionService, barcodeService: barcodeService, dataGeneratorService: dataGeneratorService, codeFormatterService: codeFormatterService, + dateTimeService: dateTimeService, } } @@ -93,6 +105,11 @@ func (s *Server) handleAPI(w http.ResponseWriter, r *http.Request) { return } + if service == "DateTimeService" { + s.handleDateTimeService(method, w, r) + return + } + http.Error(w, fmt.Sprintf("Service not found: %s", service), http.StatusNotFound) } @@ -214,7 +231,7 @@ func (s *Server) handleBarcodeService(method string, w http.ResponseWriter, r *h return } - req := wails.GenerateBarcodeRequest{ + req := barcode.GenerateBarcodeRequest{ Content: getStringFromMap(reqData, "content"), Standard: getStringFromMap(reqData, "standard"), Size: getIntFromMap(reqData, "size"), @@ -372,6 +389,73 @@ func getBoolFromMap(m map[string]interface{}, key string) bool { return false } +func (s *Server) handleDateTimeService(method string, w http.ResponseWriter, r *http.Request) { + var payload struct { + Args []interface{} `json:"args"` + } + + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + var result interface{} + var err error + + switch method { + case "Convert": + if len(payload.Args) < 1 { + http.Error(w, "Missing arguments", http.StatusBadRequest) + return + } + reqData, ok := payload.Args[0].(map[string]interface{}) + if !ok { + http.Error(w, "Invalid request format", http.StatusBadRequest) + return + } + + req := datetimeconverter.ConvertRequest{ + Input: getStringFromMap(reqData, "input"), + Precision: getStringFromMap(reqData, "precision"), + Timezone: getStringFromMap(reqData, "timezone"), + OutputFormat: getStringFromMap(reqData, "outputFormat"), + CustomFormat: getStringFromMap(reqData, "customFormat"), + } + result, err = s.dateTimeService.Convert(req) + case "GetPresets": + result, err = s.dateTimeService.GetPresets() + case "CalculateDelta": + if len(payload.Args) < 1 { + http.Error(w, "Missing arguments", http.StatusBadRequest) + return + } + reqData, ok := payload.Args[0].(map[string]interface{}) + if !ok { + http.Error(w, "Invalid request format", http.StatusBadRequest) + return + } + + req := datetimeconverter.DeltaRequest{ + DateA: getStringFromMap(reqData, "dateA"), + DateB: getStringFromMap(reqData, "dateB"), + } + result, err = s.dateTimeService.CalculateDelta(req) + case "GetAvailableTimezones": + result, err = s.dateTimeService.GetAvailableTimezones() + default: + http.Error(w, fmt.Sprintf("Method not found: %s", method), http.StatusNotFound) + return + } + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") diff --git a/service/barcode.go b/service/barcode.go new file mode 100644 index 0000000..f13d1f4 --- /dev/null +++ b/service/barcode.go @@ -0,0 +1,49 @@ +package service + +import ( + "context" + "devtoolbox/internal/barcode" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +type BarcodeService struct { + app *application.App + svc barcode.BarcodeService +} + +func NewBarcodeService(app *application.App) *BarcodeService { + return &BarcodeService{ + app: app, + svc: barcode.NewBarcodeService(), + } +} + +func (s *BarcodeService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return nil +} + +// GenerateBarcode generates a barcode based on the selected standard +func (s *BarcodeService) GenerateBarcode(req barcode.GenerateBarcodeRequest) barcode.GenerateBarcodeResponse { + return s.svc.GenerateBarcode(req) +} + +// GetBarcodeStandards returns available barcode standards +func (s *BarcodeService) GetBarcodeStandards() []map[string]string { + return s.svc.GetBarcodeStandards() +} + +// GetQRErrorLevels returns available error correction levels for QR codes +func (s *BarcodeService) GetQRErrorLevels() []map[string]string { + return s.svc.GetQRErrorLevels() +} + +// GetBarcodeSizes returns available barcode sizes +func (s *BarcodeService) GetBarcodeSizes() []map[string]interface{} { + return s.svc.GetBarcodeSizes() +} + +// ValidateContent validates content for specific barcode standards +func (s *BarcodeService) ValidateContent(content string, standard string) map[string]interface{} { + return s.svc.ValidateContent(content, standard) +} diff --git a/internal/wails/codeformatter_service.go b/service/codeformatter.go similarity index 65% rename from internal/wails/codeformatter_service.go rename to service/codeformatter.go index 3eef29b..0e4e931 100644 --- a/internal/wails/codeformatter_service.go +++ b/service/codeformatter.go @@ -1,26 +1,29 @@ -package wails +package service import ( "context" - "dev-toolbox/internal/codeformatter" + "devtoolbox/internal/codeformatter" + + "github.com/wailsapp/wails/v3/pkg/application" ) // CodeFormatterService is the Wails binding struct for code formatting operations type CodeFormatterService struct { - ctx context.Context + app *application.App svc codeformatter.CodeFormatterService } // NewCodeFormatterService creates a new CodeFormatterService instance -func NewCodeFormatterService() *CodeFormatterService { +func NewCodeFormatterService(app *application.App) *CodeFormatterService { return &CodeFormatterService{ svc: codeformatter.NewCodeFormatterService(), + app: app, } } // Startup is called when the app starts (Wails lifecycle) -func (c *CodeFormatterService) Startup(ctx context.Context) { - c.ctx = ctx +func (c *CodeFormatterService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return nil } // Format formats code based on the request diff --git a/internal/wails/conversion_service.go b/service/conversion.go similarity index 57% rename from internal/wails/conversion_service.go rename to service/conversion.go index 377ccd7..4cca3f2 100644 --- a/internal/wails/conversion_service.go +++ b/service/conversion.go @@ -1,23 +1,26 @@ -package wails +package service import ( "context" - "dev-toolbox/internal/converter" + "devtoolbox/internal/converter" + + "github.com/wailsapp/wails/v3/pkg/application" ) type ConversionService struct { - ctx context.Context + app *application.App svc converter.ConverterService } -func NewConversionService() *ConversionService { +func NewConversionService(app *application.App) *ConversionService { return &ConversionService{ svc: converter.NewConverterService(), + app: app, } } -func (s *ConversionService) Startup(ctx context.Context) { - s.ctx = ctx +func (s *ConversionService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return nil } func (s *ConversionService) Convert(input, category, method string, config map[string]interface{}) (string, error) { diff --git a/internal/wails/data_generator_service.go b/service/datagenerator.go similarity index 79% rename from internal/wails/data_generator_service.go rename to service/datagenerator.go index 7866ed9..ffe6d4d 100644 --- a/internal/wails/data_generator_service.go +++ b/service/datagenerator.go @@ -1,27 +1,29 @@ -package wails +package service import ( "context" + "devtoolbox/internal/datagenerator" - "dev-toolbox/internal/datagenerator" + "github.com/wailsapp/wails/v3/pkg/application" ) // DataGeneratorService provides data generation functionality via Wails type DataGeneratorService struct { - ctx context.Context + app *application.App svc datagenerator.DataGeneratorService } // NewDataGeneratorService creates a new DataGeneratorService -func NewDataGeneratorService() *DataGeneratorService { +func NewDataGeneratorService(app *application.App) *DataGeneratorService { return &DataGeneratorService{ svc: datagenerator.NewDataGeneratorService(), + app: app, } } // Startup is called when the service starts -func (d *DataGeneratorService) Startup(ctx context.Context) { - d.ctx = ctx +func (d *DataGeneratorService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return nil } // Generate generates data based on the provided request diff --git a/service/datetime.go b/service/datetime.go new file mode 100644 index 0000000..e9fef8b --- /dev/null +++ b/service/datetime.go @@ -0,0 +1,49 @@ +package service + +import ( + "context" + "devtoolbox/internal/datetimeconverter" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +// DateTimeService is the Wails binding struct for Unix Time operations +type DateTimeService struct { + app *application.App + svc datetimeconverter.Service +} + +// NewDateTimeService creates a new UnixTimeService instance +func NewDateTimeService(app *application.App) *DateTimeService { + return &DateTimeService{ + svc: datetimeconverter.NewService(), + app: app, + } +} + +// ServiceStartup is called when the app starts (Wails lifecycle) +func (u *DateTimeService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return nil +} + +// Convert converts a timestamp or date string to all formats +// This method is exposed to the frontend via Wails +func (u *DateTimeService) Convert(req datetimeconverter.ConvertRequest) (datetimeconverter.ConvertResponse, error) { + return u.svc.Convert(req), nil +} + +// GetPresets returns all available quick presets +// This method is exposed to the frontend via Wails +func (u *DateTimeService) GetPresets() (datetimeconverter.PresetsResponse, error) { + return u.svc.GetPresets(), nil +} + +// CalculateDelta calculates the difference between two dates +// This method is exposed to the frontend via Wails +func (u *DateTimeService) CalculateDelta(req datetimeconverter.DeltaRequest) (datetimeconverter.DeltaResponse, error) { + return u.svc.CalculateDelta(req), nil +} + +func (u *DateTimeService) GetAvailableTimezones() (datetimeconverter.AvailableTimezonesResponse, error) { + return u.svc.GetAvailableTimezones(), nil +} diff --git a/internal/wails/jwt_service.go b/service/jwt.go similarity index 83% rename from internal/wails/jwt_service.go rename to service/jwt.go index ec3bd1c..e93b1f5 100644 --- a/internal/wails/jwt_service.go +++ b/service/jwt.go @@ -1,29 +1,30 @@ -package wails +package service import ( "context" - "dev-toolbox/internal/jwt" + "devtoolbox/internal/jwt" "encoding/json" + + "github.com/wailsapp/wails/v3/pkg/application" ) // JWTService is the Wails binding struct for JWT operations type JWTService struct { - ctx context.Context svc jwt.JWTService + app *application.App } // NewJWTService creates a new JWTService instance -func NewJWTService() *JWTService { - parser := jwt.NewParser() - svc := jwt.NewJWTService(parser) +func NewJWTService(app *application.App) *JWTService { return &JWTService{ - svc: svc, + svc: jwt.NewJWTService(jwt.NewParser()), + app: app, } } -// Startup is called when the app starts (Wails lifecycle) -func (j *JWTService) Startup(ctx context.Context) { - j.ctx = ctx +// ServiceStartup is called when the app starts (Wails lifecycle) +func (j *JWTService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return nil } // Decode decodes a JWT token diff --git a/src/pages/UnixTimeConverter.jsx b/src/pages/UnixTimeConverter.jsx deleted file mode 100644 index ddc907e..0000000 --- a/src/pages/UnixTimeConverter.jsx +++ /dev/null @@ -1,409 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Button, TextInput, Tag, Dropdown, TextArea } from '@carbon/react'; -import { Time, Calendar } from '@carbon/icons-react'; -import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../components/ToolUI'; -import useLayoutToggle from '../hooks/useLayoutToggle'; - -const DATE_FORMATS = [ - { id: 'iso', label: 'ISO 8601', format: 'YYYY-MM-DDTHH:mm:ss.sssZ' }, - { id: 'rfc2822', label: 'RFC 2822', format: 'ddd, DD MMM YYYY HH:mm:ss ZZ' }, - { id: 'sql', label: 'SQL DateTime', format: 'YYYY-MM-DD HH:mm:ss' }, - { id: 'us', label: 'US Format', format: 'MM/DD/YYYY HH:mm:ss' }, - { id: 'eu', label: 'EU Format', format: 'DD/MM/YYYY HH:mm:ss' }, - { id: 'compact', label: 'Compact', format: 'YYYYMMDD-HHmmss' }, - { id: 'custom', label: 'Custom', format: 'Custom format' }, -]; - -const TIMEZONES = [ - { id: 'UTC', label: 'UTC', offset: 0 }, - { id: 'local', label: 'Local Time', offset: null }, - { id: 'EST', label: 'EST (New York)', offset: -5 }, - { id: 'CST', label: 'CST (Chicago)', offset: -6 }, - { id: 'MST', label: 'MST (Denver)', offset: -7 }, - { id: 'PST', label: 'PST (Los Angeles)', offset: -8 }, - { id: 'GMT', label: 'GMT (London)', offset: 0 }, - { id: 'CET', label: 'CET (Paris)', offset: 1 }, - { id: 'IST', label: 'IST (India)', offset: 5.5 }, - { id: 'JST', label: 'JST (Tokyo)', offset: 9 }, - { id: 'AEST', label: 'AEST (Sydney)', offset: 10 }, -]; - -const parseDateWithFormat = (input, format) => { - if (!input) return null; - - // Try parsing as ISO first - let date = new Date(input); - if (!isNaN(date.getTime())) return date; - - // Try parsing as timestamp - if (!isNaN(input) && input.toString().length >= 10) { - const ts = parseInt(input, 10); - if (ts > 1000000000) { - date = new Date(ts > 9999999999 ? ts : ts * 1000); - if (!isNaN(date.getTime())) return date; - } - } - - return null; -}; - -const formatDate = (date, formatId, customFormat) => { - if (!date || isNaN(date.getTime())) return ''; - - const pad = (n) => n.toString().padStart(2, '0'); - const year = date.getFullYear(); - const month = pad(date.getMonth() + 1); - const day = pad(date.getDate()); - const hours = pad(date.getHours()); - const minutes = pad(date.getMinutes()); - const seconds = pad(date.getSeconds()); - const ms = date.getMilliseconds().toString().padStart(3, '0'); - - switch (formatId) { - case 'iso': - return date.toISOString(); - case 'rfc2822': - return date.toUTCString(); - case 'sql': - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; - case 'us': - return `${month}/${day}/${year} ${hours}:${minutes}:${seconds}`; - case 'eu': - return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`; - case 'compact': - return `${year}${month}${day}-${hours}${minutes}${seconds}`; - case 'custom': - if (customFormat) { - // Simple format replacement - return customFormat - .replace('YYYY', year) - .replace('MM', month) - .replace('DD', day) - .replace('HH', hours) - .replace('mm', minutes) - .replace('ss', seconds) - .replace('sss', ms); - } - return date.toISOString(); - default: - return date.toISOString(); - } -}; - -const convertToTimezone = (date, timezoneId) => { - if (!date || timezoneId === 'local') return date; - - const tz = TIMEZONES.find(t => t.id === timezoneId); - if (!tz || tz.offset === null) return date; - - const offsetMs = tz.offset * 60 * 60 * 1000; - return new Date(date.getTime() + offsetMs); -}; - -export default function UnixTimeConverter() { - const [timestamp, setTimestamp] = useState(''); - const [dateStr, setDateStr] = useState(''); - const [localDateStr, setLocalDateStr] = useState(''); - const [relativeTime, setRelativeTime] = useState(''); - const [inputFormat, setInputFormat] = useState('iso'); - const [outputFormat, setOutputFormat] = useState('iso'); - const [customInputFormat, setCustomInputFormat] = useState('YYYY-MM-DD HH:mm:ss'); - const [customOutputFormat, setCustomOutputFormat] = useState('YYYY-MM-DD HH:mm:ss'); - const [sourceTimezone, setSourceTimezone] = useState('local'); - const [targetTimezone, setTargetTimezone] = useState('local'); - const [parsedDate, setParsedDate] = useState(null); - - const layout = useLayoutToggle({ - toolKey: 'unix-time-layout', - defaultDirection: 'horizontal', - showToggle: true, - persist: true - }); - - // Initialize with current time - useEffect(() => { - const now = Math.floor(Date.now() / 1000); - handleTsChange(now.toString()); - }, []); - - const calculateRelativeTime = (date) => { - if (!date || isNaN(date.getTime())) return ''; - - const now = Date.now(); - const diff = date.getTime() - now; - const absDiff = Math.abs(diff); - const seconds = Math.floor(absDiff / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - let relative = ''; - if (diff > 0) { - relative = 'in '; - } - - if (days > 0) relative += `${days} day${days > 1 ? 's' : ''} `; - else if (hours > 0) relative += `${hours} hour${hours > 1 ? 's' : ''} `; - else if (minutes > 0) relative += `${minutes} minute${minutes > 1 ? 's' : ''} `; - else relative += `${seconds} second${seconds > 1 ? 's' : ''} `; - - if (diff <= 0) { - relative += 'ago'; - } - - return relative; - }; - - const handleTsChange = (val) => { - setTimestamp(val); - - if (!val || val.trim() === '') { - setDateStr(''); - setLocalDateStr(''); - setRelativeTime(''); - setParsedDate(null); - return; - } - - // Try parsing as timestamp first - let date; - const numVal = parseInt(val, 10); - - if (!isNaN(numVal) && val.length >= 10) { - // Assume it's a Unix timestamp - const ts = numVal > 9999999999 ? numVal : numVal * 1000; - date = new Date(ts); - } else { - // Try parsing as date string - date = parseDateWithFormat(val, inputFormat); - } - - if (!date || isNaN(date.getTime())) { - setDateStr('Invalid date'); - setLocalDateStr('Invalid date'); - setRelativeTime(''); - setParsedDate(null); - return; - } - - setParsedDate(date); - - // Convert to source timezone - const sourceDate = sourceTimezone === 'local' ? date : convertToTimezone(date, sourceTimezone); - - // Convert to target timezone - const targetDate = targetTimezone === 'local' ? sourceDate : convertToTimezone(sourceDate, targetTimezone); - - const formatted = formatDate(targetDate, outputFormat, customOutputFormat); - setDateStr(formatted); - setLocalDateStr(targetDate.toLocaleString()); - setRelativeTime(calculateRelativeTime(date)); - }; - - const handleDateChange = (val) => { - setDateStr(val); - - const date = parseDateWithFormat(val, outputFormat); - - if (date && !isNaN(date.getTime())) { - setParsedDate(date); - const ts = Math.floor(date.getTime() / 1000); - setTimestamp(ts.toString()); - setLocalDateStr(date.toLocaleString()); - setRelativeTime(calculateRelativeTime(date)); - } - }; - - const setNow = () => { - const now = Math.floor(Date.now() / 1000); - handleTsChange(now.toString()); - }; - - const isValidTimestamp = timestamp && !isNaN(timestamp) && timestamp.length > 0; - - return ( -
- - - -
-
- handleTsChange(e.target.value)} - placeholder="Enter timestamp or date..." - style={{ fontFamily: "'IBM Plex Mono', monospace" }} - /> -
- - -
-
- - -
-
- - item ? item.label : ''} - selectedItem={DATE_FORMATS.find(f => f.id === inputFormat)} - onChange={({ selectedItem }) => { - if (selectedItem) { - setInputFormat(selectedItem.id); - } - }} - size="sm" - /> -
- - {inputFormat === 'custom' && ( -
- setCustomInputFormat(e.target.value)} - placeholder="YYYY-MM-DD HH:mm:ss" - size="sm" - /> -
- )} - -
- - item ? item.label : ''} - selectedItem={TIMEZONES.find(t => t.id === sourceTimezone)} - onChange={({ selectedItem }) => { - if (selectedItem) { - setSourceTimezone(selectedItem.id); - if (timestamp) handleTsChange(timestamp); - } - }} - size="sm" - /> -
-
-
- - -
-
- - item ? item.label : ''} - selectedItem={DATE_FORMATS.find(f => f.id === outputFormat)} - onChange={({ selectedItem }) => { - if (selectedItem) { - setOutputFormat(selectedItem.id); - if (timestamp) handleTsChange(timestamp); - } - }} - size="sm" - /> -
- - {outputFormat === 'custom' && ( -
- setCustomOutputFormat(e.target.value)} - placeholder="YYYY-MM-DD HH:mm:ss" - size="sm" - /> -
- )} - -
- - item ? item.label : ''} - selectedItem={TIMEZONES.find(t => t.id === targetTimezone)} - onChange={({ selectedItem }) => { - if (selectedItem) { - setTargetTimezone(selectedItem.id); - if (timestamp) handleTsChange(timestamp); - } - }} - size="sm" - /> -
-
-
- - {relativeTime && isValidTimestamp && ( -
- {relativeTime} - - from now - -
- )} - - - f.id === outputFormat)?.label || 'ISO'})`} - value={dateStr} - onChange={(e) => handleDateChange(e.target.value)} - placeholder="Formatted date will appear here..." - /> - t.id === targetTimezone)?.label || 'Local'})`} - value={localDateStr} - readOnly - placeholder="Local date and time will appear here..." - /> - -
- ); -} diff --git a/src/utils/backendBridge.js b/src/utils/backendBridge.js deleted file mode 100644 index f29d4bf..0000000 --- a/src/utils/backendBridge.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Backend Bridge - * - * seamless communication bridge that detects if the app is running - * in Wails (native) or Web Mode (browser) and routes requests accordingly. - */ - -// Detect Wails environment by checking for window.runtime -const isWails = () => typeof window.runtime !== 'undefined'; - -// Base URL for the local HTTP server in Web Mode -const API_BASE_URL = 'http://localhost:8081'; - -/** - * Generic helper to call backend methods - */ -async function callBackend(service, method, ...args) { - if (isWails()) { - // Direct Wails binding call - // Wails generates bindings under window.go.wails (not window.go.main) - try { - return await window.go.wails[service][method](...args); - } catch (err) { - console.error(`Wails call failed: ${service}.${method}`, err); - throw err; - } - } else { - // Remote HTTP call for Web Mode - try { - const response = await fetch(`${API_BASE_URL}/api/${service}/${method}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ args }), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP error ${response.status}: ${errorText}`); - } - - return await response.json(); - } catch (err) { - console.error(`Web API call failed: ${service}/${method}`, err); - throw err; - } - } -} - -// Exposed Backend Services -export const Backend = { - JWTService: { - Decode: (token) => callBackend('JWTService', 'Decode', token), - Encode: (header, payload, algo, secret) => callBackend('JWTService', 'Encode', header, payload, algo, secret), - Verify: (token, secret, encoding) => callBackend('JWTService', 'Verify', token, secret, encoding) - }, - ConversionService: { - Convert: (input, category, method, config) => callBackend('ConversionService', 'Convert', input, category, method, config) - }, - BarcodeService: { - GenerateBarcode: (req) => callBackend('BarcodeService', 'GenerateBarcode', req), - GetQRErrorLevels: () => callBackend('BarcodeService', 'GetQRErrorLevels'), - GetBarcodeSizes: () => callBackend('BarcodeService', 'GetBarcodeSizes'), - GetBarcodeStandards: () => callBackend('BarcodeService', 'GetBarcodeStandards'), - ValidateContent: (content, standard) => callBackend('BarcodeService', 'ValidateContent', content, standard) - }, - DataGeneratorService: { - Generate: (req) => callBackend('DataGeneratorService', 'Generate', req), - GetPresets: () => callBackend('DataGeneratorService', 'GetPresets'), - ValidateTemplate: (template) => callBackend('DataGeneratorService', 'ValidateTemplate', template) - }, - CodeFormatterService: { - Format: (req) => callBackend('CodeFormatterService', 'Format', req) - } -}; diff --git a/vite.config.js b/vite.config.js index 7b6a07b..0d0b508 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,9 +1,10 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import wails from "@wailsio/runtime/plugins/vite"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), wails("./bindings")], css: { preprocessorOptions: { scss: { @@ -13,4 +14,4 @@ export default defineConfig({ } } } -}) +}) \ No newline at end of file diff --git a/wails.json b/wails.json index f108099..078d1f9 100644 --- a/wails.json +++ b/wails.json @@ -1,14 +1,10 @@ { - "$schema": "https://wails.io/schemas/config.v2.json", - "name": "dev-toolbox", - "outputfilename": "dev-toolbox", - "frontend:install": "bun install", - "frontend:build": "bun run build", - "frontend:dev:watcher": "bun run dev", - "frontend:dev:serverUrl": "auto", - "frontend:dir": ".", - "author": { - "name": "Vuong", - "email": "3168632+vuon9@users.noreply.github.com" + "name": "DevToolbox", + "frontend": { + "dir": "./frontend", + "install": "bun install", + "build": "bun run build", + "dev": "bun run dev", + "devServerUrl": "http://localhost:5173" } -} +} \ No newline at end of file diff --git a/wailsjs/go/main/App.d.ts b/wailsjs/go/main/App.d.ts deleted file mode 100755 index 02a3bb9..0000000 --- a/wailsjs/go/main/App.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 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/wailsjs/go/main/App.js b/wailsjs/go/main/App.js deleted file mode 100755 index c71ae77..0000000 --- a/wailsjs/go/main/App.js +++ /dev/null @@ -1,7 +0,0 @@ -// @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/wailsjs/go/models.ts b/wailsjs/go/models.ts deleted file mode 100755 index 2e2cbec..0000000 --- a/wailsjs/go/models.ts +++ /dev/null @@ -1,285 +0,0 @@ -export namespace codeformatter { - - export class FormatRequest { - input: string; - formatType: string; - filter?: string; - minify: boolean; - - static createFrom(source: any = {}) { - return new FormatRequest(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.input = source["input"]; - this.formatType = source["formatType"]; - this.filter = source["filter"]; - this.minify = source["minify"]; - } - } - export class FormatResponse { - output: string; - error?: string; - - static createFrom(source: any = {}) { - return new FormatResponse(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.output = source["output"]; - this.error = source["error"]; - } - } - -} - -export namespace datagenerator { - - export class GenerateRequest { - template: string; - variables: Record; - batchCount: number; - outputFormat: string; - separator: string; - - static createFrom(source: any = {}) { - return new GenerateRequest(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.template = source["template"]; - this.variables = source["variables"]; - this.batchCount = source["batchCount"]; - this.outputFormat = source["outputFormat"]; - this.separator = source["separator"]; - } - } - export class GenerateResponse { - output: string; - count: number; - error?: string; - durationMs: number; - - static createFrom(source: any = {}) { - return new GenerateResponse(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.output = source["output"]; - this.count = source["count"]; - this.error = source["error"]; - this.durationMs = source["durationMs"]; - } - } - export class Variable { - name: string; - type: string; - default: any; - options?: string[]; - min?: number; - max?: number; - description?: string; - - static createFrom(source: any = {}) { - return new Variable(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.name = source["name"]; - this.type = source["type"]; - this.default = source["default"]; - this.options = source["options"]; - this.min = source["min"]; - this.max = source["max"]; - this.description = source["description"]; - } - } - export class TemplatePreset { - id: string; - name: string; - description: string; - template: string; - variables: Variable[]; - - static createFrom(source: any = {}) { - return new TemplatePreset(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.id = source["id"]; - this.name = source["name"]; - this.description = source["description"]; - this.template = source["template"]; - this.variables = this.convertValues(source["variables"], Variable); - } - - 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 PresetsResponse { - presets: TemplatePreset[]; - error?: string; - - static createFrom(source: any = {}) { - return new PresetsResponse(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.presets = this.convertValues(source["presets"], TemplatePreset); - this.error = source["error"]; - } - - 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 ValidationResult { - valid: boolean; - error?: string; - message?: string; - - static createFrom(source: any = {}) { - return new ValidationResult(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.valid = source["valid"]; - this.error = source["error"]; - this.message = source["message"]; - } - } - -} - -export namespace jwt { - - export class DecodeResponse { - header: Record; - payload: Record; - signature: string; - isValid: boolean; - error: string; - - static createFrom(source: any = {}) { - return new DecodeResponse(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.header = source["header"]; - this.payload = source["payload"]; - this.signature = source["signature"]; - this.isValid = source["isValid"]; - this.error = source["error"]; - } - } - export class EncodeResponse { - token: string; - error: string; - - static createFrom(source: any = {}) { - return new EncodeResponse(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.token = source["token"]; - this.error = source["error"]; - } - } - export class VerifyResponse { - isValid: boolean; - validationMessage: string; - error: string; - - static createFrom(source: any = {}) { - return new VerifyResponse(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.isValid = source["isValid"]; - this.validationMessage = source["validationMessage"]; - this.error = source["error"]; - } - } - -} - -export namespace wails { - - export class GenerateBarcodeRequest { - content: string; - standard: string; - size: number; - level: string; - format: string; - - static createFrom(source: any = {}) { - return new GenerateBarcodeRequest(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.content = source["content"]; - this.standard = source["standard"]; - this.size = source["size"]; - this.level = source["level"]; - this.format = source["format"]; - } - } - export class GenerateBarcodeResponse { - dataUrl: string; - error: string; - - static createFrom(source: any = {}) { - return new GenerateBarcodeResponse(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.dataUrl = source["dataUrl"]; - this.error = source["error"]; - } - } - -} - diff --git a/wailsjs/go/wails/BarcodeService.d.ts b/wailsjs/go/wails/BarcodeService.d.ts deleted file mode 100755 index 130f5df..0000000 --- a/wailsjs/go/wails/BarcodeService.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT -import {wails} from '../models'; -import {context} from '../models'; - -export function GenerateBarcode(arg1:wails.GenerateBarcodeRequest):Promise; - -export function GetBarcodeSizes():Promise>>; - -export function GetBarcodeStandards():Promise>>; - -export function GetQRErrorLevels():Promise>>; - -export function Startup(arg1:context.Context):Promise; - -export function ValidateContent(arg1:string,arg2:string):Promise>; diff --git a/wailsjs/go/wails/BarcodeService.js b/wailsjs/go/wails/BarcodeService.js deleted file mode 100755 index b13f4d2..0000000 --- a/wailsjs/go/wails/BarcodeService.js +++ /dev/null @@ -1,27 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -export function GenerateBarcode(arg1) { - return window['go']['wails']['BarcodeService']['GenerateBarcode'](arg1); -} - -export function GetBarcodeSizes() { - return window['go']['wails']['BarcodeService']['GetBarcodeSizes'](); -} - -export function GetBarcodeStandards() { - return window['go']['wails']['BarcodeService']['GetBarcodeStandards'](); -} - -export function GetQRErrorLevels() { - return window['go']['wails']['BarcodeService']['GetQRErrorLevels'](); -} - -export function Startup(arg1) { - return window['go']['wails']['BarcodeService']['Startup'](arg1); -} - -export function ValidateContent(arg1, arg2) { - return window['go']['wails']['BarcodeService']['ValidateContent'](arg1, arg2); -} diff --git a/wailsjs/go/wails/CodeFormatterService.d.ts b/wailsjs/go/wails/CodeFormatterService.d.ts deleted file mode 100755 index 132b94e..0000000 --- a/wailsjs/go/wails/CodeFormatterService.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT -import {codeformatter} from '../models'; -import {context} from '../models'; - -export function Format(arg1:codeformatter.FormatRequest):Promise; - -export function Startup(arg1:context.Context):Promise; diff --git a/wailsjs/go/wails/CodeFormatterService.js b/wailsjs/go/wails/CodeFormatterService.js deleted file mode 100755 index 3a10606..0000000 --- a/wailsjs/go/wails/CodeFormatterService.js +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -export function Format(arg1) { - return window['go']['wails']['CodeFormatterService']['Format'](arg1); -} - -export function Startup(arg1) { - return window['go']['wails']['CodeFormatterService']['Startup'](arg1); -} diff --git a/wailsjs/go/wails/ConversionService.d.ts b/wailsjs/go/wails/ConversionService.d.ts deleted file mode 100755 index 087279d..0000000 --- a/wailsjs/go/wails/ConversionService.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT -import {context} from '../models'; - -export function Convert(arg1:string,arg2:string,arg3:string,arg4:Record):Promise; - -export function Startup(arg1:context.Context):Promise; diff --git a/wailsjs/go/wails/ConversionService.js b/wailsjs/go/wails/ConversionService.js deleted file mode 100755 index b9ba7a0..0000000 --- a/wailsjs/go/wails/ConversionService.js +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -export function Convert(arg1, arg2, arg3, arg4) { - return window['go']['wails']['ConversionService']['Convert'](arg1, arg2, arg3, arg4); -} - -export function Startup(arg1) { - return window['go']['wails']['ConversionService']['Startup'](arg1); -} diff --git a/wailsjs/go/wails/DataGeneratorService.d.ts b/wailsjs/go/wails/DataGeneratorService.d.ts deleted file mode 100755 index 949e444..0000000 --- a/wailsjs/go/wails/DataGeneratorService.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT -import {datagenerator} from '../models'; -import {context} from '../models'; - -export function Generate(arg1:datagenerator.GenerateRequest):Promise; - -export function GetPresets():Promise; - -export function Startup(arg1:context.Context):Promise; - -export function ValidateTemplate(arg1:string):Promise; diff --git a/wailsjs/go/wails/DataGeneratorService.js b/wailsjs/go/wails/DataGeneratorService.js deleted file mode 100755 index f0324b3..0000000 --- a/wailsjs/go/wails/DataGeneratorService.js +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -export function Generate(arg1) { - return window['go']['wails']['DataGeneratorService']['Generate'](arg1); -} - -export function GetPresets() { - return window['go']['wails']['DataGeneratorService']['GetPresets'](); -} - -export function Startup(arg1) { - return window['go']['wails']['DataGeneratorService']['Startup'](arg1); -} - -export function ValidateTemplate(arg1) { - return window['go']['wails']['DataGeneratorService']['ValidateTemplate'](arg1); -} diff --git a/wailsjs/go/wails/JWTService.d.ts b/wailsjs/go/wails/JWTService.d.ts deleted file mode 100755 index 061901c..0000000 --- a/wailsjs/go/wails/JWTService.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT -import {jwt} from '../models'; -import {context} from '../models'; - -export function Decode(arg1:string):Promise; - -export function Encode(arg1:string,arg2:string,arg3:string,arg4:string):Promise; - -export function Startup(arg1:context.Context):Promise; - -export function Verify(arg1:string,arg2:string,arg3:string):Promise; diff --git a/wailsjs/go/wails/JWTService.js b/wailsjs/go/wails/JWTService.js deleted file mode 100755 index 7b05874..0000000 --- a/wailsjs/go/wails/JWTService.js +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-check -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -export function Decode(arg1) { - return window['go']['wails']['JWTService']['Decode'](arg1); -} - -export function Encode(arg1, arg2, arg3, arg4) { - return window['go']['wails']['JWTService']['Encode'](arg1, arg2, arg3, arg4); -} - -export function Startup(arg1) { - return window['go']['wails']['JWTService']['Startup'](arg1); -} - -export function Verify(arg1, arg2, arg3) { - return window['go']['wails']['JWTService']['Verify'](arg1, arg2, arg3); -} diff --git a/wailsjs/runtime/package.json b/wailsjs/runtime/package.json deleted file mode 100644 index 1e7c8a5..0000000 --- a/wailsjs/runtime/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "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/wailsjs/runtime/runtime.d.ts b/wailsjs/runtime/runtime.d.ts deleted file mode 100644 index 4445dac..0000000 --- a/wailsjs/runtime/runtime.d.ts +++ /dev/null @@ -1,249 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -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/wailsjs/runtime/runtime.js b/wailsjs/runtime/runtime.js deleted file mode 100644 index 7cb89d7..0000000 --- a/wailsjs/runtime/runtime.js +++ /dev/null @@ -1,242 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -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