How to build, test, and iterate on notifications for each platform.
| Tool | Install |
|---|---|
| Go | brew install go / winget install GoLang.Go / apt install golang |
| Wails CLI | go install github.com/wailsapp/wails/v2/cmd/wails@latest |
| protoc | Only needed if you edit proto/hermes.proto |
Platform-specific webview requirements:
| Platform | Webview engine | Dev dependency |
|---|---|---|
| Windows | WebView2 | Included with Windows 10+ (Edge) |
| macOS | WKWebView | Included with macOS |
| Linux | WebKitGTK | apt install libwebkit2gtk-4.1-dev |
# macOS
wails build
# Linux (requires webkit2_41 build tag for libwebkit2gtk-4.1)
wails build -tags webkit2_41wails build -windowsconsole
-windowsconsoleis required for CLI output. Without it, Wails produces a GUI-subsystem executable (no attached console).--helpprints nothing, stdout is lost, and$LASTEXITCODEis always 0 in PowerShell.However, for a silent background notification agent (no popping up black console windows), you might prefer omitting
-windowsconsole. In that case, you cannot rely on stdout for the user's choice — you must use the service daemon (gRPC) or file-based IPC if you need the result.
wails build -platform windows/amd64 -windowsconsoleWails has a dev mode that hot-reloads the frontend and rebuilds Go on change:
wails devThis opens a webview that auto-refreshes when you edit HTML/CSS/JS in frontend/. Go changes trigger a rebuild. Useful for iterating on notification layout and styling.
The fastest way to iterate is --local mode — no service required, just renders the UI directly:
# Inline JSON
hermes --local '{"heading":"Test","message":"Quick test."}'
# From a file (edit, save, re-run)
hermes --local testdata/restart-notification.json
# Pipe from stdin
echo '{"heading":"Test","message":"Piped."}' | hermes --localStart the service in one terminal, send notifications from another:
# Terminal 1: start service
hermes serve
# Terminal 2: send notifications
hermes notify testdata/restart-notification.json
hermes list
hermes cancel <id>Build and copy the binary to a Windows machine (or use cross-compile):
# Quick test
& .\hermes.exe --local '{"heading":"Test","message":"Windows test."}'
# From file
& .\hermes.exe --local .\testdata\restart-notification.json
# Pipe via stdin (recommended for complex JSON)
$config = @'
{
"heading": "System Restart Required",
"message": "Your computer needs to restart.",
"timeout": 300,
"timeout_value": "restart",
"buttons": [
{"label": "Defer 1h", "value": "defer_1h", "style": "secondary"},
{"label": "Restart Now", "value": "restart", "style": "primary"}
]
}
'@
$config | & .\hermes.exe --local
# Check exit code
Write-Host "Exit: $LASTEXITCODE"WSL Tip: You can run the Windows binary directly from WSL if you copy it to a Windows path (e.g.
C:\Temp).# Build (cross-compile) wails build -platform windows/amd64 -windowsconsole # Copy to Windows temp cp build/bin/hermes.exe /mnt/c/Temp/hermes.exe # Run via powershell.exe powershell.exe -Command "& 'C:\Temp\hermes.exe' --local '{\"heading\":\"WSL Test\",\"message\":\"Hello from WSL\"}'"
PowerShell 5.1 strips inner double quotes when passing strings to native executables. Always pipe via stdin for complex JSON.
# Build natively
wails build
# Test
./build/bin/hermes --local testdata/restart-notification.json
# Test with service
./build/bin/hermes serve &
./build/bin/hermes notify testdata/restart-notification.jsonNotifications appear in the top-right corner (matching macOS notification behavior).
Requires a display server (X11 or Wayland with XWayland):
# Build
wails build
# Test (needs DISPLAY set)
./build/bin/hermes --local testdata/restart-notification.json
# On Wayland, hermes auto-sets GDK_BACKEND=x11 for window positioningNotifications appear in the top-right corner.
WSL doesn't have a display server by default. Options:
- WSLg (Windows 11) — GUI apps work out of the box. Just run
hermes --local .... - VcXsrv / X410 — Install an X server on Windows, set
export DISPLAY=:0. - Service-only testing — Skip the UI, test the gRPC lifecycle:
hermes serve &
hermes notify '{"heading":"Test","message":"WSL test."}' &
hermes list
hermes cancel <id>Use the bundled templates in testdata/ as starting points:
| File | Scenario |
|---|---|
restart-notification.json |
Restart with defer dropdown |
update-notification.json |
Software update with defer |
simple-notification.json |
One-button acknowledgment |
defer-with-dropdown.json |
VPN disconnect with defer menu |
short-defer-restart.json |
Short deferral (2m deadline, 3 max) for quick lifecycle testing |
short-defer-deadline.json |
Very short deadline (1m) for testing auto-action |
image-carousel.json |
Multi-slide image carousel |
install-with-watch.json |
Filesystem watch for install receipt validation |
escalation-restart.json |
Escalation ladder: soft → firm → mandatory after repeated deferrals |
action-chaining.json |
Result actions: user response triggers automatic follow-up |
quiet-hours.json |
Time-based delivery suppression (22:00–07:00) |
localized-restart.json |
Multi-language restart (ja, de, es, fr, ko, zh) |
priority-critical.json |
Priority 10 critical alert (ignores DND, no defer) |
workflow-step1-eula.json |
Dependency chain step 1: accept EULA |
workflow-step2-update.json |
Dependency chain step 2: install update (waits for EULA) |
Edit a template, run it, tweak, repeat:
# Edit
vim testdata/restart-notification.json
# Test
hermes --local testdata/restart-notification.json
# Check what the user chose
echo "User chose: $(hermes --local testdata/restart-notification.json)"
echo "Exit code: $?"Deferrals require the service daemon. State is persisted to a local bbolt database, so notifications survive service restarts.
hermes serve &
# Notification with 5-minute deadline and max 2 defers
hermes notify '{
"heading": "Restart Required",
"message": "Restart to apply updates.",
"defer_deadline": "5m",
"max_defers": 2,
"timeout_value": "restart",
"buttons": [
{"label": "Defer 1m", "value": "defer_1m", "style": "secondary"},
{"label": "Restart", "value": "restart", "style": "primary"}
]
}'
# Watch the notification lifecycle
watch hermes listAfter 2 defers or 5 minutes (whichever comes first), the notification auto-actions with timeout_value.
# 1. Start service, send a deferrable notification, defer it
hermes serve &
hermes notify '{"heading":"Persist Test","message":"Defer me.","defer_deadline":"1h","buttons":[{"label":"Defer 5m","value":"defer_5m","style":"secondary"},{"label":"OK","value":"ok","style":"primary"}]}' &
# Click "Defer 5m" in the UI
# 2. Kill the service
kill %1
# 3. Restart — the deferred notification reappears immediately
hermes serveOverride the database path with --db for isolated testing:
hermes serve --db /tmp/hermes-test.db# All internal package tests (no display server needed)
# On Linux, add -tags webkit2_41 for WebKitGTK 4.1 compatibility
go test -race -tags webkit2_41 ./internal/...
# Specific package
go test -race -tags webkit2_41 ./internal/manager/ # includes persistence tests
go test -race -tags webkit2_41 ./internal/store/ # bbolt store tests
go test -race -tags webkit2_41 ./internal/config/
go test -race -tags webkit2_41 ./internal/server/
# Vet
go vet -tags webkit2_41 ./...Note: The
-tags webkit2_41flag is only required on Linux (Ubuntu 24.04+) wherelibwebkit2gtk-4.1-devreplaces the older4.0package. On macOS and Windows, the flag is harmless but unnecessary.
Only needed if you edit proto/hermes.proto:
protoc --go_out=. --go-grpc_out=. proto/hermes.protoRequires protoc-gen-go and protoc-gen-go-grpc:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestThe frontend lives in frontend/ — plain HTML/CSS/JS, no build step:
| File | Purpose |
|---|---|
index.html |
Notification layout |
style.css |
Dark theme, CSS custom properties (--accent) |
main.js |
Countdown timer, button handling, Wails bindings |
The accent color is set dynamically from the JSON config's accent_color field. To preview different themes:
hermes --local '{"heading":"Blue","message":"Test","accent_color":"#0078D4"}'
hermes --local '{"heading":"Red","message":"Test","accent_color":"#E74C3C"}'
hermes --local '{"heading":"Green","message":"Test","accent_color":"#27AE60"}'