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()
}