package display import ( "context" "fmt" "os" "path/filepath" "runtime" "forge.lthn.ai/core/go-config" "forge.lthn.ai/core/go/pkg/core" "forge.lthn.ai/core/gui/pkg/menu" "forge.lthn.ai/core/gui/pkg/systray" "forge.lthn.ai/core/gui/pkg/window" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/services/notifications" ) // Options holds configuration for the display service. type Options struct{} // WindowInfo is an alias for window.WindowInfo (backward compatibility). type WindowInfo = window.WindowInfo // 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. type Service struct { *core.ServiceRuntime[Options] wailsApp *application.App app App config Options configData map[string]map[string]any cfg *config.Config // go-config instance for file persistence notifier *notifications.NotificationService events *WSEventManager } // New is the constructor for the display service. func New() (*Service, error) { return &Service{ configData: map[string]map[string]any{ "window": {}, "systray": {}, "menu": {}, }, }, nil } // Register creates a factory closure that captures the Wails 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 := New() 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. 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(newWailsEventSource(s.wailsApp)) s.events.SetupWindowEventListeners() } return nil } // HandleIPCEvents is auto-discovered and registered by core.WithService. // It bridges sub-service IPC actions to WebSocket events for TS apps. 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) } return nil } // 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 windows — future: add TaskHideWindow case "env-info": if s.app != nil { s.ShowEnvironmentDialog() } case "quit": if s.app != nil { s.app.Quit() } } } func guiConfigPath() string { home, err := os.UserHomeDir() if err != nil { return filepath.Join(".core", "gui", "config.yaml") } return filepath.Join(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. 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. 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, fmt.Errorf("window service not available") } info, _ := result.(*window.WindowInfo) return info, nil } // ListWindowInfos returns information about all tracked windows via IPC. 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. 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. 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. 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. func (s *Service) MaximizeWindow(name string) error { _, _, err := s.Core().PERFORM(window.TaskMaximise{Name: name}) return err } // MinimizeWindow minimizes a window via IPC. 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. func (s *Service) FocusWindow(name string) error { _, _, err := s.Core().PERFORM(window.TaskFocus{Name: name}) return err } // CloseWindow closes a window via IPC. func (s *Service) CloseWindow(name string) error { _, _, err := s.Core().PERFORM(window.TaskCloseWindow{Name: name}) return err } // RestoreWindow restores a maximized/minimized window. // Uses direct Manager access (no IPC task for restore yet). func (s *Service) RestoreWindow(name string) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } pw.Restore() return nil } // SetWindowVisibility shows or hides a window. // Uses direct Manager access (no IPC task for visibility yet). func (s *Service) SetWindowVisibility(name string, visible bool) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } pw.SetVisibility(visible) return nil } // SetWindowAlwaysOnTop sets whether a window stays on top. // Uses direct Manager access (no IPC task for always-on-top yet). func (s *Service) SetWindowAlwaysOnTop(name string, alwaysOnTop bool) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } pw.SetAlwaysOnTop(alwaysOnTop) return nil } // SetWindowTitle changes a window's title. // Uses direct Manager access (no IPC task for title yet). func (s *Service) SetWindowTitle(name string, title string) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } pw.SetTitle(title) return nil } // SetWindowFullscreen sets a window to fullscreen mode. // Uses direct Manager access (no IPC task for fullscreen yet). func (s *Service) SetWindowFullscreen(name string, fullscreen bool) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } if fullscreen { pw.Fullscreen() } else { pw.UnFullscreen() } return nil } // SetWindowBackgroundColour sets the background colour of a window. // Uses direct Manager access (no IPC task for background colour yet). func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } pw.SetBackgroundColour(r, g, b, a) return nil } // GetFocusedWindow returns the name of the currently focused window. 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. func (s *Service) GetWindowTitle(name string) (string, error) { info, err := s.GetWindowInfo(name) if err != nil { return "", err } if info == nil { return "", fmt.Errorf("window not found: %s", name) } return info.Name, nil // Wails v3 doesn't expose a title getter } // ResetWindowState clears saved window positions. func (s *Service) ResetWindowState() error { ws := s.windowService() if ws != nil { ws.Manager().State().Clear() } return nil } // GetSavedWindowStates returns all saved window states. 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. 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"` } // CreateWindow creates a new window with the specified options. func (s *Service) CreateWindow(opts CreateWindowOptions) (*window.WindowInfo, error) { if opts.Name == "" { return nil, fmt.Errorf("window name is required") } result, _, err := s.Core().PERFORM(window.TaskOpenWindow{ Opts: []window.WindowOption{ window.WithName(opts.Name), window.WithTitle(opts.Title), window.WithURL(opts.URL), window.WithSize(opts.Width, opts.Height), window.WithPosition(opts.X, opts.Y), }, }) 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. func (s *Service) SaveLayout(name string) error { ws := s.windowService() if ws == nil { return fmt.Errorf("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. func (s *Service) RestoreLayout(name string) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } layout, ok := ws.Manager().Layout().GetLayout(name) if !ok { return fmt.Errorf("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. 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. func (s *Service) DeleteLayout(name string) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } ws.Manager().Layout().DeleteLayout(name) return nil } // GetLayout returns a specific layout by name. 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. func (s *Service) TileWindows(mode window.TileMode, windowNames []string) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } return ws.Manager().TileWindows(mode, windowNames, 1920, 1080) // TODO: use actual screen size } // SnapWindow snaps a window to a screen edge or corner. func (s *Service) SnapWindow(name string, position window.SnapPosition) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } return ws.Manager().SnapWindow(name, position, 1920, 1080) // TODO: use actual screen size } // StackWindows arranges windows in a cascade pattern. func (s *Service) StackWindows(windowNames []string, offsetX, offsetY int) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } return ws.Manager().StackWindows(windowNames, offsetX, offsetY) } // ApplyWorkflowLayout applies a predefined layout for a specific workflow. func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error { ws := s.windowService() if ws == nil { return fmt.Errorf("window service not available") } return ws.Manager().ApplyWorkflow(workflow, ws.Manager().List(), 1920, 1080) } // --- Screen queries (remain in display — use application.Get() directly) --- // ScreenInfo contains information about a display screen. type ScreenInfo struct { ID string `json:"id"` Name string `json:"name"` X int `json:"x"` Y int `json:"y"` Width int `json:"width"` Height int `json:"height"` Primary bool `json:"primary"` } // WorkArea represents usable screen space. type WorkArea struct { ScreenID string `json:"screenId"` X int `json:"x"` Y int `json:"y"` Width int `json:"width"` Height int `json:"height"` } // GetScreens returns information about all available screens. func (s *Service) GetScreens() []ScreenInfo { app := application.Get() if app == nil || app.Screen == nil { return nil } screens := app.Screen.GetAll() if screens == nil { return nil } result := make([]ScreenInfo, 0, len(screens)) for _, screen := range screens { result = append(result, ScreenInfo{ ID: screen.ID, Name: screen.Name, X: screen.Bounds.X, Y: screen.Bounds.Y, Width: screen.Bounds.Width, Height: screen.Bounds.Height, Primary: screen.IsPrimary, }) } return result } // GetWorkAreas returns the usable work area for all screens. func (s *Service) GetWorkAreas() []WorkArea { app := application.Get() if app == nil || app.Screen == nil { return nil } screens := app.Screen.GetAll() if screens == nil { return nil } result := make([]WorkArea, 0, len(screens)) for _, screen := range screens { result = append(result, WorkArea{ ScreenID: screen.ID, X: screen.WorkArea.X, Y: screen.WorkArea.Y, Width: screen.WorkArea.Width, Height: screen.WorkArea.Height, }) } return result } // GetPrimaryScreen returns information about the primary screen. func (s *Service) GetPrimaryScreen() (*ScreenInfo, error) { app := application.Get() if app == nil || app.Screen == nil { return nil, fmt.Errorf("screen service not available") } screens := app.Screen.GetAll() for _, screen := range screens { if screen.IsPrimary { return &ScreenInfo{ ID: screen.ID, Name: screen.Name, X: screen.Bounds.X, Y: screen.Bounds.Y, Width: screen.Bounds.Width, Height: screen.Bounds.Height, Primary: true, }, nil } } return nil, fmt.Errorf("no primary screen found") } // GetScreen returns information about a specific screen by ID. func (s *Service) GetScreen(id string) (*ScreenInfo, error) { app := application.Get() if app == nil || app.Screen == nil { return nil, fmt.Errorf("screen service not available") } screens := app.Screen.GetAll() for _, screen := range screens { if screen.ID == id { return &ScreenInfo{ ID: screen.ID, Name: screen.Name, X: screen.Bounds.X, Y: screen.Bounds.Y, Width: screen.Bounds.Width, Height: screen.Bounds.Height, Primary: screen.IsPrimary, }, nil } } return nil, fmt.Errorf("screen not found: %s", id) } // GetScreenAtPoint returns the screen containing a specific point. func (s *Service) GetScreenAtPoint(x, y int) (*ScreenInfo, error) { app := application.Get() if app == nil || app.Screen == nil { return nil, fmt.Errorf("screen service not available") } screens := app.Screen.GetAll() for _, screen := range screens { bounds := screen.Bounds if x >= bounds.X && x < bounds.X+bounds.Width && y >= bounds.Y && y < bounds.Y+bounds.Height { return &ScreenInfo{ ID: screen.ID, Name: screen.Name, X: bounds.X, Y: bounds.Y, Width: bounds.Width, Height: bounds.Height, Primary: screen.IsPrimary, }, nil } } return nil, fmt.Errorf("no screen found at point (%d, %d)", x, y) } // GetScreenForWindow returns the screen containing a specific window. func (s *Service) GetScreenForWindow(name string) (*ScreenInfo, error) { info, err := s.GetWindowInfo(name) if err != nil { return nil, err } centerX := info.X + info.Width/2 centerY := info.Y + info.Height/2 return s.GetScreenAtPoint(centerX, centerY) } // ShowEnvironmentDialog displays environment information. func (s *Service) ShowEnvironmentDialog() { envInfo := s.app.Env().Info() details := "Environment Information:\n\n" details += fmt.Sprintf("Operating System: %s\n", envInfo.OS) details += fmt.Sprintf("Architecture: %s\n", envInfo.Arch) details += fmt.Sprintf("Debug Mode: %t\n\n", envInfo.Debug) details += fmt.Sprintf("Dark Mode: %t\n\n", s.app.Env().IsDarkMode()) details += "Platform Information:" for key, value := range envInfo.PlatformInfo { details += fmt.Sprintf("\n%s: %v", key, value) } if envInfo.OSInfo != nil { details += fmt.Sprintf("\n\nOS Details:\nName: %s\nVersion: %s", envInfo.OSInfo.Name, envInfo.OSInfo.Version) } dialog := s.app.Dialog().Info() dialog.SetTitle("Environment Information") dialog.SetMessage(details) dialog.Show() } // GetEventManager returns the event manager for WebSocket event subscriptions. func (s *Service) GetEventManager() *WSEventManager { return s.events } // --- 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() { dialog := s.app.Dialog().OpenFile() dialog.SetTitle("Open File") dialog.CanChooseFiles(true) dialog.CanChooseDirectories(false) result, err := dialog.PromptForSingleSelection() if err != nil || result == "" { return } _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ Opts: []window.WindowOption{ window.WithName("editor"), window.WithTitle(result + " - Editor"), window.WithURL("/#/developer/editor?file=" + result), window.WithSize(1200, 800), }, }) } func (s *Service) handleSaveFile() { s.app.Event().Emit("ide: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.app.Event().Emit("ide:run") } func (s *Service) handleBuild() { s.app.Event().Emit("ide: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"}, }}) }