gui/pkg/systray/service.go
Snider 569a3427dc
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
Add Electron menu and tray bridge
2026-04-15 19:25:16 +01:00

220 lines
6.5 KiB
Go

package systray
import (
"context"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/gui/pkg/notification"
)
type Options struct{}
type Service struct {
*core.ServiceRuntime[Options]
manager *Manager
platform Platform
iconPath string
}
func (s *Service) OnStartup(_ context.Context) core.Result {
r := s.Core().QUERY(QueryConfig{})
if r.OK {
if trayConfig, ok := r.Value.(map[string]any); ok {
s.applyConfig(trayConfig)
}
}
s.Core().RegisterQuery(s.handleQuery)
s.Core().Action("systray.setIcon", func(_ context.Context, opts core.Options) core.Result {
t := taskSetTrayIconFromOptions(opts)
return core.Result{Value: nil, OK: true}.New(s.manager.SetIcon(t.Data))
})
s.Core().Action("systray.setTooltip", func(_ context.Context, opts core.Options) core.Result {
t := taskSetTrayTooltipFromOptions(opts)
return core.Result{Value: nil, OK: true}.New(s.manager.SetTooltip(t.Tooltip))
})
s.Core().Action("systray.setLabel", func(_ context.Context, opts core.Options) core.Result {
t := taskSetTrayLabelFromOptions(opts)
return core.Result{Value: nil, OK: true}.New(s.manager.SetLabel(t.Label))
})
s.Core().Action("systray.setMenu", func(_ context.Context, opts core.Options) core.Result {
t := taskSetTrayMenuFromOptions(opts)
return core.Result{Value: nil, OK: true}.New(s.taskSetTrayMenu(t))
})
s.Core().Action("systray.showMessage", func(_ context.Context, opts core.Options) core.Result {
t := taskShowMessageFromOptions(opts)
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)
return core.Result{OK: true}
})
s.Core().Action("systray.hidePanel", func(_ context.Context, _ core.Options) core.Result {
// Panel hide — deferred (requires WindowHandle integration)
return core.Result{OK: true}
})
return core.Result{OK: true}
}
func (s *Service) applyConfig(configData map[string]any) {
tooltip, _ := configData["tooltip"].(string)
if tooltip == "" {
tooltip = "Core"
}
_ = s.manager.Setup(tooltip, tooltip)
if iconPath, ok := configData["icon"].(string); ok && iconPath != "" {
// Icon loading is deferred to when assets are available.
// Store the path for later use.
s.iconPath = iconPath
}
}
func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result {
return core.Result{OK: true}
}
func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result {
switch q.(type) {
case QueryInfo:
return core.Result{Value: s.manager.GetInfo(), OK: true}
default:
return core.Result{}
}
}
func (s *Service) taskSetTrayMenu(t TaskSetTrayMenu) error {
// Register IPC-emitting callbacks for each menu item
for _, item := range t.Items {
if item.ActionID != "" {
actionID := item.ActionID
s.manager.RegisterCallback(actionID, func() {
_ = s.Core().ACTION(ActionTrayMenuItemClicked{ActionID: actionID})
})
}
}
return s.manager.SetMenu(t.Items)
}
func (s *Service) Manager() *Manager {
return s.manager
}
func taskSetTrayIconFromOptions(opts core.Options) TaskSetTrayIcon {
if task := opts.Get("task"); task.OK {
switch value := task.Value.(type) {
case TaskSetTrayIcon:
return value
case map[string]any:
var decoded TaskSetTrayIcon
if result := core.JSONUnmarshalString(core.JSONMarshalString(value), &decoded); result.OK {
return decoded
}
}
}
var decoded TaskSetTrayIcon
if result := core.JSONUnmarshalString(core.JSONMarshalString(optsToMap(opts)), &decoded); result.OK {
return decoded
}
return TaskSetTrayIcon{}
}
func taskSetTrayTooltipFromOptions(opts core.Options) TaskSetTrayTooltip {
if task := opts.Get("task"); task.OK {
switch value := task.Value.(type) {
case TaskSetTrayTooltip:
return value
case map[string]any:
var decoded TaskSetTrayTooltip
if result := core.JSONUnmarshalString(core.JSONMarshalString(value), &decoded); result.OK {
return decoded
}
}
}
var decoded TaskSetTrayTooltip
if result := core.JSONUnmarshalString(core.JSONMarshalString(optsToMap(opts)), &decoded); result.OK {
return decoded
}
return TaskSetTrayTooltip{}
}
func taskSetTrayLabelFromOptions(opts core.Options) TaskSetTrayLabel {
if task := opts.Get("task"); task.OK {
switch value := task.Value.(type) {
case TaskSetTrayLabel:
return value
case map[string]any:
var decoded TaskSetTrayLabel
if result := core.JSONUnmarshalString(core.JSONMarshalString(value), &decoded); result.OK {
return decoded
}
}
}
var decoded TaskSetTrayLabel
if result := core.JSONUnmarshalString(core.JSONMarshalString(optsToMap(opts)), &decoded); result.OK {
return decoded
}
return TaskSetTrayLabel{}
}
func taskSetTrayMenuFromOptions(opts core.Options) TaskSetTrayMenu {
if task := opts.Get("task"); task.OK {
switch value := task.Value.(type) {
case TaskSetTrayMenu:
return value
case map[string]any:
var decoded TaskSetTrayMenu
if result := core.JSONUnmarshalString(core.JSONMarshalString(value), &decoded); result.OK {
return decoded
}
}
}
var decoded TaskSetTrayMenu
if result := core.JSONUnmarshalString(core.JSONMarshalString(optsToMap(opts)), &decoded); result.OK {
return decoded
}
return TaskSetTrayMenu{}
}
func taskShowMessageFromOptions(opts core.Options) TaskShowMessage {
if task := opts.Get("task"); task.OK {
switch value := task.Value.(type) {
case TaskShowMessage:
return value
case map[string]any:
var decoded TaskShowMessage
if result := core.JSONUnmarshalString(core.JSONMarshalString(value), &decoded); result.OK {
return decoded
}
}
}
var decoded TaskShowMessage
if result := core.JSONUnmarshalString(core.JSONMarshalString(optsToMap(opts)), &decoded); result.OK {
return decoded
}
return TaskShowMessage{}
}
func optsToMap(opts core.Options) map[string]any {
items := make(map[string]any, opts.Len())
for _, item := range opts.Items() {
items[item.Key] = item.Value
}
return items
}