package window import ( "context" coreerr "dappco.re/go/core/log" core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/screen" ) type Options struct{} type Service struct { *core.ServiceRuntime[Options] manager *Manager platform Platform } func (s *Service) OnStartup(_ context.Context) core.Result { // Query config — display registers its handler before us (registration order guarantee). // If display is not registered, OK=false and we skip config. r := s.Core().QUERY(QueryConfig{}) if r.OK { if windowConfig, ok := r.Value.(map[string]any); ok { s.applyConfig(windowConfig) } } s.Core().RegisterQuery(s.handleQuery) s.registerTaskActions() return core.Result{OK: true} } func (s *Service) applyConfig(configData map[string]any) { if width, ok := configData["default_width"]; ok { if width, ok := width.(int); ok { s.manager.SetDefaultWidth(width) } } if height, ok := configData["default_height"]; ok { if height, ok := height.(int); ok { s.manager.SetDefaultHeight(height) } } if stateFile, ok := configData["state_file"]; ok { if stateFile, ok := stateFile.(string); ok { s.manager.State().SetPath(stateFile) } } } func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { return core.Result{OK: true} } func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q := q.(type) { case QueryWindowList: return core.Result{Value: s.queryWindowList(), OK: true} case QueryWindowByName: return core.Result{Value: s.queryWindowByName(q.Name), OK: true} case QueryLayoutList: return core.Result{Value: s.manager.Layout().ListLayouts(), OK: true} case QueryLayoutGet: l, ok := s.manager.Layout().GetLayout(q.Name) if !ok { return core.Result{Value: (*Layout)(nil), OK: true} } return core.Result{Value: &l, OK: true} case QueryWindowZoom: return s.queryWindowZoom(q.Name) case QueryWindowBounds: return s.queryWindowBounds(q.Name) default: return core.Result{} } } func (s *Service) queryWindowList() []WindowInfo { names := s.manager.List() result := make([]WindowInfo, 0, len(names)) for _, name := range names { if pw, ok := s.manager.Get(name); ok { x, y := pw.Position() w, h := pw.Size() result = append(result, WindowInfo{ Name: name, Title: pw.Title(), X: x, Y: y, Width: w, Height: h, Maximized: pw.IsMaximised(), Focused: pw.IsFocused(), }) } } return result } func (s *Service) queryWindowByName(name string) *WindowInfo { pw, ok := s.manager.Get(name) if !ok { return nil } x, y := pw.Position() w, h := pw.Size() return &WindowInfo{ Name: name, Title: pw.Title(), X: x, Y: y, Width: w, Height: h, Maximized: pw.IsMaximised(), Focused: pw.IsFocused(), } } // --- Action Registration --- // registerTaskActions registers all window task handlers as named Core actions. func (s *Service) registerTaskActions() { c := s.Core() c.Action("window.open", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskOpenWindow) return s.taskOpenWindow(t) }) c.Action("window.close", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskCloseWindow) return core.Result{Value: nil, OK: true}.New(s.taskCloseWindow(t.Name)) }) c.Action("window.setPosition", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetPosition) return core.Result{Value: nil, OK: true}.New(s.taskSetPosition(t.Name, t.X, t.Y)) }) c.Action("window.setSize", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetSize) return core.Result{Value: nil, OK: true}.New(s.taskSetSize(t.Name, t.Width, t.Height)) }) c.Action("window.maximise", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskMaximise) return core.Result{Value: nil, OK: true}.New(s.taskMaximise(t.Name)) }) c.Action("window.minimise", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskMinimise) return core.Result{Value: nil, OK: true}.New(s.taskMinimise(t.Name)) }) c.Action("window.focus", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskFocus) return core.Result{Value: nil, OK: true}.New(s.taskFocus(t.Name)) }) c.Action("window.restore", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskRestore) return core.Result{Value: nil, OK: true}.New(s.taskRestore(t.Name)) }) c.Action("window.setTitle", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetTitle) return core.Result{Value: nil, OK: true}.New(s.taskSetTitle(t.Name, t.Title)) }) c.Action("window.setAlwaysOnTop", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetAlwaysOnTop) return core.Result{Value: nil, OK: true}.New(s.taskSetAlwaysOnTop(t.Name, t.AlwaysOnTop)) }) c.Action("window.setBackgroundColour", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetBackgroundColour) return core.Result{Value: nil, OK: true}.New(s.taskSetBackgroundColour(t.Name, t.Red, t.Green, t.Blue, t.Alpha)) }) c.Action("window.setVisibility", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetVisibility) return core.Result{Value: nil, OK: true}.New(s.taskSetVisibility(t.Name, t.Visible)) }) c.Action("window.fullscreen", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskFullscreen) return core.Result{Value: nil, OK: true}.New(s.taskFullscreen(t.Name, t.Fullscreen)) }) c.Action("window.saveLayout", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSaveLayout) return core.Result{Value: nil, OK: true}.New(s.taskSaveLayout(t.Name)) }) c.Action("window.restoreLayout", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskRestoreLayout) return core.Result{Value: nil, OK: true}.New(s.taskRestoreLayout(t.Name)) }) c.Action("window.deleteLayout", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskDeleteLayout) s.manager.Layout().DeleteLayout(t.Name) return core.Result{OK: true} }) c.Action("window.tileWindows", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskTileWindows) return core.Result{Value: nil, OK: true}.New(s.taskTileWindows(t.Mode, t.Windows)) }) c.Action("window.stackWindows", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskStackWindows) return core.Result{Value: nil, OK: true}.New(s.taskStackWindows(t.Windows, t.OffsetX, t.OffsetY)) }) c.Action("window.snapWindow", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSnapWindow) return core.Result{Value: nil, OK: true}.New(s.taskSnapWindow(t.Name, t.Position)) }) c.Action("window.applyWorkflow", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskApplyWorkflow) return core.Result{Value: nil, OK: true}.New(s.taskApplyWorkflow(t.Workflow, t.Windows)) }) c.Action("window.setZoom", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetZoom) return core.Result{Value: nil, OK: true}.New(s.taskSetZoom(t.Name, t.Magnification)) }) c.Action("window.zoomIn", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskZoomIn) return core.Result{Value: nil, OK: true}.New(s.taskZoomIn(t.Name)) }) c.Action("window.zoomOut", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskZoomOut) return core.Result{Value: nil, OK: true}.New(s.taskZoomOut(t.Name)) }) c.Action("window.zoomReset", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskZoomReset) return core.Result{Value: nil, OK: true}.New(s.taskZoomReset(t.Name)) }) c.Action("window.setURL", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetURL) return core.Result{Value: nil, OK: true}.New(s.taskSetURL(t.Name, t.URL)) }) c.Action("window.setHTML", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetHTML) return core.Result{Value: nil, OK: true}.New(s.taskSetHTML(t.Name, t.HTML)) }) c.Action("window.execJS", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskExecJS) return core.Result{Value: nil, OK: true}.New(s.taskExecJS(t.Name, t.JS)) }) c.Action("window.toggleFullscreen", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskToggleFullscreen) return core.Result{Value: nil, OK: true}.New(s.taskToggleFullscreen(t.Name)) }) c.Action("window.toggleMaximise", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskToggleMaximise) return core.Result{Value: nil, OK: true}.New(s.taskToggleMaximise(t.Name)) }) c.Action("window.setBounds", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetBounds) return core.Result{Value: nil, OK: true}.New(s.taskSetBounds(t.Name, t.X, t.Y, t.Width, t.Height)) }) c.Action("window.setContentProtection", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskSetContentProtection) return core.Result{Value: nil, OK: true}.New(s.taskSetContentProtection(t.Name, t.Protection)) }) c.Action("window.flash", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskFlash) return core.Result{Value: nil, OK: true}.New(s.taskFlash(t.Name, t.Enabled)) }) c.Action("window.print", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskPrint) return core.Result{Value: nil, OK: true}.New(s.taskPrint(t.Name)) }) } func (s *Service) primaryScreenArea() (int, int, int, int) { const fallbackX = 0 const fallbackY = 0 const fallbackWidth = 1920 const fallbackHeight = 1080 r := s.Core().QUERY(screen.QueryPrimary{}) if !r.OK { return fallbackX, fallbackY, fallbackWidth, fallbackHeight } primary, ok := r.Value.(*screen.Screen) if !ok || primary == nil { return fallbackX, fallbackY, fallbackWidth, fallbackHeight } x := primary.WorkArea.X y := primary.WorkArea.Y width := primary.WorkArea.Width height := primary.WorkArea.Height if width <= 0 || height <= 0 { x = primary.Bounds.X y = primary.Bounds.Y width = primary.Bounds.Width height = primary.Bounds.Height } if width <= 0 || height <= 0 { return fallbackX, fallbackY, fallbackWidth, fallbackHeight } return x, y, width, height } func (s *Service) taskOpenWindow(t TaskOpenWindow) core.Result { var ( pw PlatformWindow err error ) if t.Window != nil { pw, err = s.manager.Create(t.Window) } else { pw, err = s.manager.Open(t.Options...) } if err != nil { return core.Result{Value: err, OK: false} } x, y := pw.Position() w, h := pw.Size() info := WindowInfo{Name: pw.Name(), Title: pw.Title(), X: x, Y: y, Width: w, Height: h} // Attach platform event listeners that convert to IPC actions s.trackWindow(pw) // Broadcast to all listeners _ = s.Core().ACTION(ActionWindowOpened{Name: pw.Name()}) return core.Result{Value: info, OK: true} } // trackWindow attaches platform event listeners that emit IPC actions. func (s *Service) trackWindow(pw PlatformWindow) { pw.OnWindowEvent(func(e WindowEvent) { switch e.Type { case "focus": _ = s.Core().ACTION(ActionWindowFocused{Name: e.Name}) case "blur": _ = s.Core().ACTION(ActionWindowBlurred{Name: e.Name}) case "move": if data := e.Data; data != nil { x, _ := data["x"].(int) y, _ := data["y"].(int) _ = s.Core().ACTION(ActionWindowMoved{Name: e.Name, X: x, Y: y}) } case "resize": if data := e.Data; data != nil { w, _ := data["w"].(int) h, _ := data["h"].(int) _ = s.Core().ACTION(ActionWindowResized{Name: e.Name, Width: w, Height: h}) } case "close": _ = s.Core().ACTION(ActionWindowClosed{Name: e.Name}) } }) pw.OnFileDrop(func(paths []string, targetID string) { _ = s.Core().ACTION(ActionFilesDropped{ Name: pw.Name(), Paths: paths, TargetID: targetID, }) }) } func (s *Service) taskCloseWindow(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskClose", "window not found: "+name, nil) } // Persist state BEFORE closing (spec requirement) s.manager.State().CaptureState(pw) pw.Close() s.manager.Remove(name) _ = s.Core().ACTION(ActionWindowClosed{Name: name}) return nil } func (s *Service) taskSetPosition(name string, x, y int) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetPosition", "window not found: "+name, nil) } pw.SetPosition(x, y) s.manager.State().UpdatePosition(name, x, y) return nil } func (s *Service) taskSetSize(name string, width, height int) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetSize", "window not found: "+name, nil) } pw.SetSize(width, height) s.manager.State().UpdateSize(name, width, height) return nil } func (s *Service) taskMaximise(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskMaximise", "window not found: "+name, nil) } pw.Maximise() s.manager.State().UpdateMaximized(name, true) return nil } func (s *Service) taskMinimise(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskMinimise", "window not found: "+name, nil) } pw.Minimise() return nil } func (s *Service) taskFocus(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskFocus", "window not found: "+name, nil) } pw.Focus() return nil } func (s *Service) taskRestore(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskRestore", "window not found: "+name, nil) } pw.Restore() s.manager.State().UpdateMaximized(name, false) return nil } func (s *Service) taskSetTitle(name, title string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetTitle", "window not found: "+name, nil) } pw.SetTitle(title) return nil } func (s *Service) taskSetAlwaysOnTop(name string, alwaysOnTop bool) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetAlwaysOnTop", "window not found: "+name, nil) } pw.SetAlwaysOnTop(alwaysOnTop) return nil } func (s *Service) taskSetBackgroundColour(name string, red, green, blue, alpha uint8) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetBackgroundColour", "window not found: "+name, nil) } pw.SetBackgroundColour(red, green, blue, alpha) return nil } func (s *Service) taskSetVisibility(name string, visible bool) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetVisibility", "window not found: "+name, nil) } pw.SetVisibility(visible) return nil } func (s *Service) taskFullscreen(name string, fullscreen bool) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskFullscreen", "window not found: "+name, nil) } if fullscreen { pw.Fullscreen() } else { pw.UnFullscreen() } return nil } func (s *Service) taskSaveLayout(name string) error { windows := s.queryWindowList() states := make(map[string]WindowState, len(windows)) for _, w := range windows { states[w.Name] = WindowState{ X: w.X, Y: w.Y, Width: w.Width, Height: w.Height, Maximized: w.Maximized, } } return s.manager.Layout().SaveLayout(name, states) } func (s *Service) taskRestoreLayout(name string) error { layout, ok := s.manager.Layout().GetLayout(name) if !ok { return coreerr.E("window.taskRestoreLayout", "layout not found: "+name, nil) } for winName, state := range layout.Windows { pw, found := s.manager.Get(winName) if !found { continue } pw.SetPosition(state.X, state.Y) pw.SetSize(state.Width, state.Height) if state.Maximized { pw.Maximise() } else { pw.Restore() } s.manager.State().CaptureState(pw) } return nil } var tileModeMap = map[string]TileMode{ "left-half": TileModeLeftHalf, "right-half": TileModeRightHalf, "top-half": TileModeTopHalf, "bottom-half": TileModeBottomHalf, "top-left": TileModeTopLeft, "top-right": TileModeTopRight, "bottom-left": TileModeBottomLeft, "bottom-right": TileModeBottomRight, "left-right": TileModeLeftRight, "grid": TileModeGrid, } func (s *Service) taskTileWindows(mode string, names []string) error { tm, ok := tileModeMap[mode] if !ok { return coreerr.E("window.taskTileWindows", "unknown tile mode: "+mode, nil) } if len(names) == 0 { names = s.manager.List() } originX, originY, screenWidth, screenHeight := s.primaryScreenArea() return s.manager.TileWindows(tm, names, screenWidth, screenHeight, originX, originY) } func (s *Service) taskStackWindows(names []string, offsetX, offsetY int) error { if len(names) == 0 { names = s.manager.List() } originX, originY, _, _ := s.primaryScreenArea() return s.manager.StackWindows(names, offsetX, offsetY, originX, originY) } var snapPosMap = map[string]SnapPosition{ "left": SnapLeft, "right": SnapRight, "top": SnapTop, "bottom": SnapBottom, "top-left": SnapTopLeft, "top-right": SnapTopRight, "bottom-left": SnapBottomLeft, "bottom-right": SnapBottomRight, "center": SnapCenter, "centre": SnapCenter, } func (s *Service) taskSnapWindow(name, position string) error { pos, ok := snapPosMap[position] if !ok { return coreerr.E("window.taskSnapWindow", "unknown snap position: "+position, nil) } originX, originY, screenWidth, screenHeight := s.primaryScreenArea() return s.manager.SnapWindow(name, pos, screenWidth, screenHeight, originX, originY) } var workflowLayoutMap = map[string]WorkflowLayout{ "coding": WorkflowCoding, "debugging": WorkflowDebugging, "presenting": WorkflowPresenting, "side-by-side": WorkflowSideBySide, } func (s *Service) taskApplyWorkflow(workflow string, names []string) error { layout, ok := workflowLayoutMap[workflow] if !ok { return coreerr.E("window.taskApplyWorkflow", "unknown workflow layout: "+workflow, nil) } if len(names) == 0 { names = s.manager.List() } originX, originY, screenWidth, screenHeight := s.primaryScreenArea() return s.manager.ApplyWorkflow(layout, names, screenWidth, screenHeight, originX, originY) } // --- Zoom --- func (s *Service) queryWindowZoom(name string) core.Result { pw, ok := s.manager.Get(name) if !ok { return core.Result{Value: coreerr.E("window.queryWindowZoom", "window not found: "+name, nil), OK: false} } return core.Result{Value: pw.GetZoom(), OK: true} } func (s *Service) taskSetZoom(name string, magnification float64) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetZoom", "window not found: "+name, nil) } pw.SetZoom(magnification) return nil } func (s *Service) taskZoomIn(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskZoomIn", "window not found: "+name, nil) } current := pw.GetZoom() pw.SetZoom(current + 0.1) return nil } func (s *Service) taskZoomOut(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskZoomOut", "window not found: "+name, nil) } current := pw.GetZoom() next := current - 0.1 if next < 0.1 { next = 0.1 } pw.SetZoom(next) return nil } func (s *Service) taskZoomReset(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskZoomReset", "window not found: "+name, nil) } pw.SetZoom(1.0) return nil } // --- Content --- func (s *Service) taskSetURL(name, url string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetURL", "window not found: "+name, nil) } pw.SetURL(url) return nil } func (s *Service) taskSetHTML(name, html string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetHTML", "window not found: "+name, nil) } pw.SetHTML(html) return nil } func (s *Service) taskExecJS(name, js string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskExecJS", "window not found: "+name, nil) } pw.ExecJS(js) return nil } // --- State toggles --- func (s *Service) taskToggleFullscreen(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskToggleFullscreen", "window not found: "+name, nil) } pw.ToggleFullscreen() return nil } func (s *Service) taskToggleMaximise(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskToggleMaximise", "window not found: "+name, nil) } pw.ToggleMaximise() return nil } // --- Bounds --- func (s *Service) queryWindowBounds(name string) core.Result { pw, ok := s.manager.Get(name) if !ok { return core.Result{Value: coreerr.E("window.queryWindowBounds", "window not found: "+name, nil), OK: false} } x, y, width, height := pw.GetBounds() return core.Result{Value: WindowBounds{X: x, Y: y, Width: width, Height: height}, OK: true} } func (s *Service) taskSetBounds(name string, x, y, width, height int) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetBounds", "window not found: "+name, nil) } pw.SetBounds(x, y, width, height) s.manager.State().UpdatePosition(name, x, y) s.manager.State().UpdateSize(name, width, height) return nil } // --- Content protection --- func (s *Service) taskSetContentProtection(name string, protection bool) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetContentProtection", "window not found: "+name, nil) } pw.SetContentProtection(protection) return nil } // --- Flash --- func (s *Service) taskFlash(name string, enabled bool) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskFlash", "window not found: "+name, nil) } pw.Flash(enabled) return nil } // --- Print --- func (s *Service) taskPrint(name string) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskPrint", "window not found: "+name, nil) } return pw.Print() } // Manager returns the underlying window Manager for direct access. func (s *Service) Manager() *Manager { return s.manager }