gui/pkg/display/display.go
Snider 62ec735c10
Some checks failed
Security Scan / security (push) Has been cancelled
Test / test (push) Has been cancelled
refactor: AX compliance sweep — replace banned stdlib imports with core primitives
Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.

Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-13 09:32:01 +01:00

2420 lines
73 KiB
Go

// pkg/display/display.go
package display
import (
"context"
"encoding/base64"
"os"
"runtime"
corego "dappco.re/go/core"
"forge.lthn.ai/core/config"
"forge.lthn.ai/core/go/pkg/core"
"dappco.re/go/core/gui/pkg/browser"
"dappco.re/go/core/gui/pkg/clipboard"
"dappco.re/go/core/gui/pkg/contextmenu"
"dappco.re/go/core/gui/pkg/dialog"
"dappco.re/go/core/gui/pkg/dock"
"dappco.re/go/core/gui/pkg/environment"
"dappco.re/go/core/gui/pkg/keybinding"
"dappco.re/go/core/gui/pkg/lifecycle"
"dappco.re/go/core/gui/pkg/menu"
"dappco.re/go/core/gui/pkg/notification"
"dappco.re/go/core/gui/pkg/screen"
"dappco.re/go/core/gui/pkg/systray"
"dappco.re/go/core/gui/pkg/webview"
"dappco.re/go/core/gui/pkg/window"
"github.com/wailsapp/wails/v3/pkg/application"
)
// Options holds configuration for the display service.
// Use: svc, err := display.NewService()
type Options struct{}
// WindowInfo is an alias for window.WindowInfo (backward compatibility).
type WindowInfo = window.WindowInfo
// LayoutSuggestion is an alias for window.LayoutSuggestion (backward compatibility).
type LayoutSuggestion = window.LayoutSuggestion
// Service manages windowing, dialogs, and other visual elements.
// It orchestrates sub-services (window, systray, menu) via IPC and bridges
// IPC actions to WebSocket events for TypeScript apps.
// Use: svc, err := display.NewService()
type Service struct {
*core.ServiceRuntime[Options]
wailsApp *application.App
app App
config Options
configData map[string]map[string]any
cfg *config.Config // config instance for file persistence
events *WSEventManager
}
// NewService creates a display service with empty config sections.
// Use: svc, err := display.NewService()
func NewService() (*Service, error) {
return &Service{
configData: map[string]map[string]any{
"window": {},
"systray": {},
"menu": {},
},
}, nil
}
// Deprecated: use NewService.
// Use: svc, err := display.New()
func New() (*Service, error) {
return NewService()
}
// Register creates a factory closure that captures the Wails app.
// Use: core.WithService(display.Register(app))
// Pass nil for testing without a Wails runtime.
func Register(wailsApp *application.App) func(*core.Core) (any, error) {
return func(c *core.Core) (any, error) {
s, err := NewService()
if err != nil {
return nil, err
}
s.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{})
s.wailsApp = wailsApp
return s, nil
}
}
// OnStartup loads config and registers IPC handlers synchronously.
// CRITICAL: config handlers MUST be registered before returning —
// sub-services depend on them during their own OnStartup.
// Use: _ = svc.OnStartup(context.Background())
func (s *Service) OnStartup(ctx context.Context) error {
s.loadConfig()
// Register config query/task handlers — available NOW for sub-services
s.Core().RegisterQuery(s.handleConfigQuery)
s.Core().RegisterTask(s.handleConfigTask)
// Initialise Wails wrappers if app is available (nil in tests)
if s.wailsApp != nil {
s.app = newWailsApp(s.wailsApp)
s.events = NewWSEventManager()
}
return nil
}
// HandleIPCEvents is auto-discovered and registered by core.WithService.
// It bridges sub-service IPC actions to WebSocket events for TS apps.
// Use: _ = svc.HandleIPCEvents(core, msg)
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
switch m := msg.(type) {
case core.ActionServiceStartup:
// All services have completed OnStartup — safe to PERFORM on sub-services
s.buildMenu()
s.setupTray()
case window.ActionWindowOpened:
if s.events != nil {
s.events.Emit(Event{Type: EventWindowCreate, Window: m.Name,
Data: map[string]any{"name": m.Name}})
}
case window.ActionWindowClosed:
if s.events != nil {
s.events.Emit(Event{Type: EventWindowClose, Window: m.Name,
Data: map[string]any{"name": m.Name}})
}
case window.ActionWindowMoved:
if s.events != nil {
s.events.Emit(Event{Type: EventWindowMove, Window: m.Name,
Data: map[string]any{"x": m.X, "y": m.Y}})
}
case window.ActionWindowResized:
if s.events != nil {
s.events.Emit(Event{Type: EventWindowResize, Window: m.Name,
Data: map[string]any{"w": m.W, "h": m.H}})
}
case window.ActionWindowFocused:
if s.events != nil {
s.events.Emit(Event{Type: EventWindowFocus, Window: m.Name})
}
case window.ActionWindowBlurred:
if s.events != nil {
s.events.Emit(Event{Type: EventWindowBlur, Window: m.Name})
}
case systray.ActionTrayClicked:
if s.events != nil {
s.events.Emit(Event{Type: EventTrayClick})
}
case systray.ActionTrayMenuItemClicked:
if s.events != nil {
s.events.Emit(Event{Type: EventTrayMenuItemClick,
Data: map[string]any{"actionId": m.ActionID}})
}
s.handleTrayAction(m.ActionID)
case environment.ActionThemeChanged:
if s.events != nil {
theme := "light"
if m.IsDark {
theme = "dark"
}
s.events.Emit(Event{Type: EventThemeChange,
Data: map[string]any{"isDark": m.IsDark, "theme": theme}})
}
case notification.ActionNotificationClicked:
if s.events != nil {
s.events.Emit(Event{Type: EventNotificationClick,
Data: map[string]any{"id": m.ID}})
}
case screen.ActionScreensChanged:
if s.events != nil {
s.events.Emit(Event{Type: EventScreenChange,
Data: map[string]any{"screens": m.Screens}})
}
case keybinding.ActionTriggered:
if s.events != nil {
s.events.Emit(Event{Type: EventKeybindingTriggered,
Data: map[string]any{"accelerator": m.Accelerator}})
}
case window.ActionFilesDropped:
if s.events != nil {
s.events.Emit(Event{Type: EventWindowFileDrop, Window: m.Name,
Data: map[string]any{"paths": m.Paths, "targetId": m.TargetID}})
}
case dock.ActionVisibilityChanged:
if s.events != nil {
s.events.Emit(Event{Type: EventDockVisibility,
Data: map[string]any{"visible": m.Visible}})
}
case lifecycle.ActionApplicationStarted:
if s.events != nil {
s.events.Emit(Event{Type: EventAppStarted})
}
case lifecycle.ActionOpenedWithFile:
if s.events != nil {
s.events.Emit(Event{Type: EventAppOpenedWithFile,
Data: map[string]any{"path": m.Path}})
}
case lifecycle.ActionWillTerminate:
if s.events != nil {
s.events.Emit(Event{Type: EventAppWillTerminate})
}
case lifecycle.ActionDidBecomeActive:
if s.events != nil {
s.events.Emit(Event{Type: EventAppActive})
}
case lifecycle.ActionDidResignActive:
if s.events != nil {
s.events.Emit(Event{Type: EventAppInactive})
}
case lifecycle.ActionPowerStatusChanged:
if s.events != nil {
s.events.Emit(Event{Type: EventSystemPowerChange})
}
case lifecycle.ActionSystemSuspend:
if s.events != nil {
s.events.Emit(Event{Type: EventSystemSuspend})
}
case lifecycle.ActionSystemResume:
if s.events != nil {
s.events.Emit(Event{Type: EventSystemResume})
}
case contextmenu.ActionItemClicked:
if s.events != nil {
s.events.Emit(Event{Type: EventContextMenuClick,
Data: map[string]any{
"menuName": m.MenuName,
"actionId": m.ActionID,
"data": m.Data,
}})
}
case webview.ActionConsoleMessage:
if s.events != nil {
s.events.Emit(Event{Type: EventWebviewConsole, Window: m.Window,
Data: map[string]any{"message": m.Message}})
}
case webview.ActionException:
if s.events != nil {
s.events.Emit(Event{Type: EventWebviewException, Window: m.Window,
Data: map[string]any{"exception": m.Exception}})
}
case ActionIDECommand:
if s.events != nil {
s.events.Emit(Event{Type: EventIDECommand,
Data: map[string]any{"command": m.Command}})
}
}
return nil
}
// WSMessage represents a command received from a WebSocket client.
type WSMessage struct {
Action string `json:"action"`
Data map[string]any `json:"data,omitempty"`
}
// wsRequire extracts a string field from WS data and returns an error if it is empty.
func wsRequire(data map[string]any, key string) (string, error) {
v, _ := data[key].(string)
if v == "" {
return "", corego.NewError(corego.Sprintf("ws: missing required field %q", key))
}
return v, nil
}
// handleWSMessage bridges WebSocket commands to IPC calls.
func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
var result any
var handled bool
var err error
switch msg.Action {
case "keybinding:add":
accelerator, _ := msg.Data["accelerator"].(string)
description, _ := msg.Data["description"].(string)
result, handled, err = s.Core().PERFORM(keybinding.TaskAdd{
Accelerator: accelerator, Description: description,
})
case "keybinding:remove":
accelerator, _ := msg.Data["accelerator"].(string)
result, handled, err = s.Core().PERFORM(keybinding.TaskRemove{
Accelerator: accelerator,
})
case "keybinding:list":
result, handled, err = s.Core().QUERY(keybinding.QueryList{})
case "browser:open-url":
url, _ := msg.Data["url"].(string)
result, handled, err = s.Core().PERFORM(browser.TaskOpenURL{URL: url})
case "browser:open-file":
path, _ := msg.Data["path"].(string)
result, handled, err = s.Core().PERFORM(browser.TaskOpenFile{Path: path})
case "window:list":
result, handled, err = s.Core().QUERY(window.QueryWindowList{})
case "window:get":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(window.QueryWindowByName{Name: name})
case "window:focused":
result, handled, err = s.GetFocusedWindow(), true, nil
case "window:create":
var opts CreateWindowOptions
encodedR := corego.JSONMarshal(msg.Data)
if encodedR.OK {
_ = corego.JSONUnmarshal(encodedR.Value.([]byte), &opts)
}
return nil, false, corego.Wrap(err, "display.ws", "ws: invalid window create options")
info, createErr := s.CreateWindow(opts)
if createErr != nil {
return nil, false, createErr
}
result, handled, err = info, true, nil
case "window:close":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
result, handled, err = nil, true, s.CloseWindow(name)
case "window:position":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
x, _ := msg.Data["x"].(float64)
y, _ := msg.Data["y"].(float64)
result, handled, err = nil, true, s.SetWindowPosition(name, int(x), int(y))
case "window:size":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
width, _ := msg.Data["width"].(float64)
height, _ := msg.Data["height"].(float64)
result, handled, err = nil, true, s.SetWindowSize(name, int(width), int(height))
case "window:bounds":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
x, _ := msg.Data["x"].(float64)
y, _ := msg.Data["y"].(float64)
width, _ := msg.Data["width"].(float64)
height, _ := msg.Data["height"].(float64)
result, handled, err = nil, true, s.SetWindowBounds(name, int(x), int(y), int(width), int(height))
case "window:maximize":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
result, handled, err = nil, true, s.MaximizeWindow(name)
case "window:minimize":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
result, handled, err = nil, true, s.MinimizeWindow(name)
case "window:restore":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
result, handled, err = nil, true, s.RestoreWindow(name)
case "window:focus":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
result, handled, err = nil, true, s.FocusWindow(name)
case "focus:set":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
result, handled, err = nil, true, s.FocusSet(name)
case "window:visibility":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
visible, _ := msg.Data["visible"].(bool)
result, handled, err = nil, true, s.SetWindowVisibility(name, visible)
case "window:title-set":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
title, e := wsRequire(msg.Data, "title")
if e != nil {
return nil, false, e
}
result, handled, err = nil, true, s.SetWindowTitle(name, title)
case "window:title-get":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
title, titleErr := s.GetWindowTitle(name)
if titleErr != nil {
return nil, false, titleErr
}
result, handled, err = title, true, nil
case "window:always-on-top":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
alwaysOnTop, _ := msg.Data["alwaysOnTop"].(bool)
result, handled, err = nil, true, s.SetWindowAlwaysOnTop(name, alwaysOnTop)
case "window:background-colour":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
red, _ := msg.Data["red"].(float64)
green, _ := msg.Data["green"].(float64)
blue, _ := msg.Data["blue"].(float64)
alpha, _ := msg.Data["alpha"].(float64)
result, handled, err = nil, true, s.SetWindowBackgroundColour(name, uint8(red), uint8(green), uint8(blue), uint8(alpha))
case "window:opacity":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
opacity, _ := msg.Data["opacity"].(float64)
result, handled, err = nil, true, s.SetWindowOpacity(name, float32(opacity))
case "window:fullscreen":
name, e := wsRequire(msg.Data, "name")
if e != nil {
return nil, false, e
}
fullscreen, _ := msg.Data["fullscreen"].(bool)
result, handled, err = nil, true, s.SetWindowFullscreen(name, fullscreen)
case "dock:show":
result, handled, err = s.Core().PERFORM(dock.TaskShowIcon{})
case "dock:hide":
result, handled, err = s.Core().PERFORM(dock.TaskHideIcon{})
case "dock:badge":
label, _ := msg.Data["label"].(string)
result, handled, err = s.Core().PERFORM(dock.TaskSetBadge{Label: label})
case "dock:badge-remove":
result, handled, err = s.Core().PERFORM(dock.TaskRemoveBadge{})
case "dock:visible":
result, handled, err = s.Core().QUERY(dock.QueryVisible{})
case "contextmenu:add":
name, _ := msg.Data["name"].(string)
menuR := corego.JSONMarshal(msg.Data["menu"])
var menuDef contextmenu.ContextMenuDef
if menuR.OK {
_ = corego.JSONUnmarshal(menuR.Value.([]byte), &menuDef)
}
result, handled, err = s.Core().PERFORM(contextmenu.TaskAdd{
Name: name, Menu: menuDef,
})
case "contextmenu:remove":
name, _ := msg.Data["name"].(string)
result, handled, err = s.Core().PERFORM(contextmenu.TaskRemove{Name: name})
case "contextmenu:get":
name, _ := msg.Data["name"].(string)
result, handled, err = s.Core().QUERY(contextmenu.QueryGet{Name: name})
case "contextmenu:list":
result, handled, err = s.Core().QUERY(contextmenu.QueryList{})
case "webview:eval":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
script, _ := msg.Data["script"].(string)
result, handled, err = s.Core().PERFORM(webview.TaskEvaluate{Window: w, Script: script})
case "webview:click":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskClick{Window: w, Selector: sel})
case "webview:type":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
text, _ := msg.Data["text"].(string)
result, handled, err = s.Core().PERFORM(webview.TaskType{Window: w, Selector: sel, Text: text})
case "webview:navigate":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
url, e := wsRequire(msg.Data, "url")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskNavigate{Window: w, URL: url})
case "webview:screenshot":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskScreenshot{Window: w})
case "webview:screenshot-element":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskScreenshotElement{Window: w, Selector: sel})
case "webview:scroll":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
x, _ := msg.Data["x"].(float64)
y, _ := msg.Data["y"].(float64)
result, handled, err = s.Core().PERFORM(webview.TaskScroll{Window: w, X: int(x), Y: int(y)})
case "webview:hover":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskHover{Window: w, Selector: sel})
case "webview:select":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
val, _ := msg.Data["value"].(string)
result, handled, err = s.Core().PERFORM(webview.TaskSelect{Window: w, Selector: sel, Value: val})
case "webview:check":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
checked, _ := msg.Data["checked"].(bool)
result, handled, err = s.Core().PERFORM(webview.TaskCheck{Window: w, Selector: sel, Checked: checked})
case "webview:upload":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
pathsRaw, _ := msg.Data["paths"].([]any)
var paths []string
for _, p := range pathsRaw {
if ps, ok := p.(string); ok {
paths = append(paths, ps)
}
}
result, handled, err = s.Core().PERFORM(webview.TaskUploadFile{Window: w, Selector: sel, Paths: paths})
case "webview:viewport":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
width, _ := msg.Data["width"].(float64)
height, _ := msg.Data["height"].(float64)
result, handled, err = s.Core().PERFORM(webview.TaskSetViewport{Window: w, Width: int(width), Height: int(height)})
case "webview:clear-console":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskClearConsole{Window: w})
case "webview:highlight":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
colour, _ := msg.Data["colour"].(string)
result, handled, err = s.Core().PERFORM(webview.TaskHighlight{Window: w, Selector: sel, Colour: colour})
case "webview:computed-style":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QueryComputedStyle{Window: w, Selector: sel})
case "webview:performance":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QueryPerformance{Window: w})
case "webview:resources":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QueryResources{Window: w})
case "webview:network":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
limit := 0
if l, ok := msg.Data["limit"].(float64); ok {
limit = int(l)
}
result, handled, err = s.Core().QUERY(webview.QueryNetwork{Window: w, Limit: limit})
case "webview:network-inject":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskInjectNetworkLogging{Window: w})
case "webview:network-clear":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskClearNetworkLog{Window: w})
case "webview:print":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskPrint{Window: w})
case "webview:pdf":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskExportPDF{Window: w})
case "webview:console":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
level, _ := msg.Data["level"].(string)
limit := 100
if l, ok := msg.Data["limit"].(float64); ok {
limit = int(l)
}
result, handled, err = s.Core().QUERY(webview.QueryConsole{Window: w, Level: level, Limit: limit})
case "webview:query":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QuerySelector{Window: w, Selector: sel})
case "webview:query-all":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QuerySelectorAll{Window: w, Selector: sel})
case "webview:dom-tree":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, _ := msg.Data["selector"].(string) // selector optional for dom-tree (defaults to root)
result, handled, err = s.Core().QUERY(webview.QueryDOMTree{Window: w, Selector: sel})
case "webview:source":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QueryDOMTree{Window: w})
case "webview:element-info":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QuerySelector{Window: w, Selector: sel})
case "webview:url":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QueryURL{Window: w})
case "webview:title":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QueryTitle{Window: w})
case "webview:devtools-open":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskOpenDevTools{Window: w})
case "webview:devtools-close":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskCloseDevTools{Window: w})
case "webview:errors":
w, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
limit := 0
if l, ok := msg.Data["limit"].(float64); ok {
limit = int(l)
}
result, handled, err = s.Core().QUERY(webview.QueryExceptions{Window: w, Limit: limit})
case "layout:beside-editor":
editor, _ := msg.Data["editor"].(string)
windowName, _ := msg.Data["window"].(string)
result, handled, err = s.Core().PERFORM(window.TaskBesideEditor{
Editor: editor,
Window: windowName,
})
case "layout:stack":
offsetX, _ := msg.Data["offsetX"].(float64)
offsetY, _ := msg.Data["offsetY"].(float64)
var names []string
if raw, ok := msg.Data["windows"].([]any); ok {
for _, v := range raw {
if name, ok := v.(string); ok && name != "" {
names = append(names, name)
}
}
}
result, handled, err = s.Core().PERFORM(window.TaskStackWindows{
Windows: names,
OffsetX: int(offsetX),
OffsetY: int(offsetY),
})
case "layout:workflow":
workflowName, e := wsRequire(msg.Data, "workflow")
if e != nil {
return nil, false, e
}
workflow, ok := window.ParseWorkflowLayout(workflowName)
if !ok {
return nil, false, corego.NewError(corego.Sprintf("ws: unknown workflow %q", workflowName))
}
var names []string
if raw, ok := msg.Data["windows"].([]any); ok {
for _, v := range raw {
if name, ok := v.(string); ok && name != "" {
names = append(names, name)
}
}
}
result, handled, err = s.Core().PERFORM(window.TaskApplyWorkflow{
Workflow: workflow,
Windows: names,
})
case "window:arrange-pair":
first, e := wsRequire(msg.Data, "first")
if e != nil {
return nil, false, e
}
second, e := wsRequire(msg.Data, "second")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(window.TaskArrangePair{
First: first,
Second: second,
})
case "layout:suggest":
windowCount := 0
if count, ok := msg.Data["windowCount"].(float64); ok {
windowCount = int(count)
}
screenWidth := 0
if width, ok := msg.Data["screenWidth"].(float64); ok {
screenWidth = int(width)
}
screenHeight := 0
if height, ok := msg.Data["screenHeight"].(float64); ok {
screenHeight = int(height)
}
if windowCount <= 0 {
windowCount = len(s.ListWindowInfos())
}
if screenWidth <= 0 || screenHeight <= 0 {
screenWidth, screenHeight = s.primaryScreenSize()
}
result, handled, err = s.Core().QUERY(window.QueryLayoutSuggestion{
WindowCount: windowCount,
ScreenWidth: screenWidth,
ScreenHeight: screenHeight,
})
case "screen:find-space":
width := 0
if w, ok := msg.Data["width"].(float64); ok {
width = int(w)
}
height := 0
if h, ok := msg.Data["height"].(float64); ok {
height = int(h)
}
screenWidth, screenHeight := s.primaryScreenSize()
result, handled, err = s.Core().QUERY(window.QueryFindSpace{
Width: width,
Height: height,
ScreenWidth: screenWidth,
ScreenHeight: screenHeight,
})
case "screen:list":
result, handled, err = s.Core().QUERY(screen.QueryAll{})
case "screen:get":
id, e := wsRequire(msg.Data, "id")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(screen.QueryByID{ID: id})
case "screen:primary":
result, handled, err = s.Core().QUERY(screen.QueryPrimary{})
case "screen:at-point":
x, _ := msg.Data["x"].(float64)
y, _ := msg.Data["y"].(float64)
result, handled, err = s.Core().QUERY(screen.QueryAtPoint{X: int(x), Y: int(y)})
case "screen:work-areas":
result, handled, err = s.Core().QUERY(screen.QueryWorkAreas{})
case "screen:for-window":
name, e := wsRequire(msg.Data, "window")
if e != nil {
return nil, false, e
}
screenInfo, screenErr := s.GetScreenForWindow(name)
if screenErr != nil {
return nil, false, screenErr
}
result, handled, err = screenInfo, true, nil
case "clipboard:read":
result, handled, err = s.Core().QUERY(clipboard.QueryText{})
case "clipboard:write":
text, e := wsRequire(msg.Data, "text")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(clipboard.TaskSetText{Text: text})
case "clipboard:has":
textResult, textHandled, textErr := s.Core().QUERY(clipboard.QueryText{})
if textErr != nil {
return nil, false, textErr
}
hasContent := false
if textHandled {
if content, ok := textResult.(clipboard.ClipboardContent); ok {
hasContent = content.HasContent
}
}
if !hasContent {
imageResult, imageHandled, imageErr := s.Core().QUERY(clipboard.QueryImage{})
if imageErr != nil {
return nil, false, imageErr
}
if imageHandled {
if content, ok := imageResult.(clipboard.ClipboardImageContent); ok {
hasContent = content.HasContent
}
}
}
result, handled, err = hasContent, true, nil
case "clipboard:clear":
result, handled, err = s.Core().PERFORM(clipboard.TaskClear{})
case "clipboard:read-image":
result, handled, err = s.Core().QUERY(clipboard.QueryImage{})
case "clipboard:write-image":
data, ok := msg.Data["data"].(string)
if !ok || data == "" {
return nil, false, corego.NewError(corego.Sprintf("ws: missing required field %q", "data"))
}
decoded, decodeErr := base64.StdEncoding.DecodeString(data)
if decodeErr != nil {
return nil, false, corego.Wrap(decodeErr, "display.ws", "ws: invalid base64 image data")
}
result, handled, err = s.Core().PERFORM(clipboard.TaskSetImage{Data: decoded})
case "notification:show":
var opts notification.NotificationOptions
encodedR := corego.JSONMarshal(msg.Data)
if encodedR.OK {
_ = corego.JSONUnmarshal(encodedR.Value.([]byte), &opts)
}
result, handled, err = s.Core().PERFORM(notification.TaskSend{Opts: opts})
case "notification:info":
title, e := wsRequire(msg.Data, "title")
if e != nil {
return nil, false, e
}
message, e := wsRequire(msg.Data, "message")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(notification.TaskSend{Opts: notification.NotificationOptions{
Title: title,
Message: message,
Severity: notification.SeverityInfo,
}})
case "notification:warning":
title, e := wsRequire(msg.Data, "title")
if e != nil {
return nil, false, e
}
message, e := wsRequire(msg.Data, "message")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(notification.TaskSend{Opts: notification.NotificationOptions{
Title: title,
Message: message,
Severity: notification.SeverityWarning,
}})
case "notification:error":
title, e := wsRequire(msg.Data, "title")
if e != nil {
return nil, false, e
}
message, e := wsRequire(msg.Data, "message")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(notification.TaskSend{Opts: notification.NotificationOptions{
Title: title,
Message: message,
Severity: notification.SeverityError,
}})
case "notification:with-actions":
title, e := wsRequire(msg.Data, "title")
if e != nil {
return nil, false, e
}
message, e := wsRequire(msg.Data, "message")
if e != nil {
return nil, false, e
}
subtitle, _ := msg.Data["subtitle"].(string)
actions := make([]notification.NotificationAction, 0)
if raw, ok := msg.Data["actions"]; ok {
encodedR := corego.JSONMarshal(raw)
if encodedR.OK {
_ = corego.JSONUnmarshal(encodedR.Value.([]byte), &actions)
}
}
result, handled, err = s.Core().PERFORM(notification.TaskSend{
Opts: notification.NotificationOptions{
Title: title,
Message: message,
Subtitle: subtitle,
Actions: actions,
},
})
case "notification:clear":
result, handled, err = s.Core().PERFORM(notification.TaskClear{})
case "notification:permission-request":
result, handled, err = s.Core().PERFORM(notification.TaskRequestPermission{})
case "notification:permission-check":
result, handled, err = s.Core().QUERY(notification.QueryPermission{})
case "tray:show-message":
title, e := wsRequire(msg.Data, "title")
if e != nil {
return nil, false, e
}
message, e := wsRequire(msg.Data, "message")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(systray.TaskShowMessage{
Title: title,
Message: message,
})
case "tray:set-tooltip":
tooltip, e := wsRequire(msg.Data, "tooltip")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(systray.TaskSetTooltip{Tooltip: tooltip})
case "tray:set-label":
label, e := wsRequire(msg.Data, "label")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(systray.TaskSetLabel{Label: label})
case "tray:set-icon":
data, e := wsRequire(msg.Data, "data")
if e != nil {
return nil, false, e
}
decoded, decodeErr := base64.StdEncoding.DecodeString(data)
if decodeErr != nil {
return nil, false, corego.Wrap(decodeErr, "display.ws", "ws: invalid base64 tray icon data")
}
result, handled, err = s.Core().PERFORM(systray.TaskSetTrayIcon{Data: decoded})
case "tray:set-menu":
raw, ok := msg.Data["items"]
if !ok {
return nil, false, corego.NewError(corego.Sprintf("ws: missing required field %q", "items"))
}
encodedItemsR := corego.JSONMarshal(raw)
var items []systray.TrayMenuItem
if encodedItemsR.OK {
if r := corego.JSONUnmarshal(encodedItemsR.Value.([]byte), &items); !r.OK {
return nil, false, corego.E("display.ws", "invalid tray menu items", nil)
}
}
result, handled, err = s.Core().PERFORM(systray.TaskSetTrayMenu{Items: items})
case "tray:info":
result, handled, err = s.GetTrayInfo(), true, nil
case "event:info":
result, handled, err = s.GetEventInfo(), true, nil
case "theme:get":
result, handled, err = s.GetTheme(), true, nil
case "theme:system":
result, handled, err = s.GetSystemTheme(), true, nil
case "theme:set":
if theme, ok := msg.Data["theme"].(string); ok && theme != "" {
result, handled, err = nil, true, s.SetThemeMode(theme)
break
}
isDark, _ := msg.Data["isDark"].(bool)
result, handled, err = nil, true, s.SetTheme(isDark)
case "dialog:open-file":
var opts dialog.OpenFileOptions
encodedR := corego.JSONMarshal(msg.Data)
if encodedR.OK {
_ = corego.JSONUnmarshal(encodedR.Value.([]byte), &opts)
}
return nil, false, corego.Wrap(err, "display.ws", "ws: invalid open file options")
paths, openErr := s.OpenFileDialog(opts)
if openErr != nil {
return nil, false, openErr
}
result, handled, err = paths, true, nil
case "dialog:save-file":
var opts dialog.SaveFileOptions
encodedR := corego.JSONMarshal(msg.Data)
if encodedR.OK {
_ = corego.JSONUnmarshal(encodedR.Value.([]byte), &opts)
}
return nil, false, corego.Wrap(err, "display.ws", "ws: invalid save file options")
path, saveErr := s.SaveFileDialog(opts)
if saveErr != nil {
return nil, false, saveErr
}
result, handled, err = path, true, nil
case "dialog:open-directory":
var opts dialog.OpenDirectoryOptions
encodedR := corego.JSONMarshal(msg.Data)
if encodedR.OK {
_ = corego.JSONUnmarshal(encodedR.Value.([]byte), &opts)
}
return nil, false, corego.Wrap(err, "display.ws", "ws: invalid open directory options")
path, dirErr := s.OpenDirectoryDialog(opts)
if dirErr != nil {
return nil, false, dirErr
}
result, handled, err = path, true, nil
case "dialog:confirm":
title, e := wsRequire(msg.Data, "title")
if e != nil {
return nil, false, e
}
message, e := wsRequire(msg.Data, "message")
if e != nil {
return nil, false, e
}
confirmed, confirmErr := s.ConfirmDialog(title, message)
if confirmErr != nil {
return nil, false, confirmErr
}
result, handled, err = confirmed, true, nil
case "dialog:prompt":
title, e := wsRequire(msg.Data, "title")
if e != nil {
return nil, false, e
}
message, e := wsRequire(msg.Data, "message")
if e != nil {
return nil, false, e
}
button, accepted, promptErr := s.PromptDialog(title, message)
if promptErr != nil {
return nil, false, promptErr
}
_ = accepted
result, handled, err = button, true, nil
default:
return nil, false, nil
}
return result, handled, err
}
// handleTrayAction processes tray menu item clicks.
func (s *Service) handleTrayAction(actionID string) {
switch actionID {
case "open-desktop":
// Show all windows
infos := s.ListWindowInfos()
for _, info := range infos {
_, _, _ = s.Core().PERFORM(window.TaskFocus{Name: info.Name})
}
case "close-desktop":
// Hide all tracked windows using the existing visibility task.
infos := s.ListWindowInfos()
for _, info := range infos {
_, _, _ = s.Core().PERFORM(window.TaskSetVisibility{
Name: info.Name,
Visible: false,
})
}
case "env-info":
// Query environment info via IPC and show as dialog
result, handled, _ := s.Core().QUERY(environment.QueryInfo{})
if handled {
info := result.(environment.EnvironmentInfo)
details := corego.Sprintf("OS: %s\nArch: %s\nPlatform: %s %s",
info.OS, info.Arch, info.Platform.Name, info.Platform.Version)
_, _, _ = s.Core().PERFORM(dialog.TaskMessageDialog{
Opts: dialog.MessageDialogOptions{
Type: dialog.DialogInfo, Title: "Environment",
Message: details, Buttons: []string{"OK"},
},
})
}
case "quit":
if s.app != nil {
s.app.Quit()
}
}
}
func guiConfigPath() string {
home, err := os.UserHomeDir()
if err != nil {
return corego.JoinPath(".core", "gui", "config.yaml")
}
return corego.JoinPath(home, ".core", "gui", "config.yaml")
}
func (s *Service) loadConfig() {
if s.cfg != nil {
return // Already loaded (e.g., via loadConfigFrom in tests)
}
s.loadConfigFrom(guiConfigPath())
}
func (s *Service) loadConfigFrom(path string) {
cfg, err := config.New(config.WithPath(path))
if err != nil {
// Non-critical — continue with empty configData
return
}
s.cfg = cfg
for _, section := range []string{"window", "systray", "menu"} {
var data map[string]any
if err := cfg.Get(section, &data); err == nil && data != nil {
s.configData[section] = data
}
}
}
func (s *Service) handleConfigQuery(c *core.Core, q core.Query) (any, bool, error) {
switch q.(type) {
case window.QueryConfig:
return s.configData["window"], true, nil
case systray.QueryConfig:
return s.configData["systray"], true, nil
case menu.QueryConfig:
return s.configData["menu"], true, nil
default:
return nil, false, nil
}
}
func (s *Service) handleConfigTask(c *core.Core, t core.Task) (any, bool, error) {
switch t := t.(type) {
case window.TaskSaveConfig:
s.configData["window"] = t.Value
s.persistSection("window", t.Value)
return nil, true, nil
case systray.TaskSaveConfig:
s.configData["systray"] = t.Value
s.persistSection("systray", t.Value)
return nil, true, nil
case menu.TaskSaveConfig:
s.configData["menu"] = t.Value
s.persistSection("menu", t.Value)
return nil, true, nil
default:
return nil, false, nil
}
}
func (s *Service) persistSection(key string, value map[string]any) {
if s.cfg == nil {
return
}
_ = s.cfg.Set(key, value)
_ = s.cfg.Commit()
}
// --- Service accessors ---
// windowService returns the window service from Core, or nil if not registered.
func (s *Service) windowService() *window.Service {
svc, err := core.ServiceFor[*window.Service](s.Core(), "window")
if err != nil {
return nil
}
return svc
}
// --- Window Management (delegates via IPC) ---
// OpenWindow creates a new window via IPC.
// Use: _ = svc.OpenWindow(window.WithName("editor"), window.WithURL("/editor"))
func (s *Service) OpenWindow(opts ...window.WindowOption) error {
_, _, err := s.Core().PERFORM(window.TaskOpenWindow{Opts: opts})
return err
}
// GetWindowInfo returns information about a window via IPC.
// Use: info, err := svc.GetWindowInfo("editor")
func (s *Service) GetWindowInfo(name string) (*window.WindowInfo, error) {
result, handled, err := s.Core().QUERY(window.QueryWindowByName{Name: name})
if err != nil {
return nil, err
}
if !handled {
return nil, corego.NewError(corego.Sprintf("window service not available"))
}
info, _ := result.(*window.WindowInfo)
return info, nil
}
// ListWindowInfos returns information about all tracked windows via IPC.
// Use: infos := svc.ListWindowInfos()
func (s *Service) ListWindowInfos() []window.WindowInfo {
result, handled, _ := s.Core().QUERY(window.QueryWindowList{})
if !handled {
return nil
}
list, _ := result.([]window.WindowInfo)
return list
}
// SetWindowPosition moves a window via IPC.
// Use: _ = svc.SetWindowPosition("editor", 160, 120)
func (s *Service) SetWindowPosition(name string, x, y int) error {
_, _, err := s.Core().PERFORM(window.TaskSetPosition{Name: name, X: x, Y: y})
return err
}
// SetWindowSize resizes a window via IPC.
// Use: _ = svc.SetWindowSize("editor", 1280, 800)
func (s *Service) SetWindowSize(name string, width, height int) error {
_, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, W: width, H: height})
return err
}
// SetWindowBounds sets both position and size of a window via IPC.
// Use: _ = svc.SetWindowBounds("editor", 160, 120, 1280, 800)
func (s *Service) SetWindowBounds(name string, x, y, width, height int) error {
if _, _, err := s.Core().PERFORM(window.TaskSetPosition{Name: name, X: x, Y: y}); err != nil {
return err
}
_, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, W: width, H: height})
return err
}
// MaximizeWindow maximizes a window via IPC.
// Use: _ = svc.MaximizeWindow("editor")
func (s *Service) MaximizeWindow(name string) error {
_, _, err := s.Core().PERFORM(window.TaskMaximise{Name: name})
return err
}
// MinimizeWindow minimizes a window via IPC.
// Use: _ = svc.MinimizeWindow("editor")
func (s *Service) MinimizeWindow(name string) error {
_, _, err := s.Core().PERFORM(window.TaskMinimise{Name: name})
return err
}
// FocusWindow brings a window to the front via IPC.
// Use: _ = svc.FocusWindow("editor")
func (s *Service) FocusWindow(name string) error {
_, _, err := s.Core().PERFORM(window.TaskFocus{Name: name})
return err
}
// FocusSet is a compatibility alias for FocusWindow.
// Use: _ = svc.FocusSet("editor")
func (s *Service) FocusSet(name string) error {
return s.FocusWindow(name)
}
// CloseWindow closes a window via IPC.
// Use: _ = svc.CloseWindow("editor")
func (s *Service) CloseWindow(name string) error {
_, _, err := s.Core().PERFORM(window.TaskCloseWindow{Name: name})
return err
}
// RestoreWindow restores a maximized/minimized window via IPC.
// Use: _ = svc.RestoreWindow("editor")
func (s *Service) RestoreWindow(name string) error {
_, _, err := s.Core().PERFORM(window.TaskRestore{Name: name})
return err
}
// SetWindowVisibility shows or hides a window via IPC.
// Use: _ = svc.SetWindowVisibility("editor", false)
func (s *Service) SetWindowVisibility(name string, visible bool) error {
_, _, err := s.Core().PERFORM(window.TaskSetVisibility{Name: name, Visible: visible})
return err
}
// SetWindowAlwaysOnTop sets whether a window stays on top via IPC.
// Use: _ = svc.SetWindowAlwaysOnTop("editor", true)
func (s *Service) SetWindowAlwaysOnTop(name string, alwaysOnTop bool) error {
_, _, err := s.Core().PERFORM(window.TaskSetAlwaysOnTop{Name: name, AlwaysOnTop: alwaysOnTop})
return err
}
// SetWindowTitle changes a window's title via IPC.
// Use: _ = svc.SetWindowTitle("editor", "Core Editor")
func (s *Service) SetWindowTitle(name string, title string) error {
_, _, err := s.Core().PERFORM(window.TaskSetTitle{Name: name, Title: title})
return err
}
// SetWindowFullscreen sets a window to fullscreen mode via IPC.
// Use: _ = svc.SetWindowFullscreen("editor", true)
func (s *Service) SetWindowFullscreen(name string, fullscreen bool) error {
_, _, err := s.Core().PERFORM(window.TaskFullscreen{Name: name, Fullscreen: fullscreen})
return err
}
// SetWindowBackgroundColour sets the background colour of a window via IPC.
// Use: _ = svc.SetWindowBackgroundColour("editor", 0, 0, 0, 0)
func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error {
_, _, err := s.Core().PERFORM(window.TaskSetBackgroundColour{
Name: name,
Red: r,
Green: g,
Blue: b,
Alpha: a,
})
return err
}
// SetWindowOpacity updates a window's opacity via IPC.
// Use: _ = svc.SetWindowOpacity("editor", 0.85)
func (s *Service) SetWindowOpacity(name string, opacity float32) error {
_, _, err := s.Core().PERFORM(window.TaskSetOpacity{
Name: name,
Opacity: opacity,
})
return err
}
// ClearWebviewConsole clears the captured console buffer for a window.
// Use: _ = svc.ClearWebviewConsole("editor")
func (s *Service) ClearWebviewConsole(name string) error {
_, _, err := s.Core().PERFORM(webview.TaskClearConsole{Window: name})
return err
}
// GetFocusedWindow returns the name of the currently focused window.
// Use: focused := svc.GetFocusedWindow()
func (s *Service) GetFocusedWindow() string {
infos := s.ListWindowInfos()
for _, info := range infos {
if info.Focused {
return info.Name
}
}
return ""
}
// GetWindowTitle returns the title of a window by name.
// Use: title, err := svc.GetWindowTitle("editor")
func (s *Service) GetWindowTitle(name string) (string, error) {
info, err := s.GetWindowInfo(name)
if err != nil {
return "", err
}
if info == nil {
return "", corego.NewError(corego.Sprintf("window not found: %s", name))
}
return info.Title, nil
}
// ResetWindowState clears saved window positions.
// Use: _ = svc.ResetWindowState()
func (s *Service) ResetWindowState() error {
ws := s.windowService()
if ws != nil {
ws.Manager().State().Clear()
}
return nil
}
// GetSavedWindowStates returns all saved window states.
// Use: states := svc.GetSavedWindowStates()
func (s *Service) GetSavedWindowStates() map[string]window.WindowState {
ws := s.windowService()
if ws == nil {
return nil
}
result := make(map[string]window.WindowState)
for _, name := range ws.Manager().State().ListStates() {
if state, ok := ws.Manager().State().GetState(name); ok {
result[name] = state
}
}
return result
}
// CreateWindowOptions contains options for creating a new window.
// Use: opts := display.CreateWindowOptions{Name: "editor", URL: "/editor"}
type CreateWindowOptions struct {
Name string `json:"name"`
Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"`
X int `json:"x,omitempty"`
Y int `json:"y,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
AlwaysOnTop bool `json:"alwaysOnTop,omitempty"`
BackgroundColour [4]uint8 `json:"backgroundColour,omitempty"`
}
// CreateWindow creates a new window with the specified options.
// Use: info, err := svc.CreateWindow(display.CreateWindowOptions{Name: "editor", URL: "/editor"})
func (s *Service) CreateWindow(opts CreateWindowOptions) (*window.WindowInfo, error) {
if opts.Name == "" {
return nil, corego.NewError(corego.Sprintf("window name is required"))
}
result, _, err := s.Core().PERFORM(window.TaskOpenWindow{
Window: &window.Window{
Name: opts.Name,
Title: opts.Title,
URL: opts.URL,
X: opts.X,
Y: opts.Y,
Width: opts.Width,
Height: opts.Height,
AlwaysOnTop: opts.AlwaysOnTop,
BackgroundColour: opts.BackgroundColour,
},
})
if err != nil {
return nil, err
}
info := result.(window.WindowInfo)
return &info, nil
}
// --- Layout delegation ---
// SaveLayout saves the current window arrangement as a named layout.
// Use: _ = svc.SaveLayout("coding")
func (s *Service) SaveLayout(name string) error {
ws := s.windowService()
if ws == nil {
return corego.NewError(corego.Sprintf("window service not available"))
}
states := make(map[string]window.WindowState)
for _, n := range ws.Manager().List() {
if pw, ok := ws.Manager().Get(n); ok {
x, y := pw.Position()
w, h := pw.Size()
states[n] = window.WindowState{X: x, Y: y, Width: w, Height: h, Maximized: pw.IsMaximised()}
}
}
return ws.Manager().Layout().SaveLayout(name, states)
}
// RestoreLayout applies a saved layout.
// Use: _ = svc.RestoreLayout("coding")
func (s *Service) RestoreLayout(name string) error {
ws := s.windowService()
if ws == nil {
return corego.NewError(corego.Sprintf("window service not available"))
}
layout, ok := ws.Manager().Layout().GetLayout(name)
if !ok {
return corego.NewError(corego.Sprintf("layout not found: %s", name))
}
for wName, state := range layout.Windows {
if pw, ok := ws.Manager().Get(wName); ok {
pw.SetPosition(state.X, state.Y)
pw.SetSize(state.Width, state.Height)
if state.Maximized {
pw.Maximise()
} else {
pw.Restore()
}
}
}
return nil
}
// ListLayouts returns all saved layout names with metadata.
// Use: layouts := svc.ListLayouts()
func (s *Service) ListLayouts() []window.LayoutInfo {
ws := s.windowService()
if ws == nil {
return nil
}
return ws.Manager().Layout().ListLayouts()
}
// DeleteLayout removes a saved layout by name.
// Use: _ = svc.DeleteLayout("coding")
func (s *Service) DeleteLayout(name string) error {
ws := s.windowService()
if ws == nil {
return corego.NewError(corego.Sprintf("window service not available"))
}
ws.Manager().Layout().DeleteLayout(name)
return nil
}
// GetLayout returns a specific layout by name.
// Use: layout := svc.GetLayout("coding")
func (s *Service) GetLayout(name string) *window.Layout {
ws := s.windowService()
if ws == nil {
return nil
}
layout, ok := ws.Manager().Layout().GetLayout(name)
if !ok {
return nil
}
return &layout
}
// --- Tiling/snapping delegation ---
// TileWindows arranges windows in a tiled layout.
// Use: _ = svc.TileWindows(window.TileModeLeftRight, []string{"left", "right"})
func (s *Service) TileWindows(mode window.TileMode, windowNames []string) error {
ws := s.windowService()
if ws == nil {
return corego.NewError(corego.Sprintf("window service not available"))
}
screenWidth, screenHeight := s.primaryScreenSize()
return ws.Manager().TileWindows(mode, windowNames, screenWidth, screenHeight)
}
// SnapWindow snaps a window to a screen edge or corner.
// Use: _ = svc.SnapWindow("editor", window.SnapRight)
func (s *Service) SnapWindow(name string, position window.SnapPosition) error {
ws := s.windowService()
if ws == nil {
return corego.NewError(corego.Sprintf("window service not available"))
}
screenWidth, screenHeight := s.primaryScreenSize()
return ws.Manager().SnapWindow(name, position, screenWidth, screenHeight)
}
func (s *Service) primaryScreenSize() (int, int) {
const fallbackWidth = 1920
const fallbackHeight = 1080
result, handled, err := s.Core().QUERY(screen.QueryPrimary{})
if err != nil || !handled {
return fallbackWidth, fallbackHeight
}
primary, ok := result.(*screen.Screen)
if !ok || primary == nil {
return fallbackWidth, fallbackHeight
}
width := primary.WorkArea.Width
height := primary.WorkArea.Height
if width <= 0 || height <= 0 {
width = primary.Bounds.Width
height = primary.Bounds.Height
}
if width <= 0 || height <= 0 {
return fallbackWidth, fallbackHeight
}
return width, height
}
// StackWindows arranges windows in a cascade pattern.
// Use: _ = svc.StackWindows([]string{"editor", "terminal"}, 24, 24)
func (s *Service) StackWindows(windowNames []string, offsetX, offsetY int) error {
ws := s.windowService()
if ws == nil {
return corego.NewError(corego.Sprintf("window service not available"))
}
return ws.Manager().StackWindows(windowNames, offsetX, offsetY)
}
// ApplyWorkflowLayout applies a predefined layout for a specific workflow.
// Use: _ = svc.ApplyWorkflowLayout(window.WorkflowCoding)
func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error {
ws := s.windowService()
if ws == nil {
return corego.NewError(corego.Sprintf("window service not available"))
}
screenWidth, screenHeight := s.primaryScreenSize()
return ws.Manager().ApplyWorkflow(workflow, ws.Manager().List(), screenWidth, screenHeight)
}
// ArrangeWindowPair places two windows side by side using the window manager's balanced split.
// Use: _ = svc.ArrangeWindowPair("editor", "terminal")
func (s *Service) ArrangeWindowPair(first, second string) error {
ws := s.windowService()
if ws == nil {
return corego.NewError(corego.Sprintf("window service not available"))
}
screenWidth, screenHeight := s.primaryScreenSize()
return ws.Manager().ArrangePair(first, second, screenWidth, screenHeight)
}
// FindSpace returns a free placement suggestion for a new window.
// Use: space, err := svc.FindSpace(800, 600)
func (s *Service) FindSpace(width, height int) (window.SpaceInfo, error) {
ws := s.windowService()
if ws == nil {
return window.SpaceInfo{}, corego.NewError(corego.Sprintf("window service not available"))
}
screenWidth, screenHeight := s.primaryScreenSize()
if width <= 0 {
width = screenWidth / 2
}
if height <= 0 {
height = screenHeight / 2
}
return ws.Manager().FindSpace(screenWidth, screenHeight, width, height), nil
}
// SuggestLayout returns a recommended arrangement for the current screen.
// Use: suggestion, err := svc.SuggestLayout(3, 1920, 1080)
func (s *Service) SuggestLayout(windowCount, screenWidth, screenHeight int) (window.LayoutSuggestion, error) {
result, handled, err := s.Core().QUERY(window.QueryLayoutSuggestion{
WindowCount: windowCount,
ScreenWidth: screenWidth,
ScreenHeight: screenHeight,
})
if err != nil {
return window.LayoutSuggestion{}, err
}
if !handled {
return window.LayoutSuggestion{}, corego.NewError(corego.Sprintf("window service not available"))
}
suggestion, _ := result.(window.LayoutSuggestion)
return suggestion, nil
}
// BesideEditor positions a target window beside an editor window.
// Use: _ = svc.BesideEditor("editor", "assistant")
func (s *Service) BesideEditor(editorName, windowName string) error {
_, _, err := s.Core().PERFORM(window.TaskBesideEditor{
Editor: editorName,
Window: windowName,
})
return err
}
// --- Screen management ---
// GetScreens returns all known screens.
// Use: screens := svc.GetScreens()
func (s *Service) GetScreens() []screen.Screen {
result, handled, _ := s.Core().QUERY(screen.QueryAll{})
if !handled {
return nil
}
screens, _ := result.([]screen.Screen)
return screens
}
// GetScreen returns a screen by ID.
// Use: screenInfo, err := svc.GetScreen("primary")
func (s *Service) GetScreen(id string) (*screen.Screen, error) {
result, handled, err := s.Core().QUERY(screen.QueryByID{ID: id})
if err != nil {
return nil, err
}
if !handled {
return nil, corego.NewError(corego.Sprintf("screen service not available"))
}
scr, _ := result.(*screen.Screen)
return scr, nil
}
// GetPrimaryScreen returns the primary screen.
// Use: primary, err := svc.GetPrimaryScreen()
func (s *Service) GetPrimaryScreen() (*screen.Screen, error) {
result, handled, err := s.Core().QUERY(screen.QueryPrimary{})
if err != nil {
return nil, err
}
if !handled {
return nil, corego.NewError(corego.Sprintf("screen service not available"))
}
scr, _ := result.(*screen.Screen)
return scr, nil
}
// GetScreenAtPoint returns the screen containing the specified point.
// Use: screenInfo, err := svc.GetScreenAtPoint(1280, 720)
func (s *Service) GetScreenAtPoint(x, y int) (*screen.Screen, error) {
result, handled, err := s.Core().QUERY(screen.QueryAtPoint{X: x, Y: y})
if err != nil {
return nil, err
}
if !handled {
return nil, corego.NewError(corego.Sprintf("screen service not available"))
}
scr, _ := result.(*screen.Screen)
return scr, nil
}
// GetScreenForWindow returns the screen containing the named window.
// Use: screenInfo, err := svc.GetScreenForWindow("editor")
func (s *Service) GetScreenForWindow(name string) (*screen.Screen, error) {
info, err := s.GetWindowInfo(name)
if err != nil {
return nil, err
}
if info == nil {
return nil, nil
}
x := info.X
y := info.Y
if info.Width > 0 && info.Height > 0 {
x += info.Width / 2
y += info.Height / 2
}
return s.GetScreenAtPoint(x, y)
}
// GetWorkAreas returns the usable area of every screen.
// Use: areas := svc.GetWorkAreas()
func (s *Service) GetWorkAreas() []screen.Rect {
result, handled, _ := s.Core().QUERY(screen.QueryWorkAreas{})
if !handled {
return nil
}
areas, _ := result.([]screen.Rect)
return areas
}
// --- Clipboard ---
// ReadClipboard returns the current clipboard text content.
// Use: text, err := svc.ReadClipboard()
func (s *Service) ReadClipboard() (string, error) {
result, handled, err := s.Core().QUERY(clipboard.QueryText{})
if err != nil {
return "", err
}
if !handled {
return "", core.E("display.ReadClipboard", "clipboard service not available", nil)
}
content, _ := result.(clipboard.ClipboardContent)
return content.Text, nil
}
// WriteClipboard writes text to the clipboard.
// Use: _ = svc.WriteClipboard("updated")
func (s *Service) WriteClipboard(text string) error {
result, handled, err := s.Core().PERFORM(clipboard.TaskSetText{Text: text})
if err != nil {
return err
}
if !handled {
return core.E("display.WriteClipboard", "clipboard service not available", nil)
}
if ok, _ := result.(bool); !ok {
return core.E("display.WriteClipboard", "clipboard write failed", nil)
}
return nil
}
// HasClipboard reports whether the clipboard has text or image content.
// Use: hasContent := svc.HasClipboard()
func (s *Service) HasClipboard() bool {
textResult, textHandled, _ := s.Core().QUERY(clipboard.QueryText{})
if textHandled {
if content, ok := textResult.(clipboard.ClipboardContent); ok && content.HasContent {
return true
}
}
imageResult, imageHandled, _ := s.Core().QUERY(clipboard.QueryImage{})
if imageHandled {
if content, ok := imageResult.(clipboard.ClipboardImageContent); ok && content.HasContent {
return true
}
}
return false
}
// ClearClipboard clears clipboard text and any image data when supported.
// Use: _ = svc.ClearClipboard()
func (s *Service) ClearClipboard() error {
result, handled, err := s.Core().PERFORM(clipboard.TaskClear{})
if err != nil {
return err
}
if !handled {
return core.E("display.ClearClipboard", "clipboard service not available", nil)
}
if ok, _ := result.(bool); !ok {
return core.E("display.ClearClipboard", "clipboard clear failed", nil)
}
return nil
}
// ReadClipboardImage returns the clipboard image content.
// Use: image, err := svc.ReadClipboardImage()
func (s *Service) ReadClipboardImage() (clipboard.ClipboardImageContent, error) {
result, handled, err := s.Core().QUERY(clipboard.QueryImage{})
if err != nil {
return clipboard.ClipboardImageContent{}, err
}
if !handled {
return clipboard.ClipboardImageContent{}, core.E("display.ReadClipboardImage", "clipboard service not available", nil)
}
content, _ := result.(clipboard.ClipboardImageContent)
return content, nil
}
// WriteClipboardImage writes raw image data to the clipboard.
// Use: _ = svc.WriteClipboardImage(data)
func (s *Service) WriteClipboardImage(data []byte) error {
result, handled, err := s.Core().PERFORM(clipboard.TaskSetImage{Data: data})
if err != nil {
return err
}
if !handled {
return core.E("display.WriteClipboardImage", "clipboard service not available", nil)
}
if ok, _ := result.(bool); !ok {
return core.E("display.WriteClipboardImage", "clipboard image write failed", nil)
}
return nil
}
// --- Notifications ---
// ShowNotification sends a native notification.
// Use: _ = svc.ShowNotification(notification.NotificationOptions{Title: "Build complete", Message: "All checks passed"})
func (s *Service) ShowNotification(opts notification.NotificationOptions) error {
_, handled, err := s.Core().PERFORM(notification.TaskSend{Opts: opts})
if err != nil {
return err
}
if !handled {
return core.E("display.ShowNotification", "notification service not available", nil)
}
return nil
}
// ShowInfoNotification sends an informational notification.
// Use: _ = svc.ShowInfoNotification("Build complete", "All checks passed")
func (s *Service) ShowInfoNotification(title, message string) error {
return s.ShowNotification(notification.NotificationOptions{
Title: title,
Message: message,
Severity: notification.SeverityInfo,
})
}
// ShowWarningNotification sends a warning notification.
// Use: _ = svc.ShowWarningNotification("Build warning", "Tests are flaky")
func (s *Service) ShowWarningNotification(title, message string) error {
return s.ShowNotification(notification.NotificationOptions{
Title: title,
Message: message,
Severity: notification.SeverityWarning,
})
}
// ShowErrorNotification sends an error notification.
// Use: _ = svc.ShowErrorNotification("Build failed", "See the log output")
func (s *Service) ShowErrorNotification(title, message string) error {
return s.ShowNotification(notification.NotificationOptions{
Title: title,
Message: message,
Severity: notification.SeverityError,
})
}
// RequestNotificationPermission requests notification permission.
// Use: granted, err := svc.RequestNotificationPermission()
func (s *Service) RequestNotificationPermission() (bool, error) {
result, handled, err := s.Core().PERFORM(notification.TaskRequestPermission{})
if err != nil {
return false, err
}
if !handled {
return false, corego.NewError(corego.Sprintf("notification service not available"))
}
granted, _ := result.(bool)
return granted, nil
}
// CheckNotificationPermission checks notification permission.
// Use: granted, err := svc.CheckNotificationPermission()
func (s *Service) CheckNotificationPermission() (bool, error) {
result, handled, err := s.Core().QUERY(notification.QueryPermission{})
if err != nil {
return false, err
}
if !handled {
return false, corego.NewError(corego.Sprintf("notification service not available"))
}
status, _ := result.(notification.PermissionStatus)
return status.Granted, nil
}
// ClearNotifications clears notifications when supported.
// Use: _ = svc.ClearNotifications()
func (s *Service) ClearNotifications() error {
_, handled, err := s.Core().PERFORM(notification.TaskClear{})
if err != nil {
return err
}
if !handled {
return corego.NewError(corego.Sprintf("notification service not available"))
}
return nil
}
// --- Dialogs ---
// OpenFileDialog opens a file picker and returns all selected paths.
// Use: paths, err := svc.OpenFileDialog(dialog.OpenFileOptions{Title: "Open report"})
func (s *Service) OpenFileDialog(opts dialog.OpenFileOptions) ([]string, error) {
result, handled, err := s.Core().PERFORM(dialog.TaskOpenFile{Opts: opts})
if err != nil {
return nil, err
}
if !handled {
return nil, corego.NewError(corego.Sprintf("dialog service not available"))
}
paths, _ := result.([]string)
return paths, nil
}
// OpenSingleFileDialog opens a file picker and returns the first selected path.
// Use: path, err := svc.OpenSingleFileDialog(dialog.OpenFileOptions{Title: "Open report"})
func (s *Service) OpenSingleFileDialog(opts dialog.OpenFileOptions) (string, error) {
paths, err := s.OpenFileDialog(opts)
if err != nil {
return "", err
}
if len(paths) == 0 {
return "", nil
}
return paths[0], nil
}
// SaveFileDialog opens a save dialog and returns the selected path.
// Use: path, err := svc.SaveFileDialog(dialog.SaveFileOptions{Title: "Export report"})
func (s *Service) SaveFileDialog(opts dialog.SaveFileOptions) (string, error) {
result, handled, err := s.Core().PERFORM(dialog.TaskSaveFile{Opts: opts})
if err != nil {
return "", err
}
if !handled {
return "", corego.NewError(corego.Sprintf("dialog service not available"))
}
path, _ := result.(string)
return path, nil
}
// OpenDirectoryDialog opens a directory picker and returns the selected path.
// Use: path, err := svc.OpenDirectoryDialog(dialog.OpenDirectoryOptions{Title: "Choose workspace"})
func (s *Service) OpenDirectoryDialog(opts dialog.OpenDirectoryOptions) (string, error) {
result, handled, err := s.Core().PERFORM(dialog.TaskOpenDirectory{Opts: opts})
if err != nil {
return "", err
}
if !handled {
return "", corego.NewError(corego.Sprintf("dialog service not available"))
}
path, _ := result.(string)
return path, nil
}
// ConfirmDialog shows a confirmation prompt.
// Use: confirmed, err := svc.ConfirmDialog("Delete file", "Remove report.txt?")
func (s *Service) ConfirmDialog(title, message string) (bool, error) {
result, handled, err := s.Core().PERFORM(dialog.TaskMessageDialog{
Opts: dialog.MessageDialogOptions{
Type: dialog.DialogQuestion,
Title: title,
Message: message,
Buttons: []string{"Yes", "No"},
},
})
if err != nil {
return false, err
}
if !handled {
return false, corego.NewError(corego.Sprintf("dialog service not available"))
}
button, _ := result.(string)
return button == "Yes" || button == "OK", nil
}
// PromptDialog shows a prompt-style dialog and returns entered text when the webview
// prompt path is available, otherwise it falls back to a button-based message dialog.
// Use: value, accepted, err := svc.PromptDialog("Rename file", "Enter a new name")
func (s *Service) PromptDialog(title, message string) (string, bool, error) {
if text, ok, err := s.promptViaWebView(title, message); err == nil {
if ok {
return text, true, nil
}
return "", false, nil
}
// Fall back to the native message dialog path when no webview prompt is available.
// The returned error is intentionally ignored unless the fallback also fails.
result, handled, err := s.Core().PERFORM(dialog.TaskMessageDialog{
Opts: dialog.MessageDialogOptions{
Type: dialog.DialogInfo,
Title: title,
Message: message,
Buttons: []string{"OK", "Cancel"},
},
})
if err != nil {
return "", false, err
}
if !handled {
return "", false, corego.NewError(corego.Sprintf("dialog service not available"))
}
button, _ := result.(string)
return button, button == "OK", nil
}
func (s *Service) promptViaWebView(title, message string) (string, bool, error) {
windowName := s.GetFocusedWindow()
if windowName == "" {
infos := s.ListWindowInfos()
if len(infos) > 0 {
windowName = infos[0].Name
}
}
if windowName == "" {
return "", false, corego.NewError(corego.Sprintf("no webview window available"))
}
encodedTitleR := corego.JSONMarshal(title)
if !encodedTitleR.OK {
return "", false, corego.E("display.showDialog", "failed to marshal title", nil)
}
encodedMessageR := corego.JSONMarshal(message)
if !encodedMessageR.OK {
return "", false, corego.E("display.showDialog", "failed to marshal message", nil)
}
result, handled, err := s.Core().PERFORM(webview.TaskEvaluate{
Window: windowName,
Script: "window.prompt(" + string(encodedTitleR.Value.([]byte)) + "," + string(encodedMessageR.Value.([]byte)) + ")",
})
if err != nil {
return "", false, err
}
if !handled {
return "", false, corego.NewError(corego.Sprintf("webview service not available"))
}
if result == nil {
return "", false, nil
}
if text, ok := result.(string); ok {
return text, true, nil
}
return corego.Sprint(result), true, nil
}
// DialogMessage shows an informational, warning, or error message via the notification pipeline.
// Use: _ = svc.DialogMessage("warning", "Build failed", "Check the log output")
func (s *Service) DialogMessage(kind, title, message string) error {
var severity notification.NotificationSeverity
switch kind {
case "warning":
severity = notification.SeverityWarning
case "error":
severity = notification.SeverityError
default:
severity = notification.SeverityInfo
}
_, _, err := s.Core().PERFORM(notification.TaskSend{
Opts: notification.NotificationOptions{
Title: title,
Message: message,
Severity: severity,
},
})
return err
}
// --- Theme ---
// GetTheme returns the current theme state.
// Use: theme := svc.GetTheme()
func (s *Service) GetTheme() *Theme {
result, handled, err := s.Core().QUERY(environment.QueryTheme{})
if err != nil || !handled {
return nil
}
theme, ok := result.(environment.ThemeInfo)
if !ok {
return nil
}
return &Theme{IsDark: theme.IsDark}
}
// GetSystemTheme returns the current system theme preference.
// Use: theme := svc.GetSystemTheme()
func (s *Service) GetSystemTheme() string {
result, handled, err := s.Core().QUERY(environment.QueryTheme{})
if err != nil || !handled {
return ""
}
theme, ok := result.(environment.ThemeInfo)
if !ok {
return ""
}
if theme.IsDark {
return "dark"
}
return "light"
}
// SetTheme overrides the application theme.
// Use: _ = svc.SetTheme(true)
func (s *Service) SetTheme(isDark bool) error {
if isDark {
return s.SetThemeMode("dark")
}
return s.SetThemeMode("light")
}
// SetThemeMode overrides the application theme using a declarative mode string.
// Use: _ = svc.SetThemeMode("system")
func (s *Service) SetThemeMode(theme string) error {
_, handled, err := s.Core().PERFORM(environment.TaskSetTheme{Theme: theme})
if err != nil {
return err
}
if !handled {
return corego.NewError(corego.Sprintf("environment service not available"))
}
return nil
}
// --- Tray ---
// SetTrayIcon sets the tray icon image.
// Use: _ = svc.SetTrayIcon(iconBytes)
func (s *Service) SetTrayIcon(data []byte) error {
_, handled, err := s.Core().PERFORM(systray.TaskSetTrayIcon{Data: data})
if err != nil {
return err
}
if !handled {
return corego.NewError(corego.Sprintf("systray service not available"))
}
return nil
}
// SetTrayTooltip updates the tray tooltip.
// Use: _ = svc.SetTrayTooltip("Core is ready")
func (s *Service) SetTrayTooltip(tooltip string) error {
_, handled, err := s.Core().PERFORM(systray.TaskSetTooltip{Tooltip: tooltip})
if err != nil {
return err
}
if !handled {
return corego.NewError(corego.Sprintf("systray service not available"))
}
return nil
}
// SetTrayLabel updates the tray label.
// Use: _ = svc.SetTrayLabel("Core")
func (s *Service) SetTrayLabel(label string) error {
_, handled, err := s.Core().PERFORM(systray.TaskSetLabel{Label: label})
if err != nil {
return err
}
if !handled {
return corego.NewError(corego.Sprintf("systray service not available"))
}
return nil
}
// SetTrayMenu replaces the tray menu items.
// Use: _ = svc.SetTrayMenu([]systray.TrayMenuItem{{Label: "Quit", ActionID: "quit"}})
func (s *Service) SetTrayMenu(items []systray.TrayMenuItem) error {
_, handled, err := s.Core().PERFORM(systray.TaskSetTrayMenu{Items: items})
if err != nil {
return err
}
if !handled {
return corego.NewError(corego.Sprintf("systray service not available"))
}
return nil
}
// GetTrayInfo returns current tray state information.
// Use: info := svc.GetTrayInfo()
func (s *Service) GetTrayInfo() map[string]any {
trayService, err := core.ServiceFor[*systray.Service](s.Core(), "systray")
if err != nil || trayService == nil || trayService.Manager() == nil {
return nil
}
return trayService.Manager().GetInfo()
}
// ShowTrayMessage shows a tray message or notification.
// Use: _ = svc.ShowTrayMessage("Core", "Sync complete")
func (s *Service) ShowTrayMessage(title, message string) error {
_, handled, err := s.Core().PERFORM(systray.TaskShowMessage{Title: title, Message: message})
if err != nil {
return err
}
if !handled {
return corego.NewError(corego.Sprintf("systray service not available"))
}
return nil
}
// GetEventManager returns the event manager for WebSocket event subscriptions.
// Use: events := svc.GetEventManager()
func (s *Service) GetEventManager() *WSEventManager {
return s.events
}
// GetEventInfo returns a summary of the live WebSocket event server state.
// Use: info := svc.GetEventInfo()
func (s *Service) GetEventInfo() EventServerInfo {
if s.events == nil {
return EventServerInfo{}
}
return s.events.Info()
}
// --- Menu (handlers stay in display, structure delegated via IPC) ---
func (s *Service) buildMenu() {
items := []menu.MenuItem{
{Role: ptr(menu.RoleAppMenu)},
{Role: ptr(menu.RoleFileMenu)},
{Role: ptr(menu.RoleViewMenu)},
{Role: ptr(menu.RoleEditMenu)},
{Label: "Workspace", Children: []menu.MenuItem{
{Label: "New...", OnClick: s.handleNewWorkspace},
{Label: "List", OnClick: s.handleListWorkspaces},
}},
{Label: "Developer", Children: []menu.MenuItem{
{Label: "New File", Accelerator: "CmdOrCtrl+N", OnClick: s.handleNewFile},
{Label: "Open File...", Accelerator: "CmdOrCtrl+O", OnClick: s.handleOpenFile},
{Label: "Save", Accelerator: "CmdOrCtrl+S", OnClick: s.handleSaveFile},
{Type: "separator"},
{Label: "Editor", OnClick: s.handleOpenEditor},
{Label: "Terminal", OnClick: s.handleOpenTerminal},
{Type: "separator"},
{Label: "Run", Accelerator: "CmdOrCtrl+R", OnClick: s.handleRun},
{Label: "Build", Accelerator: "CmdOrCtrl+B", OnClick: s.handleBuild},
}},
{Role: ptr(menu.RoleWindowMenu)},
{Role: ptr(menu.RoleHelpMenu)},
}
// On non-macOS, remove the AppMenu role
if runtime.GOOS != "darwin" {
items = items[1:] // skip AppMenu
}
_, _, _ = s.Core().PERFORM(menu.TaskSetAppMenu{Items: items})
}
func ptr[T any](v T) *T { return &v }
// --- Menu handler methods ---
func (s *Service) handleNewWorkspace() {
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
Opts: []window.WindowOption{
window.WithName("workspace-new"),
window.WithTitle("New Workspace"),
window.WithURL("/workspace/new"),
window.WithSize(500, 400),
},
})
}
func (s *Service) handleListWorkspaces() {
ws := s.Core().Service("workspace")
if ws == nil {
return
}
lister, ok := ws.(interface{ ListWorkspaces() []string })
if !ok {
return
}
_ = lister.ListWorkspaces()
}
func (s *Service) handleNewFile() {
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
Opts: []window.WindowOption{
window.WithName("editor"),
window.WithTitle("New File - Editor"),
window.WithURL("/#/developer/editor?new=true"),
window.WithSize(1200, 800),
},
})
}
func (s *Service) handleOpenFile() {
result, handled, err := s.Core().PERFORM(dialog.TaskOpenFile{
Opts: dialog.OpenFileOptions{
Title: "Open File",
AllowMultiple: false,
},
})
if err != nil || !handled {
return
}
paths, ok := result.([]string)
if !ok || len(paths) == 0 {
return
}
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
Opts: []window.WindowOption{
window.WithName("editor"),
window.WithTitle(paths[0] + " - Editor"),
window.WithURL("/#/developer/editor?file=" + paths[0]),
window.WithSize(1200, 800),
},
})
}
func (s *Service) handleSaveFile() { _ = s.Core().ACTION(ActionIDECommand{Command: "save"}) }
func (s *Service) handleOpenEditor() {
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
Opts: []window.WindowOption{
window.WithName("editor"),
window.WithTitle("Editor"),
window.WithURL("/#/developer/editor"),
window.WithSize(1200, 800),
},
})
}
func (s *Service) handleOpenTerminal() {
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
Opts: []window.WindowOption{
window.WithName("terminal"),
window.WithTitle("Terminal"),
window.WithURL("/#/developer/terminal"),
window.WithSize(800, 500),
},
})
}
func (s *Service) handleRun() { _ = s.Core().ACTION(ActionIDECommand{Command: "run"}) }
func (s *Service) handleBuild() { _ = s.Core().ACTION(ActionIDECommand{Command: "build"}) }
// --- Tray (setup delegated via IPC) ---
func (s *Service) setupTray() {
_, _, _ = s.Core().PERFORM(systray.TaskSetTrayMenu{Items: []systray.TrayMenuItem{
{Label: "Open Desktop", ActionID: "open-desktop"},
{Label: "Close Desktop", ActionID: "close-desktop"},
{Type: "separator"},
{Label: "Environment Info", ActionID: "env-info"},
{Type: "separator"},
{Label: "Quit", ActionID: "quit"},
}})
}