feat: keep Verstak running in the system tray

This commit is contained in:
2026-07-14 02:54:30 +08:00
parent e33c63d3ad
commit d8a4213569
9 changed files with 354 additions and 2 deletions
+32
View File
@@ -42,6 +42,9 @@ var emitFrontendEvent = runtime.EventsEmit
var initializeNativeNotifications = runtime.InitializeNotifications
var cleanupNativeNotifications = runtime.CleanupNotifications
var sendNativeNotification = runtime.SendNotification
var hideNativeWindow = runtime.WindowHide
var showNativeWindow = runtime.WindowShow
var quitNativeApplication = runtime.Quit
type notificationService interface {
Replace(pluginID string, requests []notifications.Request) error
@@ -96,6 +99,7 @@ type App struct {
activityEvents map[string]bool
browserInboxEvents map[string]bool
browserInboxEnabled atomic.Bool
allowQuit atomic.Bool
}
// SetNotificationService attaches the core-owned plugin notification scheduler.
@@ -217,6 +221,34 @@ func (a *App) Shutdown(ctx context.Context) {
cleanupNativeNotifications(ctx)
}
// BeforeClose hides the primary window until the user explicitly quits from the tray.
func (a *App) BeforeClose(ctx context.Context) bool {
if a.allowQuit.Load() {
return false
}
hideNativeWindow(ctx)
return true
}
// ShowWindow brings the primary window back from the tray.
func (a *App) ShowWindow() {
if a == nil || a.ctx == nil {
return
}
showNativeWindow(a.ctx)
}
// Quit allows the close event and ends the application process.
func (a *App) Quit() {
if a == nil {
return
}
a.allowQuit.Store(true)
if a.ctx != nil {
quitNativeApplication(a.ctx)
}
}
// NativeNotificationSender delivers scheduler items through the Wails runtime.
type NativeNotificationSender struct{}
+49
View File
@@ -306,6 +306,55 @@ func TestNativeNotificationSenderUsesStablePluginScopedID(t *testing.T) {
}
}
func TestBeforeCloseHidesWindowUntilUserChoosesQuit(t *testing.T) {
oldHide := hideNativeWindow
defer func() { hideNativeWindow = oldHide }()
hideCalls := 0
hideNativeWindow = func(context.Context) { hideCalls++ }
app := &App{}
if prevent := app.BeforeClose(context.Background()); !prevent {
t.Fatal("BeforeClose() = false, want true while tray mode is active")
}
if hideCalls != 1 {
t.Fatalf("hide calls = %d, want 1", hideCalls)
}
}
func TestTrayQuitAllowsWindowCloseAndQuitsApplication(t *testing.T) {
oldQuit := quitNativeApplication
defer func() { quitNativeApplication = oldQuit }()
quitCalls := 0
quitNativeApplication = func(context.Context) { quitCalls++ }
app := &App{ctx: context.Background()}
app.Quit()
if quitCalls != 1 {
t.Fatalf("quit calls = %d, want 1", quitCalls)
}
if prevent := app.BeforeClose(context.Background()); prevent {
t.Fatal("BeforeClose() = true after Quit(), want false")
}
}
func TestShowWindowUsesWailsContext(t *testing.T) {
oldShow := showNativeWindow
defer func() { showNativeWindow = oldShow }()
showCalls := 0
showNativeWindow = func(context.Context) { showCalls++ }
app := &App{ctx: context.Background()}
app.ShowWindow()
if showCalls != 1 {
t.Fatalf("show calls = %d, want 1", showCalls)
}
}
// TestGetPluginFrontendInfo_KnownPluginWithFrontend verifies that
// GetPluginFrontendInfo returns correct metadata for a plugin with a frontend.
func TestGetPluginFrontendInfo_KnownPluginWithFrontend(t *testing.T) {