Window package is now a full core.Service with typed IPC messages. Register(Platform) factory closure captures platform adapter for WithService. OnStartup queries config and registers query/task handlers. Platform events converted to IPC actions via trackWindow. Co-Authored-By: Virgil <virgil@lethean.io>
224 lines
5.7 KiB
Go
224 lines
5.7 KiB
Go
package window
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"forge.lthn.ai/core/go/pkg/core"
|
|
)
|
|
|
|
// Options holds configuration for the window service.
|
|
type Options struct{}
|
|
|
|
// Service is a core.Service managing window lifecycle via IPC.
|
|
// It embeds ServiceRuntime for Core access and composes Manager for platform operations.
|
|
type Service struct {
|
|
*core.ServiceRuntime[Options]
|
|
manager *Manager
|
|
platform Platform
|
|
}
|
|
|
|
// OnStartup queries config from the display orchestrator and registers IPC handlers.
|
|
func (s *Service) OnStartup(ctx context.Context) error {
|
|
// Query config — display registers its handler before us (registration order guarantee).
|
|
// If display is not registered, handled=false and we skip config.
|
|
cfg, handled, _ := s.Core().QUERY(QueryConfig{})
|
|
if handled {
|
|
if wCfg, ok := cfg.(map[string]any); ok {
|
|
s.applyConfig(wCfg)
|
|
}
|
|
}
|
|
|
|
// Register QUERY and TASK handlers manually.
|
|
// ACTION handler (HandleIPCEvents) is auto-registered by WithService —
|
|
// do NOT call RegisterAction here or actions will double-fire.
|
|
s.Core().RegisterQuery(s.handleQuery)
|
|
s.Core().RegisterTask(s.handleTask)
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) applyConfig(cfg map[string]any) {
|
|
// Apply config to manager defaults — future expansion.
|
|
// e.g., default_width, default_height, state_file path.
|
|
}
|
|
|
|
// HandleIPCEvents is auto-discovered and registered by core.WithService.
|
|
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
|
return nil
|
|
}
|
|
|
|
// --- Query Handlers ---
|
|
|
|
func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) {
|
|
switch q := q.(type) {
|
|
case QueryWindowList:
|
|
return s.queryWindowList(), true, nil
|
|
case QueryWindowByName:
|
|
return s.queryWindowByName(q.Name), true, nil
|
|
default:
|
|
return nil, false, nil
|
|
}
|
|
}
|
|
|
|
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, 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, X: x, Y: y, Width: w, Height: h,
|
|
Maximized: pw.IsMaximised(),
|
|
Focused: pw.IsFocused(),
|
|
}
|
|
}
|
|
|
|
// --- Task Handlers ---
|
|
|
|
func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|
switch t := t.(type) {
|
|
case TaskOpenWindow:
|
|
return s.taskOpenWindow(t)
|
|
case TaskCloseWindow:
|
|
return nil, true, s.taskCloseWindow(t.Name)
|
|
case TaskSetPosition:
|
|
return nil, true, s.taskSetPosition(t.Name, t.X, t.Y)
|
|
case TaskSetSize:
|
|
return nil, true, s.taskSetSize(t.Name, t.W, t.H)
|
|
case TaskMaximise:
|
|
return nil, true, s.taskMaximise(t.Name)
|
|
case TaskMinimise:
|
|
return nil, true, s.taskMinimise(t.Name)
|
|
case TaskFocus:
|
|
return nil, true, s.taskFocus(t.Name)
|
|
default:
|
|
return nil, false, nil
|
|
}
|
|
}
|
|
|
|
func (s *Service) taskOpenWindow(t TaskOpenWindow) (any, bool, error) {
|
|
pw, err := s.manager.Open(t.Opts...)
|
|
if err != nil {
|
|
return nil, true, err
|
|
}
|
|
x, y := pw.Position()
|
|
w, h := pw.Size()
|
|
info := WindowInfo{Name: pw.Name(), 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 info, true, nil
|
|
}
|
|
|
|
// 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, W: w, H: h})
|
|
}
|
|
case "close":
|
|
_ = s.Core().ACTION(ActionWindowClosed{Name: e.Name})
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *Service) taskCloseWindow(name string) error {
|
|
pw, ok := s.manager.Get(name)
|
|
if !ok {
|
|
return fmt.Errorf("window not found: %s", name)
|
|
}
|
|
// 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 fmt.Errorf("window not found: %s", name)
|
|
}
|
|
pw.SetPosition(x, y)
|
|
s.manager.State().UpdatePosition(name, x, y)
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) taskSetSize(name string, w, h int) error {
|
|
pw, ok := s.manager.Get(name)
|
|
if !ok {
|
|
return fmt.Errorf("window not found: %s", name)
|
|
}
|
|
pw.SetSize(w, h)
|
|
s.manager.State().UpdateSize(name, w, h)
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) taskMaximise(name string) error {
|
|
pw, ok := s.manager.Get(name)
|
|
if !ok {
|
|
return fmt.Errorf("window not found: %s", name)
|
|
}
|
|
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 fmt.Errorf("window not found: %s", name)
|
|
}
|
|
pw.Minimise()
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) taskFocus(name string) error {
|
|
pw, ok := s.manager.Get(name)
|
|
if !ok {
|
|
return fmt.Errorf("window not found: %s", name)
|
|
}
|
|
pw.Focus()
|
|
return nil
|
|
}
|
|
|
|
// Manager returns the underlying window Manager for direct access.
|
|
func (s *Service) Manager() *Manager {
|
|
return s.manager
|
|
}
|