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>
2420 lines
73 KiB
Go
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"},
|
|
}})
|
|
}
|