From cd7f1ec51aee525f103ef15410ccc6aeb06ad992 Mon Sep 17 00:00:00 2001 From: Vuong <3168632+vuon9@users.noreply.github.com> Date: Mon, 25 May 2026 09:46:03 +0700 Subject: [PATCH] Add signed macOS release pipeline --- .github/workflows/ci.yml | 6 +- .github/workflows/release.yml | 181 +++++++++++++++++++--------------- build/darwin/Info.plist | 12 +-- docs/BUILD.md | 28 +++--- docs/MACOS_RELEASE.md | 47 +++++++++ frontend/src/test/setup.js | 14 +++ main.go | 114 ++++++++------------- service/settings.go | 28 ++++-- service/window.go | 32 ++++++ 9 files changed, 278 insertions(+), 184 deletions(-) create mode 100644 docs/MACOS_RELEASE.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c14bac..234cb26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - name: Cache APT packages uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: libgtk-4-dev libwebkitgtk-6.0-dev + packages: libgtk-3-dev libwebkit2gtk-4.1-dev version: 1.0 execute_install_scripts: false @@ -133,7 +133,7 @@ jobs: - name: Install system deps (Wails CGO) run: | sudo apt-get update - sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev - name: Run govulncheck run: | @@ -162,7 +162,7 @@ jobs: - name: Cache APT packages uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: libgtk-4-dev libwebkitgtk-6.0-dev + packages: libgtk-3-dev libwebkit2gtk-4.1-dev version: 1.0 execute_install_scripts: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71de16d..dc7c278 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,113 +73,135 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev - - - name: Install Task - run: | - go install github.com/go-task/task/v3/cmd/task@latest - echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - shell: bash + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev - name: Install Frontend Dependencies run: | cd frontend && bun install shell: bash - - name: Install Wails CLI + - name: Check macOS signing inputs + id: macos_signing + if: matrix.os == 'macos-latest' + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | - go install github.com/wailsapp/wails/v3/cmd/wails3@latest - GOPATH=$(go env GOPATH) - # Create 'wails' alias for 'wails3' for future compatibility - if [ "${{ matrix.os }}" = "windows-latest" ]; then - cp "$GOPATH/bin/wails3.exe" "$GOPATH/bin/wails.exe" - echo "$GOPATH/bin" >> $GITHUB_PATH - elif [ "${{ matrix.os }}" = "macos-latest" ]; then - ln -sf "$GOPATH/bin/wails3" "$GOPATH/bin/wails" - echo "$GOPATH/bin" >> $GITHUB_PATH - else - sudo ln -sf "$GOPATH/bin/wails3" /usr/local/bin/wails + missing=0 + for name in MACOS_CERTIFICATE MACOS_CERTIFICATE_PASSWORD MACOS_SIGN_IDENTITY APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do + if [ -z "${!name}" ]; then + echo "::error::$name is required for signed and notarized macOS releases" + missing=1 + fi + done + + if [ "$missing" -ne 0 ]; then + exit 1 fi + + echo "available=true" >> "$GITHUB_OUTPUT" shell: bash + - name: Import macOS Developer ID certificate + if: matrix.os == 'macos-latest' && steps.macos_signing.outputs.available == 'true' + uses: apple-actions/import-codesign-certs@v2 + with: + p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }} + p12-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + keychain: build + keychain-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + - name: Build Application run: | - # Build for current platform (native runner) + cd frontend + bun run build + cd .. + + mkdir -p bin + if [ "${{ matrix.os }}" = "macos-latest" ]; then - # macOS: Build with code signing if certificates are available - if [ -n "$MACOS_CERTIFICATE" ] && [ -n "$MACOS_CERTIFICATE_PASSWORD" ]; then - echo "Building with code signing..." - wails build -sign -signIdentity "Developer ID Application" - else - echo "Building without code signing (no certificates found)..." - wails build + export GOOS=darwin + export CGO_ENABLED=1 + export CGO_CFLAGS="-mmacosx-version-min=10.15" + export CGO_LDFLAGS="-mmacosx-version-min=10.15" + export MACOSX_DEPLOYMENT_TARGET="10.15" + + GOARCH=amd64 go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o bin/DevToolbox-amd64 . + GOARCH=arm64 go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o bin/DevToolbox-arm64 . + lipo -create -output bin/DevToolbox bin/DevToolbox-amd64 bin/DevToolbox-arm64 + rm bin/DevToolbox-amd64 bin/DevToolbox-arm64 + + mkdir -p "bin/DevToolbox.app/Contents/MacOS" + mkdir -p "bin/DevToolbox.app/Contents/Resources" + cp "bin/DevToolbox" "bin/DevToolbox.app/Contents/MacOS/" + cp "build/darwin/Info.plist" "bin/DevToolbox.app/Contents/" + cp "build/darwin/icons.icns" "bin/DevToolbox.app/Contents/Resources/" + if [ -f "build/darwin/Assets.car" ]; then + cp "build/darwin/Assets.car" "bin/DevToolbox.app/Contents/Resources/" fi else - wails build + if [ "${{ matrix.os }}" = "windows-latest" ]; then + go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o bin/DevToolbox.exe . + else + go build -tags production -trimpath -buildvcs=false -ldflags="-w -s" -o bin/DevToolbox . + fi fi shell: bash - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - # Import macOS certificates for signing (optional - only if secrets are configured) - - name: Import macOS Certificates - if: matrix.os == 'macos-latest' && github.event_name != 'pull_request' && env.HAS_CERTS == 'true' - uses: apple-actions/import-codesign-certs@v2 - with: - p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }} - p12-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - keychain: build - keychain-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + - name: Sign macOS app + if: matrix.os == 'macos-latest' && steps.macos_signing.outputs.available == 'true' env: - HAS_CERTS: ${{ secrets.MACOS_CERTIFICATE != '' && secrets.MACOS_CERTIFICATE_PASSWORD != '' }} + MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} + run: | + APP_BUNDLE="bin/DevToolbox.app" + test -d "$APP_BUNDLE" + + codesign \ + --force \ + --deep \ + --options runtime \ + --timestamp \ + --sign "$MACOS_SIGN_IDENTITY" \ + "$APP_BUNDLE" + + codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE" + shell: bash - # Notarize macOS app (optional - only if secrets are configured and .app bundle exists) - name: Notarize macOS App - if: matrix.os == 'macos-latest' && github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v') && env.HAS_NOTARIZE_SECRETS == 'true' + if: matrix.os == 'macos-latest' && steps.macos_signing.outputs.available == 'true' run: | - BINARY_NAME=$(ls bin/ | grep -i "devtoolbox" | head -1) - if [ -d "bin/$BINARY_NAME.app" ]; then - # Create a zip for notarization - ditto -c -k --keepParent "bin/$BINARY_NAME.app" "bin/devtoolbox.zip" - - # Submit for notarization - xcrun notarytool submit "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 "bin/$BINARY_NAME.app" - else - echo "No .app bundle found, skipping notarization (binary-only build)" - fi + APP_BUNDLE="bin/DevToolbox.app" + NOTARY_ZIP="bin/DevToolbox-notary.zip" + + ditto -c -k --keepParent "$APP_BUNDLE" "$NOTARY_ZIP" + + xcrun notarytool submit "$NOTARY_ZIP" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + + xcrun stapler staple "$APP_BUNDLE" + xcrun stapler validate "$APP_BUNDLE" + spctl --assess --type execute --verbose=4 "$APP_BUNDLE" env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - HAS_NOTARIZE_SECRETS: ${{ secrets.APPLE_ID != '' && secrets.APPLE_APP_SPECIFIC_PASSWORD != '' && secrets.APPLE_TEAM_ID != '' }} + shell: bash # Package and upload build artifacts - name: Package Artifacts run: | mkdir -p release - # Discover the actual binary name (DevToolbox, devtoolbox, etc.) - BINARY_NAME=$(ls bin/ | grep -i "devtoolbox" | head -1) - echo "Found binary: $BINARY_NAME" - - if [ -z "$BINARY_NAME" ]; then - echo "ERROR: Could not find devtoolbox binary in bin/" - ls -la bin/ - exit 1 - fi - - # Check if it's an .app bundle (macOS) or just a binary - if [ -d "bin/$BINARY_NAME.app" ]; then - # macOS with .app bundle - echo "Found .app bundle, creating DMG..." + if [ "${{ matrix.os }}" = "macos-latest" ]; then + APP_BUNDLE="bin/DevToolbox.app" + test -d "$APP_BUNDLE" brew install create-dmg create-dmg \ --volname "DevToolbox" \ @@ -188,12 +210,15 @@ jobs: --icon-size 100 \ --app-drop-link 600 185 \ "release/DevToolbox-${{ matrix.build }}.dmg" \ - "bin/$BINARY_NAME.app" + "$APP_BUNDLE" + hdiutil verify "release/DevToolbox-${{ matrix.build }}.dmg" elif [ "${{ matrix.os }}" = "windows-latest" ]; then - # Windows: copy .exe + BINARY_NAME=$(ls bin/ | grep -i "devtoolbox.*\.exe$" | head -1) + test -n "$BINARY_NAME" cp "bin/$BINARY_NAME" "release/DevToolbox-${{ matrix.build }}.exe" else - # Linux or macOS binary (no .app): create tar.gz + BINARY_NAME=$(ls bin/ | grep -i "devtoolbox" | head -1) + test -n "$BINARY_NAME" tar -czf "release/DevToolbox-${{ matrix.build }}.tar.gz" -C bin "$BINARY_NAME" fi diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist index 1e0aef6..f6f0a25 100644 --- a/build/darwin/Info.plist +++ b/build/darwin/Info.plist @@ -4,15 +4,15 @@ CFBundlePackageType APPL CFBundleName - My Product + DevToolbox CFBundleExecutable - devtoolbox + DevToolbox CFBundleIdentifier - com.example.devtoolbox + com.vuon9.devtoolbox CFBundleVersion 0.1.0 CFBundleGetInfoString - This is a comment + DevToolbox is a set of useful tools for daily development. CFBundleShortVersionString 0.1.0 CFBundleIconFile @@ -24,6 +24,6 @@ NSHighResolutionCapable true NSHumanReadableCopyright - © 2026, My Company + (c) 2026, Vuong - \ No newline at end of file + diff --git a/docs/BUILD.md b/docs/BUILD.md index f6844b9..f9e2c21 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -4,7 +4,6 @@ - Go 1.25+ - Bun 1.0+ -- Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest` ## Quick Build @@ -13,11 +12,10 @@ git clone https://github.com/vuon9/devtoolbox.git cd devtoolbox -# Build -wails build - -# Or run in development mode -wails dev +# Build for the current platform +cd frontend && bun install && bun run build +cd .. +go build -o bin/DevToolbox . ``` ## Development @@ -30,15 +28,18 @@ cd frontend && bun dev go run . # Both (separate terminals) -wails dev # Terminal 1 -cd frontend && bun dev # Terminal 2 +go run . # Terminal 1 +cd frontend && bun dev # Terminal 2 ``` ## Output -Built binaries are in `build/bin/`: -- `devtoolbox` (Linux/macOS) -- `devtoolbox.exe` (Windows) +Built binaries and app bundles are in `bin/`: +- `DevToolbox` (Linux/macOS binary) +- `DevToolbox.exe` (Windows) +- `DevToolbox.app` (macOS package) + +For signed and notarized macOS releases, see [MACOS_RELEASE.md](./MACOS_RELEASE.md). ## Troubleshooting @@ -46,8 +47,3 @@ Built binaries are in `build/bin/`: ```bash cd frontend && bun install ``` - -**Wails not found:** -```bash -go install github.com/wailsapp/wails/v2/cmd/wails@latest -``` diff --git a/docs/MACOS_RELEASE.md b/docs/MACOS_RELEASE.md new file mode 100644 index 0000000..29fd68e --- /dev/null +++ b/docs/MACOS_RELEASE.md @@ -0,0 +1,47 @@ +# macOS Signed Release + +This project ships macOS releases as a signed, notarized, and stapled +`DevToolbox-macos.dmg` from `.github/workflows/release.yml`. + +## Required GitHub Secrets + +Configure these repository secrets before running a release: + +- `MACOS_CERTIFICATE`: base64 encoded Developer ID Application `.p12` +- `MACOS_CERTIFICATE_PASSWORD`: password for the `.p12` +- `MACOS_SIGN_IDENTITY`: full Developer ID Application identity name +- `APPLE_ID`: Apple ID used for notarization +- `APPLE_APP_SPECIFIC_PASSWORD`: app-specific password for the Apple ID +- `APPLE_TEAM_ID`: Apple Developer Team ID + +The release workflow fails early on the macOS job if any of these secrets are +missing. Unsigned macOS release artifacts are not uploaded by the release job. + +## What the Workflow Does + +On macOS runners, the release job: + +1. Builds a universal `DevToolbox.app`. +2. Imports the Developer ID Application certificate into a temporary keychain. +3. Signs the app with hardened runtime and timestamping. +4. Verifies the signature with `codesign --verify`. +5. Submits the app to Apple notarization and waits for completion. +6. Staples and validates the notarization ticket. +7. Runs `spctl --assess --type execute`. +8. Packages the stapled app into `DevToolbox-macos.dmg`. +9. Verifies the DMG with `hdiutil verify`. + +Mini owns certificate setup, notarization credentials, and final local Gatekeeper +verification for the released artifact. + +## Local macOS Verification + +After downloading the release DMG on macOS: + +```bash +hdiutil verify DevToolbox-macos.dmg +hdiutil attach DevToolbox-macos.dmg +spctl --assess --type execute --verbose=4 /Volumes/DevToolbox/DevToolbox.app +codesign --verify --deep --strict --verbose=2 /Volumes/DevToolbox/DevToolbox.app +open /Volumes/DevToolbox/DevToolbox.app +``` diff --git a/frontend/src/test/setup.js b/frontend/src/test/setup.js index bcb2601..2e388dd 100644 --- a/frontend/src/test/setup.js +++ b/frontend/src/test/setup.js @@ -5,6 +5,20 @@ import * as matchers from '@testing-library/jest-dom/matchers'; // Extend Vitest's expect with jest-dom matchers expect.extend(matchers); +if (typeof window !== 'undefined' && !window.localStorage) { + const store = new Map(); + + Object.defineProperty(window, 'localStorage', { + configurable: true, + value: { + clear: () => store.clear(), + getItem: (key) => (store.has(String(key)) ? store.get(String(key)) : null), + removeItem: (key) => store.delete(String(key)), + setItem: (key, value) => store.set(String(key), String(value)), + }, + }); +} + // Cleanup after each test afterEach(() => { cleanup(); diff --git a/main.go b/main.go index 699c948..430abba 100644 --- a/main.go +++ b/main.go @@ -23,26 +23,6 @@ import ( //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") - - // Register event for command palette - emit empty string as data - application.RegisterEvent[string]("command-palette:open") - application.RegisterEvent[string]("window:toggle") - application.RegisterEvent[string]("app:quit") - - // Register settings changed event - application.RegisterEvent[map[string]interface{}]("settings:changed") - - // Register spotlight events - application.RegisterEvent[string]("spotlight:closed") - application.RegisterEvent[string]("spotlight:close") - application.RegisterEvent[string]("spotlight:command-selected") // Event triggered when user selects a command from spotlight - used for navigation from spotlight to main window -} - func main() { serverOnly := flag.Bool("server-only", false, "Run in server-only mode (no GUI)") port := flag.Int("port", 8081, "HTTP server port") @@ -71,12 +51,45 @@ func main() { }) }) + // Initialize settings manager + var configDir string + if runtime.GOOS == "darwin" { + configDir = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "DevToolbox") + } else if runtime.GOOS == "windows" { + configDir = filepath.Join(os.Getenv("APPDATA"), "DevToolbox") + } else { + configDir = filepath.Join(os.Getenv("HOME"), ".config", "devtoolbox") + } + + settingsManager := settings.NewManager(configDir) + if err := settingsManager.Load(); err != nil { + log.Printf("Failed to load settings: %v", err) + } + + settingsService := service.NewSettingsService(nil, settingsManager) + spotlightService := service.NewSpotlightService(nil) + windowControls := service.NewWindowControls(nil) + // Create application with options app := application.New(application.Options{ Name: "DevToolbox", Description: "Set of tools for daily development", Services: []application.Service{ application.NewService(&GreetService{}), + application.NewService(service.NewJWTService(nil)), + application.NewService(service.NewDateTimeService(nil)), + application.NewService(service.NewEncrypterService(nil)), + application.NewService(service.NewEncoderService(nil)), + application.NewService(service.NewHashGeneratorService(nil)), + application.NewService(service.NewCodeConverterService(nil)), + application.NewService(service.NewTextUtilitiesService(nil)), + application.NewService(service.NewBarcodeService(nil)), + application.NewService(service.NewDataGeneratorService(nil)), + application.NewService(service.NewCodeFormatterService(nil)), + application.NewService(service.NewNumberConverterService(nil)), + application.NewService(settingsService), + application.NewService(spotlightService), + application.NewService(windowControls), }, Mac: application.MacOptions{ ApplicationShouldTerminateAfterLastWindowClosed: false, @@ -88,40 +101,7 @@ func main() { }, }) - // Initialize settings manager - var configDir string - if runtime.GOOS == "darwin" { - configDir = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "DevToolbox") - } else if runtime.GOOS == "windows" { - configDir = filepath.Join(os.Getenv("APPDATA"), "DevToolbox") - } else { - configDir = filepath.Join(os.Getenv("HOME"), ".config", "devtoolbox") - } - - settingsManager := settings.NewManager(configDir) - if err := settingsManager.Load(); err != nil { - log.Printf("Failed to load settings: %v", err) - } - - // Register app services - app.RegisterService(application.NewService(service.NewJWTService(app))) - app.RegisterService(application.NewService(service.NewDateTimeService(app))) - app.RegisterService(application.NewService(service.NewEncrypterService(app))) - app.RegisterService(application.NewService(service.NewEncoderService(app))) - app.RegisterService(application.NewService(service.NewHashGeneratorService(app))) - app.RegisterService(application.NewService(service.NewCodeConverterService(app))) - app.RegisterService(application.NewService(service.NewTextUtilitiesService(app))) - app.RegisterService(application.NewService(service.NewBarcodeService(app))) - app.RegisterService(application.NewService(service.NewDataGeneratorService(app))) - app.RegisterService(application.NewService(service.NewCodeFormatterService(app))) - app.RegisterService(application.NewService(service.NewNumberConverterService(app))) - app.RegisterService(application.NewService(service.NewSettingsService(app, settingsManager))) - - // Create and register spotlight service - spotlightService := service.NewSpotlightService(app) - app.RegisterService(application.NewService(spotlightService)) - - // WindowControls service must be registered after main window creation (see line 149) + settingsService.SetApp(app) // Start HTTP server for browser support (background) go func() { @@ -129,7 +109,7 @@ func main() { }() // Create main window - mainWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ + mainWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ Name: "main", Title: "DevToolbox", Width: 1024, @@ -166,13 +146,12 @@ func main() { } }) - // Register WindowControls service after window creation - app.RegisterService(application.NewService(service.NewWindowControls(mainWindow))) + windowControls.SetWindow(mainWindow) // Create spotlight window with special behaviors // Note: MacWindowLevelFloating and ActivationPolicyAccessory may require // platform-specific code. CollectionBehaviors provide most spotlight functionality. - spotlightWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ + spotlightWindow := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ Title: "Spotlight", Width: 640, Height: 384, @@ -186,11 +165,6 @@ func main() { // Prevent resizing DisableResize: true, Mac: application.MacWindow{ - // Combine multiple behaviors using bitwise OR: - // - CanJoinAllSpaces: window appears on ALL Spaces (virtual desktops) - // - FullScreenAuxiliary: window can overlay fullscreen applications - CollectionBehavior: application.MacWindowCollectionBehaviorCanJoinAllSpaces | - application.MacWindowCollectionBehaviorFullScreenAuxiliary, // Float above other windows WindowLevel: application.MacWindowLevelFloating, // Hidden title bar for clean look @@ -213,7 +187,7 @@ func main() { }) // Listen for spotlight navigation events - app.Event.On("spotlight:command-selected", func(event *application.CustomEvent) { + app.OnEvent("spotlight:command-selected", func(event *application.CustomEvent) { log.Printf("[Spotlight] Received command-selected event with data: %#v", event.Data) var path string @@ -255,18 +229,18 @@ func main() { }) // Close spotlight window - app.Event.On("spotlight:close", func(_ *application.CustomEvent) { + app.OnEvent("spotlight:close", func(_ *application.CustomEvent) { log.Printf("[Spotlight] Spotlight close requested") spotlightWindow.Hide() }) // Proxy these events to the main window - app.Event.On("spotlight:theme:toggle", func(_ *application.CustomEvent) { + app.OnEvent("spotlight:theme:toggle", func(_ *application.CustomEvent) { log.Printf("[Spotlight] Relaying theme:toggle to main window") mainWindow.EmitEvent("theme:toggle", nil) }) - app.Event.On("window:toggle", func(_ *application.CustomEvent) { + app.OnEvent("window:toggle", func(_ *application.CustomEvent) { log.Printf("[Spotlight] Window toggle requested") if mainWindow.IsVisible() { mainWindow.Hide() @@ -276,13 +250,13 @@ func main() { } }) - app.Event.On("app:quit", func(_ *application.CustomEvent) { + app.OnEvent("app:quit", func(_ *application.CustomEvent) { log.Printf("[Spotlight] App quit requested via spotlight") app.Quit() }) // Setup system tray - systray := app.SystemTray.New() + systray := app.NewSystemTray() // Set system tray icon if runtime.GOOS == "darwin" { @@ -322,8 +296,6 @@ func main() { } } - - func GinMiddleware(ginEngine *gin.Engine) application.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/service/settings.go b/service/settings.go index 35c26e4..0d51fba 100644 --- a/service/settings.go +++ b/service/settings.go @@ -23,6 +23,11 @@ func NewSettingsService(app *application.App, manager *settings.Manager) *Settin } } +// SetApp connects the service to the Wails app after application creation. +func (s *SettingsService) SetApp(app *application.App) { + s.app = app +} + // GetCloseMinimizesToTray returns the current setting func (s *SettingsService) GetCloseMinimizesToTray() bool { return s.manager.GetCloseMinimizesToTray() @@ -35,11 +40,7 @@ func (s *SettingsService) SetCloseMinimizesToTray(value bool) error { return err } - // Emit event to notify frontend that setting changed - s.app.Event.Emit("settings:changed", map[string]interface{}{ - "setting": "closeMinimizesToTray", - "value": value, - }) + s.emitSettingsChanged("closeMinimizesToTray", value) return nil } @@ -53,11 +54,18 @@ func (s *SettingsService) ToggleCloseMinimizesToTray() (bool, error) { value := s.manager.GetCloseMinimizesToTray() - // Emit event to notify frontend - s.app.Event.Emit("settings:changed", map[string]interface{}{ - "setting": "closeMinimizesToTray", - "value": value, - }) + s.emitSettingsChanged("closeMinimizesToTray", value) return value, nil } + +func (s *SettingsService) emitSettingsChanged(setting string, value bool) { + if s.app == nil { + return + } + + s.app.EmitEvent("settings:changed", map[string]interface{}{ + "setting": setting, + "value": value, + }) +} diff --git a/service/window.go b/service/window.go index b2ffe4d..f572914 100644 --- a/service/window.go +++ b/service/window.go @@ -16,13 +16,24 @@ func NewWindowControls(window *application.WebviewWindow) *WindowControls { } } +// SetWindow connects the service to the main window after window creation. +func (wc *WindowControls) SetWindow(window *application.WebviewWindow) { + wc.window = window +} + // Minimise minimises the window func (wc *WindowControls) Minimise() { + if wc.window == nil { + return + } wc.window.Minimise() } // Maximise toggles maximise state func (wc *WindowControls) Maximise() { + if wc.window == nil { + return + } if wc.window.IsMaximised() { wc.window.UnMaximise() } else { @@ -32,35 +43,56 @@ func (wc *WindowControls) Maximise() { // Close closes the window (this will trigger WindowClosing event) func (wc *WindowControls) Close() { + if wc.window == nil { + return + } wc.window.Close() } // Show shows the window func (wc *WindowControls) Show() { + if wc.window == nil { + return + } wc.window.Show() } // Hide hides the window func (wc *WindowControls) Hide() { + if wc.window == nil { + return + } wc.window.Hide() } // IsVisible returns whether the window is visible func (wc *WindowControls) IsVisible() bool { + if wc.window == nil { + return false + } return wc.window.IsVisible() } // IsMinimised returns whether the window is minimised func (wc *WindowControls) IsMinimised() bool { + if wc.window == nil { + return false + } return wc.window.IsMinimised() } // Restore restores the window from minimised/maximised state func (wc *WindowControls) Restore() { + if wc.window == nil { + return + } wc.window.Restore() } // Focus focuses the window func (wc *WindowControls) Focus() { + if wc.window == nil { + return + } wc.window.Focus() }