633 lines
18 KiB
Go
633 lines
18 KiB
Go
package display
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
core "dappco.re/go/core"
|
|
coreerr "dappco.re/go/core/log"
|
|
"forge.lthn.ai/core/gui/pkg/clipboard"
|
|
"forge.lthn.ai/core/gui/pkg/dialog"
|
|
"forge.lthn.ai/core/gui/pkg/environment"
|
|
"forge.lthn.ai/core/gui/pkg/notification"
|
|
"forge.lthn.ai/core/gui/pkg/screen"
|
|
"forge.lthn.ai/core/gui/pkg/systray"
|
|
)
|
|
|
|
// Screen is the public display-screen shape used by the display service API.
|
|
type Screen struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
X int `json:"x"`
|
|
Y int `json:"y"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
ScaleFactor float64 `json:"scaleFactor"`
|
|
IsPrimary bool `json:"isPrimary"`
|
|
}
|
|
|
|
// WorkArea is a usable screen rectangle.
|
|
type WorkArea = screen.Rect
|
|
|
|
// FileFilter describes a file type filter for the display service dialogs.
|
|
type FileFilter struct {
|
|
DisplayName string `json:"displayName"`
|
|
Pattern string `json:"pattern"`
|
|
}
|
|
|
|
// OpenFileOptions configures an open-file dialog.
|
|
type OpenFileOptions struct {
|
|
Title string `json:"title,omitempty"`
|
|
DefaultDirectory string `json:"defaultDirectory,omitempty"`
|
|
DefaultFilename string `json:"defaultFilename,omitempty"`
|
|
AllowMultiple bool `json:"allowMultiple,omitempty"`
|
|
Filters []FileFilter `json:"filters,omitempty"`
|
|
}
|
|
|
|
// SaveFileOptions configures a save-file dialog.
|
|
type SaveFileOptions struct {
|
|
Title string `json:"title,omitempty"`
|
|
DefaultDirectory string `json:"defaultDirectory,omitempty"`
|
|
DefaultFilename string `json:"defaultFilename,omitempty"`
|
|
Filters []FileFilter `json:"filters,omitempty"`
|
|
}
|
|
|
|
// OpenDirectoryOptions configures a folder picker dialog.
|
|
type OpenDirectoryOptions struct {
|
|
Title string `json:"title,omitempty"`
|
|
DefaultDirectory string `json:"defaultDirectory,omitempty"`
|
|
}
|
|
|
|
// TrayMenuItem describes a tray menu entry.
|
|
type TrayMenuItem struct {
|
|
Label string `json:"label,omitempty"`
|
|
ActionID string `json:"actionId,omitempty"`
|
|
IsSeparator bool `json:"isSeparator,omitempty"`
|
|
Children []TrayMenuItem `json:"children,omitempty"`
|
|
}
|
|
|
|
// NotificationOptions configures a native notification.
|
|
type NotificationOptions struct {
|
|
ID string `json:"id,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
Subtitle string `json:"subtitle,omitempty"`
|
|
}
|
|
|
|
// Theme reports whether the active theme is dark.
|
|
type Theme struct {
|
|
IsDark bool `json:"isDark"`
|
|
}
|
|
|
|
func unexpectedResultType(method string) error {
|
|
return coreerr.E(method, "unexpected result type", nil)
|
|
}
|
|
|
|
func failedQuery(method, query string) error {
|
|
return coreerr.E(method, query+" query failed", nil)
|
|
}
|
|
|
|
func (s *Service) GetScreens() []*Screen {
|
|
r := s.Core().QUERY(screen.QueryAll{})
|
|
if !r.OK {
|
|
return nil
|
|
}
|
|
screens, ok := r.Value.([]screen.Screen)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
result := make([]*Screen, 0, len(screens))
|
|
for i := range screens {
|
|
result = append(result, screenToDisplay(&screens[i]))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *Service) GetScreen(id string) (*Screen, error) {
|
|
r := s.Core().QUERY(screen.QueryByID{ID: id})
|
|
if !r.OK {
|
|
if err, ok := r.Value.(error); ok {
|
|
return nil, err
|
|
}
|
|
return nil, failedQuery("display.GetScreen", "screen.queryByID")
|
|
}
|
|
scr, ok := r.Value.(*screen.Screen)
|
|
if !ok {
|
|
return nil, unexpectedResultType("display.GetScreen")
|
|
}
|
|
return screenToDisplay(scr), nil
|
|
}
|
|
|
|
func (s *Service) GetPrimaryScreen() (*Screen, error) {
|
|
r := s.Core().QUERY(screen.QueryPrimary{})
|
|
if !r.OK {
|
|
if err, ok := r.Value.(error); ok {
|
|
return nil, err
|
|
}
|
|
return nil, failedQuery("display.GetPrimaryScreen", "screen.queryPrimary")
|
|
}
|
|
scr, ok := r.Value.(*screen.Screen)
|
|
if !ok {
|
|
return nil, unexpectedResultType("display.GetPrimaryScreen")
|
|
}
|
|
return screenToDisplay(scr), nil
|
|
}
|
|
|
|
func (s *Service) GetScreenAtPoint(x, y int) (*Screen, error) {
|
|
r := s.Core().QUERY(screen.QueryAtPoint{X: x, Y: y})
|
|
if !r.OK {
|
|
if err, ok := r.Value.(error); ok {
|
|
return nil, err
|
|
}
|
|
return nil, failedQuery("display.GetScreenAtPoint", "screen.queryAtPoint")
|
|
}
|
|
scr, ok := r.Value.(*screen.Screen)
|
|
if !ok {
|
|
return nil, unexpectedResultType("display.GetScreenAtPoint")
|
|
}
|
|
return screenToDisplay(scr), nil
|
|
}
|
|
|
|
func (s *Service) GetScreenForWindow(name string) (*Screen, error) {
|
|
info, err := s.GetWindowInfo(name)
|
|
if err != nil || info == nil {
|
|
return nil, err
|
|
}
|
|
return s.GetScreenAtPoint(info.X+max(info.Width/2, 1), info.Y+max(info.Height/2, 1))
|
|
}
|
|
|
|
func (s *Service) GetWorkAreas() []*WorkArea {
|
|
r := s.Core().QUERY(screen.QueryWorkAreas{})
|
|
if !r.OK {
|
|
return nil
|
|
}
|
|
areas, ok := r.Value.([]screen.Rect)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
result := make([]*WorkArea, 0, len(areas))
|
|
for i := range areas {
|
|
area := areas[i]
|
|
result = append(result, &area)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *Service) OpenSingleFileDialog(opts OpenFileOptions) (string, error) {
|
|
paths, err := s.OpenFileDialog(opts)
|
|
if err != nil || len(paths) == 0 {
|
|
return "", err
|
|
}
|
|
return paths[0], nil
|
|
}
|
|
|
|
func (s *Service) OpenFileDialog(opts OpenFileOptions) ([]string, error) {
|
|
result := s.Core().Action("dialog.openFile").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: dialog.TaskOpenFile{Options: toDialogOpenFileOptions(opts)}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return nil, err
|
|
}
|
|
return nil, coreerr.E("display.OpenFileDialog", "dialog.openFile action failed", nil)
|
|
}
|
|
paths, ok := result.Value.([]string)
|
|
if !ok {
|
|
return nil, unexpectedResultType("display.OpenFileDialog")
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
func (s *Service) SaveFileDialog(opts SaveFileOptions) (string, error) {
|
|
result := s.Core().Action("dialog.saveFile").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: dialog.TaskSaveFile{Options: toDialogSaveFileOptions(opts)}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return "", err
|
|
}
|
|
return "", coreerr.E("display.SaveFileDialog", "dialog.saveFile action failed", nil)
|
|
}
|
|
path, ok := result.Value.(string)
|
|
if !ok {
|
|
return "", unexpectedResultType("display.SaveFileDialog")
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (s *Service) OpenDirectoryDialog(opts OpenDirectoryOptions) (string, error) {
|
|
result := s.Core().Action("dialog.openDirectory").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: dialog.TaskOpenDirectory{Options: toDialogOpenDirectoryOptions(opts)}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return "", err
|
|
}
|
|
return "", coreerr.E("display.OpenDirectoryDialog", "dialog.openDirectory action failed", nil)
|
|
}
|
|
path, ok := result.Value.(string)
|
|
if !ok {
|
|
return "", unexpectedResultType("display.OpenDirectoryDialog")
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (s *Service) ConfirmDialog(title, message string) (bool, error) {
|
|
result := s.Core().Action("dialog.question").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: dialog.TaskQuestion{
|
|
Title: title,
|
|
Message: message,
|
|
Buttons: []string{"Yes", "No"},
|
|
}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return false, err
|
|
}
|
|
return false, coreerr.E("display.ConfirmDialog", "dialog.question action failed", nil)
|
|
}
|
|
button, ok := result.Value.(string)
|
|
if !ok {
|
|
return false, unexpectedResultType("display.ConfirmDialog")
|
|
}
|
|
return button == "Yes", nil
|
|
}
|
|
|
|
func (s *Service) PromptDialog(title, message string) (string, bool, error) {
|
|
result := s.Core().Action("dialog.prompt").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: dialog.TaskPrompt{Title: title, Message: message}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return "", false, err
|
|
}
|
|
return "", false, coreerr.E("display.PromptDialog", "dialog.prompt action failed", nil)
|
|
}
|
|
prompt, ok := result.Value.(dialog.PromptResult)
|
|
if !ok {
|
|
return "", false, coreerr.E("display.PromptDialog", "unexpected result type", nil)
|
|
}
|
|
return prompt.Value, prompt.Confirmed, nil
|
|
}
|
|
|
|
func (s *Service) SetTrayIcon(icon []byte) error {
|
|
result := s.Core().Action("systray.setIcon").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: systray.TaskSetTrayIcon{Data: icon}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.SetTrayIcon", "systray.setIcon action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) SetTrayTooltip(tooltip string) error {
|
|
result := s.Core().Action("systray.setTooltip").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: systray.TaskSetTrayTooltip{Tooltip: tooltip}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.SetTrayTooltip", "systray.setTooltip action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) SetTrayLabel(label string) error {
|
|
result := s.Core().Action("systray.setLabel").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: systray.TaskSetTrayLabel{Label: label}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.SetTrayLabel", "systray.setLabel action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) SetTrayMenu(items []TrayMenuItem) error {
|
|
result := s.Core().Action("systray.setMenu").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: systray.TaskSetTrayMenu{Items: trayMenuItemsToSystray(items)}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.SetTrayMenu", "systray.setMenu action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) GetTrayInfo() map[string]any {
|
|
r := s.Core().QUERY(systray.QueryInfo{})
|
|
if !r.OK {
|
|
return nil
|
|
}
|
|
info, _ := r.Value.(map[string]any)
|
|
return info
|
|
}
|
|
|
|
func (s *Service) ShowTrayMessage(title, message string) error {
|
|
result := s.Core().Action("systray.showMessage").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: systray.TaskShowMessage{Title: title, Message: message}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.ShowTrayMessage", "systray.showMessage action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) ReadClipboard() (string, error) {
|
|
r := s.Core().QUERY(clipboard.QueryText{})
|
|
if !r.OK {
|
|
if err, ok := r.Value.(error); ok {
|
|
return "", err
|
|
}
|
|
return "", nil
|
|
}
|
|
content, ok := r.Value.(clipboard.ClipboardContent)
|
|
if !ok {
|
|
return "", coreerr.E("display.ReadClipboard", "unexpected result type", nil)
|
|
}
|
|
return content.Text, nil
|
|
}
|
|
|
|
func (s *Service) WriteClipboard(text string) error {
|
|
result := s.Core().Action("clipboard.setText").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: clipboard.TaskSetText{Text: text}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.WriteClipboard", "clipboard.setText action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) HasClipboard() bool {
|
|
text, err := s.ReadClipboard()
|
|
return err == nil && text != ""
|
|
}
|
|
|
|
func (s *Service) ClearClipboard() error {
|
|
result := s.Core().Action("clipboard.clear").Run(context.Background(), core.NewOptions())
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.ClearClipboard", "clipboard.clear action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) ReadClipboardImage() ([]byte, error) {
|
|
r := s.Core().QUERY(clipboard.QueryImage{})
|
|
if !r.OK {
|
|
if err, ok := r.Value.(error); ok {
|
|
return nil, err
|
|
}
|
|
return nil, failedQuery("display.ReadClipboardImage", "clipboard.queryImage")
|
|
}
|
|
content, ok := r.Value.(clipboard.ImageContent)
|
|
if !ok {
|
|
return nil, unexpectedResultType("display.ReadClipboardImage")
|
|
}
|
|
if !content.HasImage {
|
|
return nil, nil
|
|
}
|
|
return append([]byte(nil), content.Data...), nil
|
|
}
|
|
|
|
func (s *Service) WriteClipboardImage(data []byte) error {
|
|
if len(data) == 0 {
|
|
return coreerr.E("display.WriteClipboardImage", "clipboard image data is required", nil)
|
|
}
|
|
if len(data) > clipboard.MaxImageBytes {
|
|
return coreerr.E("display.WriteClipboardImage", "clipboard image exceeds maximum size", nil)
|
|
}
|
|
result := s.Core().Action("clipboard.setImage").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "data", Value: append([]byte(nil), data...)},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.WriteClipboardImage", "clipboard.setImage action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) ShowNotification(opts NotificationOptions) error {
|
|
return s.sendNotification(notification.NotificationOptions{
|
|
ID: opts.ID,
|
|
Title: opts.Title,
|
|
Message: opts.Message,
|
|
Subtitle: opts.Subtitle,
|
|
})
|
|
}
|
|
|
|
func (s *Service) ShowInfoNotification(title, message string) error {
|
|
return s.sendNotification(notification.NotificationOptions{Title: title, Message: message})
|
|
}
|
|
|
|
func (s *Service) ShowWarningNotification(title, message string) error {
|
|
return s.sendNotification(notification.NotificationOptions{
|
|
Title: title,
|
|
Message: message,
|
|
Severity: notification.SeverityWarning,
|
|
})
|
|
}
|
|
|
|
func (s *Service) ShowErrorNotification(title, message string) error {
|
|
return s.sendNotification(notification.NotificationOptions{
|
|
Title: title,
|
|
Message: message,
|
|
Severity: notification.SeverityError,
|
|
})
|
|
}
|
|
|
|
func (s *Service) RequestNotificationPermission() (bool, error) {
|
|
r := s.Core().Action("notification.requestPermission").Run(context.Background(), core.NewOptions())
|
|
if !r.OK {
|
|
if err, ok := r.Value.(error); ok {
|
|
return false, err
|
|
}
|
|
return false, coreerr.E("display.RequestNotificationPermission", "notification.requestPermission action failed", nil)
|
|
}
|
|
granted, ok := r.Value.(bool)
|
|
if !ok {
|
|
return false, unexpectedResultType("display.RequestNotificationPermission")
|
|
}
|
|
return granted, nil
|
|
}
|
|
|
|
func (s *Service) CheckNotificationPermission() (bool, error) {
|
|
r := s.Core().QUERY(notification.QueryPermission{})
|
|
if !r.OK {
|
|
if err, ok := r.Value.(error); ok {
|
|
return false, err
|
|
}
|
|
return false, coreerr.E("display.CheckNotificationPermission", "notification query failed", nil)
|
|
}
|
|
status, ok := r.Value.(notification.PermissionStatus)
|
|
if !ok {
|
|
return false, unexpectedResultType("display.CheckNotificationPermission")
|
|
}
|
|
return status.Granted, nil
|
|
}
|
|
|
|
func (s *Service) ClearNotifications() error {
|
|
result := s.Core().Action("notification.clear").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: notification.TaskClear{}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.ClearNotifications", "notification.clear action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) SetTheme(theme string) error {
|
|
result := s.Core().Action("environment.setTheme").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: environment.TaskSetTheme{Theme: theme}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.SetTheme", "environment.setTheme action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) GetTheme() *Theme {
|
|
r := s.Core().QUERY(environment.QueryTheme{})
|
|
info, ok := themeInfoFromQueryResult(s, "display.GetTheme", r)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &Theme{IsDark: info.IsDark}
|
|
}
|
|
|
|
func (s *Service) GetSystemTheme() string {
|
|
r := s.Core().QUERY(environment.QueryTheme{})
|
|
info, ok := themeInfoFromQueryResult(s, "display.GetSystemTheme", r)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return info.Theme
|
|
}
|
|
|
|
func themeInfoFromQueryResult(s *Service, method string, r core.Result) (environment.ThemeInfo, bool) {
|
|
if !r.OK {
|
|
return environment.ThemeInfo{}, false
|
|
}
|
|
info, ok := r.Value.(environment.ThemeInfo)
|
|
if ok {
|
|
return info, true
|
|
}
|
|
if s != nil && s.Core() != nil {
|
|
s.Core().LogWarn(fmt.Errorf("query=environment.QueryTheme value_type=%T", r.Value), method, "malformed theme query payload")
|
|
}
|
|
return environment.ThemeInfo{}, false
|
|
}
|
|
|
|
func (s *Service) sendNotification(opts notification.NotificationOptions) error {
|
|
result := s.Core().Action("notification.send").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: notification.TaskSend{Options: opts}},
|
|
))
|
|
if !result.OK {
|
|
if err, ok := result.Value.(error); ok {
|
|
return err
|
|
}
|
|
return coreerr.E("display.sendNotification", "notification.send action failed", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func screenToDisplay(scr *screen.Screen) *Screen {
|
|
if scr == nil {
|
|
return nil
|
|
}
|
|
return &Screen{
|
|
ID: scr.ID,
|
|
Name: scr.Name,
|
|
X: scr.Bounds.X,
|
|
Y: scr.Bounds.Y,
|
|
Width: scr.Bounds.Width,
|
|
Height: scr.Bounds.Height,
|
|
ScaleFactor: scr.ScaleFactor,
|
|
IsPrimary: scr.IsPrimary,
|
|
}
|
|
}
|
|
|
|
func toDialogOpenFileOptions(opts OpenFileOptions) dialog.OpenFileOptions {
|
|
return dialog.OpenFileOptions{
|
|
Title: opts.Title,
|
|
Directory: opts.DefaultDirectory,
|
|
Filename: opts.DefaultFilename,
|
|
AllowMultiple: opts.AllowMultiple,
|
|
Filters: toDialogFileFilters(opts.Filters),
|
|
}
|
|
}
|
|
|
|
func toDialogSaveFileOptions(opts SaveFileOptions) dialog.SaveFileOptions {
|
|
return dialog.SaveFileOptions{
|
|
Title: opts.Title,
|
|
Directory: opts.DefaultDirectory,
|
|
Filename: opts.DefaultFilename,
|
|
Filters: toDialogFileFilters(opts.Filters),
|
|
}
|
|
}
|
|
|
|
func toDialogOpenDirectoryOptions(opts OpenDirectoryOptions) dialog.OpenDirectoryOptions {
|
|
return dialog.OpenDirectoryOptions{
|
|
Title: opts.Title,
|
|
Directory: opts.DefaultDirectory,
|
|
}
|
|
}
|
|
|
|
func toDialogFileFilters(filters []FileFilter) []dialog.FileFilter {
|
|
if len(filters) == 0 {
|
|
return nil
|
|
}
|
|
result := make([]dialog.FileFilter, 0, len(filters))
|
|
for _, filter := range filters {
|
|
result = append(result, dialog.FileFilter{
|
|
DisplayName: filter.DisplayName,
|
|
Pattern: filter.Pattern,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func trayMenuItemsToSystray(items []TrayMenuItem) []systray.TrayMenuItem {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
result := make([]systray.TrayMenuItem, 0, len(items))
|
|
for _, item := range items {
|
|
converted := systray.TrayMenuItem{
|
|
Label: item.Label,
|
|
ActionID: item.ActionID,
|
|
}
|
|
if item.IsSeparator {
|
|
converted.Type = "separator"
|
|
}
|
|
if len(item.Children) > 0 {
|
|
converted.Submenu = trayMenuItemsToSystray(item.Children)
|
|
}
|
|
result = append(result, converted)
|
|
}
|
|
return result
|
|
}
|