diff --git a/pkg/systray/service.go b/pkg/systray/service.go index b0989cf9..3176dd33 100644 --- a/pkg/systray/service.go +++ b/pkg/systray/service.go @@ -4,6 +4,8 @@ import ( "context" core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" + "forge.lthn.ai/core/gui/pkg/notification" ) type Options struct{} @@ -41,7 +43,23 @@ func (s *Service) OnStartup(_ context.Context) core.Result { }) s.Core().Action("systray.showMessage", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskShowMessage) - return core.Result{Value: nil, OK: true}.New(s.manager.ShowMessage(t.Title, t.Message)) + if err := s.manager.ShowMessage(t.Title, t.Message); err == nil { + return core.Result{OK: true} + } else { + fallback := s.Core().Action("notification.send").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: notification.TaskSend{Options: notification.NotificationOptions{ + Title: t.Title, + Message: t.Message, + }}}, + )) + if fallback.OK { + return core.Result{OK: true} + } + if fallbackErr, ok := fallback.Value.(error); ok { + return core.Result{Value: coreerr.E("systray.showMessage", "tray message failed and notification fallback failed", fallbackErr), OK: false} + } + return core.Result{Value: err, OK: false} + } }) s.Core().Action("systray.showPanel", func(_ context.Context, _ core.Options) core.Result { // Panel show — deferred (requires WindowHandle integration) diff --git a/pkg/systray/service_test.go b/pkg/systray/service_test.go index e54517ca..b0f1e4b4 100644 --- a/pkg/systray/service_test.go +++ b/pkg/systray/service_test.go @@ -5,6 +5,7 @@ import ( "testing" core "dappco.re/go/core" + "forge.lthn.ai/core/gui/pkg/notification" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -87,6 +88,51 @@ func TestTaskShowMessage_Good(t *testing.T) { assert.Equal(t, "Up", mockTray.lastMessageBody) } +type fallbackNotificationPlatform struct { + sent bool + opts notification.NotificationOptions +} + +func (m *fallbackNotificationPlatform) Send(opts notification.NotificationOptions) error { + m.sent = true + m.opts = opts + return nil +} +func (m *fallbackNotificationPlatform) RequestPermission() (bool, error) { return true, nil } +func (m *fallbackNotificationPlatform) CheckPermission() (bool, error) { return true, nil } +func (m *fallbackNotificationPlatform) RevokePermission() error { return nil } +func (m *fallbackNotificationPlatform) Clear(id string) error { return nil } + +type failingTrayPlatform struct{} + +func (failingTrayPlatform) NewTray() PlatformTray { return &failingTray{} } +func (failingTrayPlatform) NewMenu() PlatformMenu { return &mockTrayMenu{} } + +type failingTray struct{ mockTray } + +func (t *failingTray) ShowMessage(title, message string) error { + return core.NewError("tray balloon unavailable") +} + +func TestTaskShowMessage_FallbackToNotification_Good(t *testing.T) { + notifPlatform := &fallbackNotificationPlatform{} + c := core.New( + core.WithService(notification.Register(notifPlatform)), + core.WithService(Register(failingTrayPlatform{})), + core.WithServiceLock(), + ) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) + + svc := core.MustServiceFor[*Service](c, "systray") + require.NoError(t, svc.manager.Setup("Test", "Test")) + + r := taskRun(c, "systray.showMessage", TaskShowMessage{Title: "Core", Message: "Up"}) + require.True(t, r.OK) + assert.True(t, notifPlatform.sent) + assert.Equal(t, "Core", notifPlatform.opts.Title) + assert.Equal(t, "Up", notifPlatform.opts.Message) +} + func TestQueryInfo_Good(t *testing.T) { svc, c := newTestSystrayService(t) require.NoError(t, svc.manager.Setup("Core", "Core")) diff --git a/pkg/systray/wails.go b/pkg/systray/wails.go index 7f861663..0fee3025 100644 --- a/pkg/systray/wails.go +++ b/pkg/systray/wails.go @@ -2,6 +2,7 @@ package systray import ( + coreerr "dappco.re/go/core/log" "github.com/wailsapp/wails/v3/pkg/application" ) @@ -54,7 +55,7 @@ func (wt *wailsTray) AttachWindow(w WindowHandle) { func (wt *wailsTray) ShowMessage(title, message string) error { _ = title _ = message - return nil + return coreerr.E("systray.wailsTray.ShowMessage", "tray balloon messages are not supported by this backend", nil) } // wailsTrayMenu wraps *application.Menu for the PlatformMenu interface.