refactor(ax): replace window option chains with declarative specs
Some checks failed
Security Scan / security (push) Failing after 28s
Test / test (push) Successful in 1m4s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-31 06:05:36 +00:00
parent 089bdacadb
commit f0a1f9027b
10 changed files with 143 additions and 305 deletions

View file

@ -115,7 +115,7 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
case window.ActionWindowResized:
if s.events != nil {
s.events.Emit(Event{Type: EventWindowResize, Window: m.Name,
Data: map[string]any{"w": m.Width, "h": m.Height}})
Data: map[string]any{"width": m.Width, "height": m.Height}})
}
case window.ActionWindowFocused:
if s.events != nil {
@ -474,13 +474,16 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
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.TaskSetVisibility{Name: info.Name, Visible: true})
_, _, _ = s.Core().PERFORM(window.TaskFocus{Name: info.Name})
}
case "close-desktop":
// Hide all windows — future: add TaskHideWindow
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{})
@ -586,13 +589,9 @@ func (s *Service) windowService() *window.Service {
// --- Window Management (delegates via IPC) ---
// OpenWindow creates a new window via IPC.
func (s *Service) OpenWindow(options ...window.WindowOption) error {
spec, err := window.ApplyOptions(options...)
if err != nil {
return err
}
_, _, err = s.Core().PERFORM(window.TaskOpenWindow{Window: spec})
// OpenWindow creates a new window from a declarative Window spec.
func (s *Service) OpenWindow(spec window.Window) error {
_, _, err := s.Core().PERFORM(window.TaskOpenWindow{Window: spec})
return err
}
@ -749,32 +748,13 @@ func (s *Service) GetSavedWindowStates() map[string]window.WindowState {
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(options CreateWindowOptions) (*window.WindowInfo, error) {
if options.Name == "" {
// CreateWindow creates a new named window from a declarative Window spec.
func (s *Service) CreateWindow(spec window.Window) (*window.WindowInfo, error) {
if spec.Name == "" {
return nil, coreerr.E("display.CreateWindow", "window name is required", nil)
}
result, _, err := s.Core().PERFORM(window.TaskOpenWindow{
Window: &window.Window{
Name: options.Name,
Title: options.Title,
URL: options.URL,
Width: options.Width,
Height: options.Height,
X: options.X,
Y: options.Y,
},
Window: spec,
})
if err != nil {
return nil, err
@ -897,7 +877,7 @@ func ptr[T any](v T) *T { return &v }
func (s *Service) handleNewWorkspace() {
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
Window: &window.Window{
Window: window.Window{
Name: "workspace-new",
Title: "New Workspace",
URL: "/workspace/new",
@ -921,7 +901,7 @@ func (s *Service) handleListWorkspaces() {
func (s *Service) handleNewFile() {
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
Window: &window.Window{
Window: window.Window{
Name: "editor",
Title: "New File - Editor",
URL: "/#/developer/editor?new=true",
@ -946,7 +926,7 @@ func (s *Service) handleOpenFile() {
return
}
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
Window: &window.Window{
Window: window.Window{
Name: "editor",
Title: paths[0] + " - Editor",
URL: "/#/developer/editor?file=" + paths[0],
@ -959,7 +939,7 @@ func (s *Service) handleOpenFile() {
func (s *Service) handleSaveFile() { _ = s.Core().ACTION(ActionIDECommand{Command: "save"}) }
func (s *Service) handleOpenEditor() {
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
Window: &window.Window{
Window: window.Window{
Name: "editor",
Title: "Editor",
URL: "/#/developer/editor",
@ -970,7 +950,7 @@ func (s *Service) handleOpenEditor() {
}
func (s *Service) handleOpenTerminal() {
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
Window: &window.Window{
Window: window.Window{
Name: "terminal",
Title: "Terminal",
URL: "/#/developer/terminal",

View file

@ -121,7 +121,7 @@ func TestServiceConclave_Good(t *testing.T) {
// Open a window via IPC
result, handled, err := c.PERFORM(window.TaskOpenWindow{
Window: &window.Window{Name: "main"},
Window: window.Window{Name: "main"},
})
require.NoError(t, err)
assert.True(t, handled)
@ -167,8 +167,8 @@ func TestOpenWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
t.Run("creates window with default options", func(t *testing.T) {
err := svc.OpenWindow()
t.Run("creates window with default spec", func(t *testing.T) {
err := svc.OpenWindow(window.Window{})
assert.NoError(t, err)
// Verify via IPC query
@ -176,13 +176,14 @@ func TestOpenWindow_Good(t *testing.T) {
assert.GreaterOrEqual(t, len(infos), 1)
})
t.Run("creates window with custom options", func(t *testing.T) {
err := svc.OpenWindow(
window.WithName("custom-window"),
window.WithTitle("Custom Title"),
window.WithSize(640, 480),
window.WithURL("/custom"),
)
t.Run("creates window with custom spec", func(t *testing.T) {
err := svc.OpenWindow(window.Window{
Name: "custom-window",
Title: "Custom Title",
Width: 640,
Height: 480,
URL: "/custom",
})
assert.NoError(t, err)
result, _, _ := c.QUERY(window.QueryWindowByName{Name: "custom-window"})
@ -195,10 +196,7 @@ func TestGetWindowInfo_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(
window.WithName("test-win"),
window.WithSize(800, 600),
)
_ = svc.OpenWindow(window.Window{Name: "test-win", Width: 800, Height: 600})
// Modify position via IPC
_, _, _ = c.PERFORM(window.TaskSetPosition{Name: "test-win", X: 100, Y: 200})
@ -226,8 +224,8 @@ func TestListWindowInfos_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("win-1"))
_ = svc.OpenWindow(window.WithName("win-2"))
_ = svc.OpenWindow(window.Window{Name: "win-1"})
_ = svc.OpenWindow(window.Window{Name: "win-2"})
infos := svc.ListWindowInfos()
assert.Len(t, infos, 2)
@ -236,7 +234,7 @@ func TestListWindowInfos_Good(t *testing.T) {
func TestSetWindowPosition_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("pos-win"))
_ = svc.OpenWindow(window.Window{Name: "pos-win"})
err := svc.SetWindowPosition("pos-win", 300, 400)
assert.NoError(t, err)
@ -257,7 +255,7 @@ func TestSetWindowPosition_Bad(t *testing.T) {
func TestSetWindowSize_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("size-win"))
_ = svc.OpenWindow(window.Window{Name: "size-win"})
err := svc.SetWindowSize("size-win", 1024, 768)
assert.NoError(t, err)
@ -270,7 +268,7 @@ func TestSetWindowSize_Good(t *testing.T) {
func TestMaximizeWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("max-win"))
_ = svc.OpenWindow(window.Window{Name: "max-win"})
err := svc.MaximizeWindow("max-win")
assert.NoError(t, err)
@ -282,7 +280,7 @@ func TestMaximizeWindow_Good(t *testing.T) {
func TestRestoreWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("restore-win"))
_ = svc.OpenWindow(window.Window{Name: "restore-win"})
_ = svc.MaximizeWindow("restore-win")
err := svc.RestoreWindow("restore-win")
@ -295,7 +293,7 @@ func TestRestoreWindow_Good(t *testing.T) {
func TestFocusWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("focus-win"))
_ = svc.OpenWindow(window.Window{Name: "focus-win"})
err := svc.FocusWindow("focus-win")
assert.NoError(t, err)
@ -307,7 +305,7 @@ func TestFocusWindow_Good(t *testing.T) {
func TestCloseWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("close-win"))
_ = svc.OpenWindow(window.Window{Name: "close-win"})
err := svc.CloseWindow("close-win")
assert.NoError(t, err)
@ -320,7 +318,7 @@ func TestCloseWindow_Good(t *testing.T) {
func TestSetWindowVisibility_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("vis-win"))
_ = svc.OpenWindow(window.Window{Name: "vis-win"})
err := svc.SetWindowVisibility("vis-win", false)
assert.NoError(t, err)
@ -332,7 +330,7 @@ func TestSetWindowVisibility_Good(t *testing.T) {
func TestSetWindowAlwaysOnTop_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("ontop-win"))
_ = svc.OpenWindow(window.Window{Name: "ontop-win"})
err := svc.SetWindowAlwaysOnTop("ontop-win", true)
assert.NoError(t, err)
@ -341,7 +339,7 @@ func TestSetWindowAlwaysOnTop_Good(t *testing.T) {
func TestSetWindowTitle_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("title-win"))
_ = svc.OpenWindow(window.Window{Name: "title-win"})
err := svc.SetWindowTitle("title-win", "New Title")
assert.NoError(t, err)
@ -350,8 +348,8 @@ func TestSetWindowTitle_Good(t *testing.T) {
func TestGetFocusedWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("win-a"))
_ = svc.OpenWindow(window.WithName("win-b"))
_ = svc.OpenWindow(window.Window{Name: "win-a"})
_ = svc.OpenWindow(window.Window{Name: "win-b"})
_ = svc.FocusWindow("win-b")
focused := svc.GetFocusedWindow()
@ -361,7 +359,7 @@ func TestGetFocusedWindow_Good(t *testing.T) {
func TestGetFocusedWindow_NoneSelected(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("win-a"))
_ = svc.OpenWindow(window.Window{Name: "win-a"})
focused := svc.GetFocusedWindow()
assert.Equal(t, "", focused)
@ -371,7 +369,7 @@ func TestCreateWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
info, err := svc.CreateWindow(CreateWindowOptions{
info, err := svc.CreateWindow(window.Window{
Name: "new-win",
Title: "New Window",
URL: "/new",
@ -386,7 +384,7 @@ func TestCreateWindow_Bad(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_, err := svc.CreateWindow(CreateWindowOptions{})
_, err := svc.CreateWindow(window.Window{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "window name is required")
}
@ -413,7 +411,7 @@ func TestHandleIPCEvents_WindowOpened_Good(t *testing.T) {
// Open a window — this should trigger ActionWindowOpened
// which HandleIPCEvents should convert to a WS event
result, handled, err := c.PERFORM(window.TaskOpenWindow{
Window: &window.Window{Name: "test"},
Window: window.Window{Name: "test"},
})
require.NoError(t, err)
assert.True(t, handled)

View file

@ -75,30 +75,13 @@ func (s *Subsystem) windowFocused(_ context.Context, _ *mcp.CallToolRequest, _ W
// --- window_create ---
type WindowCreateInput 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"`
}
type WindowCreateOutput struct {
Window window.WindowInfo `json:"window"`
}
func (s *Subsystem) windowCreate(_ context.Context, _ *mcp.CallToolRequest, input WindowCreateInput) (*mcp.CallToolResult, WindowCreateOutput, error) {
func (s *Subsystem) windowCreate(_ context.Context, _ *mcp.CallToolRequest, input window.Window) (*mcp.CallToolResult, WindowCreateOutput, error) {
result, _, err := s.core.PERFORM(window.TaskOpenWindow{
Window: &window.Window{
Name: input.Name,
Title: input.Title,
URL: input.URL,
Width: input.Width,
Height: input.Height,
X: input.X,
Y: input.Y,
},
Window: input,
})
if err != nil {
return nil, WindowCreateOutput{}, err

View file

@ -18,8 +18,7 @@ type QueryWindowByName struct{ Name string }
type QueryConfig struct{}
type TaskOpenWindow struct {
Window *Window
Options []WindowOption
Window Window
}
type TaskCloseWindow struct{ Name string }

View file

@ -1,67 +0,0 @@
// pkg/window/options.go
package window
// WindowOption is a functional option applied to a Window descriptor.
type WindowOption func(*Window) error
// ApplyOptions creates a Window and applies all options in order.
func ApplyOptions(options ...WindowOption) (*Window, error) {
w := &Window{}
for _, option := range options {
if option == nil {
continue
}
if err := option(w); err != nil {
return nil, err
}
}
return w, nil
}
func WithName(name string) WindowOption {
return func(w *Window) error { w.Name = name; return nil }
}
func WithTitle(title string) WindowOption {
return func(w *Window) error { w.Title = title; return nil }
}
func WithURL(url string) WindowOption {
return func(w *Window) error { w.URL = url; return nil }
}
func WithSize(width, height int) WindowOption {
return func(w *Window) error { w.Width = width; w.Height = height; return nil }
}
func WithPosition(x, y int) WindowOption {
return func(w *Window) error { w.X = x; w.Y = y; return nil }
}
func WithMinSize(width, height int) WindowOption {
return func(w *Window) error { w.MinWidth = width; w.MinHeight = height; return nil }
}
func WithMaxSize(width, height int) WindowOption {
return func(w *Window) error { w.MaxWidth = width; w.MaxHeight = height; return nil }
}
func WithFrameless(frameless bool) WindowOption {
return func(w *Window) error { w.Frameless = frameless; return nil }
}
func WithHidden(hidden bool) WindowOption {
return func(w *Window) error { w.Hidden = hidden; return nil }
}
func WithAlwaysOnTop(alwaysOnTop bool) WindowOption {
return func(w *Window) error { w.AlwaysOnTop = alwaysOnTop; return nil }
}
func WithBackgroundColour(r, g, b, a uint8) WindowOption {
return func(w *Window) error { w.BackgroundColour = [4]uint8{r, g, b, a}; return nil }
}
func WithFileDrop(enabled bool) WindowOption {
return func(w *Window) error { w.EnableFileDrop = enabled; return nil }
}

View file

@ -187,15 +187,7 @@ func (s *Service) primaryScreenArea() (int, int, int, int) {
}
func (s *Service) taskOpenWindow(t TaskOpenWindow) (any, bool, error) {
var (
pw PlatformWindow
err error
)
if t.Window != nil {
pw, err = s.manager.Create(t.Window)
} else {
pw, err = s.manager.Open(t.Options...)
}
pw, err := s.manager.Create(t.Window)
if err != nil {
return nil, true, err
}
@ -227,8 +219,14 @@ func (s *Service) trackWindow(pw PlatformWindow) {
}
case "resize":
if data := e.Data; data != nil {
w, _ := data["w"].(int)
h, _ := data["h"].(int)
w, _ := data["width"].(int)
if w == 0 {
w, _ = data["w"].(int)
}
h, _ := data["height"].(int)
if h == 0 {
h, _ = data["h"].(int)
}
_ = s.Core().ACTION(ActionWindowResized{Name: e.Name, Width: w, Height: h})
}
case "close":

View file

@ -49,9 +49,9 @@ func TestTaskTileWindows_UsesPrimaryScreenSize(t *testing.T) {
},
})
_, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("left"), WithSize(400, 400)}})
_, _, err := c.PERFORM(TaskOpenWindow{Window: Window{Name: "left", Width: 400, Height: 400}})
require.NoError(t, err)
_, _, err = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("right"), WithSize(400, 400)}})
_, _, err = c.PERFORM(TaskOpenWindow{Window: Window{Name: "right", Width: 400, Height: 400}})
require.NoError(t, err)
_, handled, err := c.PERFORM(TaskTileWindows{Mode: "left-right", Windows: []string{"left", "right"}})
@ -82,7 +82,7 @@ func TestTaskSnapWindow_UsesPrimaryScreenSize(t *testing.T) {
},
})
_, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("snap"), WithSize(400, 300)}})
_, _, err := c.PERFORM(TaskOpenWindow{Window: Window{Name: "snap", Width: 400, Height: 300}})
require.NoError(t, err)
_, handled, err := c.PERFORM(TaskSnapWindow{Name: "snap", Position: "left"})
@ -107,9 +107,9 @@ func TestTaskTileWindows_UsesPrimaryWorkAreaOrigin(t *testing.T) {
},
})
_, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("left"), WithSize(400, 400)}})
_, _, err := c.PERFORM(TaskOpenWindow{Window: Window{Name: "left", Width: 400, Height: 400}})
require.NoError(t, err)
_, _, err = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("right"), WithSize(400, 400)}})
_, _, err = c.PERFORM(TaskOpenWindow{Window: Window{Name: "right", Width: 400, Height: 400}})
require.NoError(t, err)
_, handled, err := c.PERFORM(TaskTileWindows{Mode: "left-right", Windows: []string{"left", "right"}})

View file

@ -31,7 +31,7 @@ func TestRegister_Good(t *testing.T) {
func TestTaskOpenWindow_Good(t *testing.T) {
_, c := newTestWindowService(t)
result, handled, err := c.PERFORM(TaskOpenWindow{
Window: &Window{Name: "test", URL: "/"},
Window: Window{Name: "test", URL: "/"},
})
require.NoError(t, err)
assert.True(t, handled)
@ -39,10 +39,10 @@ func TestTaskOpenWindow_Good(t *testing.T) {
assert.Equal(t, "test", info.Name)
}
func TestTaskOpenWindow_OptionsFallback_Good(t *testing.T) {
func TestTaskOpenWindow_Declarative_Good(t *testing.T) {
_, c := newTestWindowService(t)
result, handled, err := c.PERFORM(TaskOpenWindow{
Options: []WindowOption{WithName("test-fallback"), WithURL("/")},
Window: Window{Name: "test-fallback", URL: "/"},
})
require.NoError(t, err)
assert.True(t, handled)
@ -60,8 +60,8 @@ func TestTaskOpenWindow_Bad(t *testing.T) {
func TestQueryWindowList_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("a")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("b")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "a"}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "b"}})
result, handled, err := c.QUERY(QueryWindowList{})
require.NoError(t, err)
@ -72,7 +72,7 @@ func TestQueryWindowList_Good(t *testing.T) {
func TestQueryWindowByName_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
result, handled, err := c.QUERY(QueryWindowByName{Name: "test"})
require.NoError(t, err)
@ -91,7 +91,7 @@ func TestQueryWindowByName_Bad(t *testing.T) {
func TestTaskCloseWindow_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskCloseWindow{Name: "test"})
require.NoError(t, err)
@ -111,7 +111,7 @@ func TestTaskCloseWindow_Bad(t *testing.T) {
func TestTaskSetPosition_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskSetPosition{Name: "test", X: 100, Y: 200})
require.NoError(t, err)
@ -125,7 +125,7 @@ func TestTaskSetPosition_Good(t *testing.T) {
func TestTaskSetSize_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskSetSize{Name: "test", Width: 800, Height: 600})
require.NoError(t, err)
@ -139,7 +139,7 @@ func TestTaskSetSize_Good(t *testing.T) {
func TestTaskMaximise_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskMaximise{Name: "test"})
require.NoError(t, err)
@ -155,7 +155,7 @@ func TestFileDrop_Good(t *testing.T) {
// Open a window
result, _, _ := c.PERFORM(TaskOpenWindow{
Options: []WindowOption{WithName("drop-test")},
Window: Window{Name: "drop-test"},
})
info := result.(WindowInfo)
assert.Equal(t, "drop-test", info.Name)
@ -190,7 +190,7 @@ func TestFileDrop_Good(t *testing.T) {
func TestTaskMinimise_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskMinimise{Name: "test"})
require.NoError(t, err)
@ -213,7 +213,7 @@ func TestTaskMinimise_Bad(t *testing.T) {
func TestTaskFocus_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskFocus{Name: "test"})
require.NoError(t, err)
@ -236,7 +236,7 @@ func TestTaskFocus_Bad(t *testing.T) {
func TestTaskRestore_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
// First maximise, then restore
_, _, _ = c.PERFORM(TaskMaximise{Name: "test"})
@ -267,7 +267,7 @@ func TestTaskRestore_Bad(t *testing.T) {
func TestTaskSetTitle_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskSetTitle{Name: "test", Title: "New Title"})
require.NoError(t, err)
@ -289,7 +289,7 @@ func TestTaskSetTitle_Bad(t *testing.T) {
func TestTaskSetAlwaysOnTop_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskSetAlwaysOnTop{Name: "test", AlwaysOnTop: true})
require.NoError(t, err)
@ -312,7 +312,7 @@ func TestTaskSetAlwaysOnTop_Bad(t *testing.T) {
func TestTaskSetBackgroundColour_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskSetBackgroundColour{
Name: "test", Red: 10, Green: 20, Blue: 30, Alpha: 40,
@ -337,7 +337,7 @@ func TestTaskSetBackgroundColour_Bad(t *testing.T) {
func TestTaskSetVisibility_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskSetVisibility{Name: "test", Visible: true})
require.NoError(t, err)
@ -366,7 +366,7 @@ func TestTaskSetVisibility_Bad(t *testing.T) {
func TestTaskFullscreen_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
// Enter fullscreen
_, handled, err := c.PERFORM(TaskFullscreen{Name: "test", Fullscreen: true})
@ -396,8 +396,8 @@ func TestTaskFullscreen_Bad(t *testing.T) {
func TestTaskSaveLayout_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("editor"), WithSize(960, 1080), WithPosition(0, 0)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("terminal"), WithSize(960, 1080), WithPosition(960, 0)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "editor", Width: 960, Height: 1080, X: 0, Y: 0}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "terminal", Width: 960, Height: 1080, X: 960, Y: 0}})
_, handled, err := c.PERFORM(TaskSaveLayout{Name: "coding"})
require.NoError(t, err)
@ -433,8 +433,8 @@ func TestTaskSaveLayout_Bad(t *testing.T) {
func TestTaskRestoreLayout_Good(t *testing.T) {
svc, c := newTestWindowService(t)
// Open windows
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("editor"), WithSize(800, 600), WithPosition(0, 0)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("terminal"), WithSize(800, 600), WithPosition(0, 0)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "editor", Width: 800, Height: 600, X: 0, Y: 0}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "terminal", Width: 800, Height: 600, X: 0, Y: 0}})
// Save a layout with specific positions
_, _, _ = c.PERFORM(TaskSaveLayout{Name: "coding"})
@ -483,8 +483,8 @@ func TestTaskRestoreLayout_Bad(t *testing.T) {
func TestTaskStackWindows_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("s1"), WithSize(800, 600)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("s2"), WithSize(800, 600)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "s1", Width: 800, Height: 600}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "s2", Width: 800, Height: 600}})
_, handled, err := c.PERFORM(TaskStackWindows{Windows: []string{"s1", "s2"}, OffsetX: 25, OffsetY: 35})
require.NoError(t, err)
@ -501,8 +501,8 @@ func TestTaskStackWindows_Good(t *testing.T) {
func TestTaskApplyWorkflow_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("editor"), WithSize(800, 600)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("terminal"), WithSize(800, 600)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "editor", Width: 800, Height: 600}})
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "terminal", Width: 800, Height: 600}})
_, handled, err := c.PERFORM(TaskApplyWorkflow{Workflow: "side-by-side"})
require.NoError(t, err)

View file

@ -1,26 +1,27 @@
// pkg/window/window.go
package window
import (
"fmt"
"sync"
)
import "sync"
// Window is CoreGUI's own window descriptor — NOT a Wails type alias.
type Window struct {
Name string
Title string
URL string
Width, Height int
X, Y int
MinWidth, MinHeight int
MaxWidth, MaxHeight int
Frameless bool
Hidden bool
AlwaysOnTop bool
BackgroundColour [4]uint8
DisableResize bool
EnableFileDrop bool
Name string `json:"name,omitempty"`
Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
X int `json:"x,omitempty"`
Y int `json:"y,omitempty"`
MinWidth int `json:"minWidth,omitempty"`
MinHeight int `json:"minHeight,omitempty"`
MaxWidth int `json:"maxWidth,omitempty"`
MaxHeight int `json:"maxHeight,omitempty"`
Frameless bool `json:"frameless,omitempty"`
Hidden bool `json:"hidden,omitempty"`
AlwaysOnTop bool `json:"alwaysOnTop,omitempty"`
BackgroundColour [4]uint8 `json:"backgroundColour,omitempty"`
DisableResize bool `json:"disableResize,omitempty"`
EnableFileDrop bool `json:"enableFileDrop,omitempty"`
}
// ToPlatformOptions converts a Window to PlatformWindowOptions for the backend.
@ -80,17 +81,15 @@ func (m *Manager) SetDefaultHeight(height int) {
}
}
// Open creates a window using functional options, applies saved state, and tracks it.
func (m *Manager) Open(options ...WindowOption) (PlatformWindow, error) {
w, err := ApplyOptions(options...)
if err != nil {
return nil, fmt.Errorf("window.Manager.Open: %w", err)
}
return m.Create(w)
// Open creates a window from a declarative Window spec, for example:
// m.Open(Window{Name: "editor", Title: "Editor", URL: "/"})
func (m *Manager) Open(spec Window) (PlatformWindow, error) {
return m.Create(spec)
}
// Create creates a window from a Window descriptor.
func (m *Manager) Create(w *Window) (PlatformWindow, error) {
func (m *Manager) Create(spec Window) (PlatformWindow, error) {
w := spec
if w.Name == "" {
w.Name = "main"
}
@ -116,7 +115,7 @@ func (m *Manager) Create(w *Window) (PlatformWindow, error) {
}
// Apply saved state if available
m.state.ApplyState(w)
m.state.ApplyState(&w)
pw := m.platform.CreateWindow(w.ToPlatformOptions())

View file

@ -14,71 +14,25 @@ func TestWindowDefaults(t *testing.T) {
assert.Equal(t, 0, w.Width)
}
func TestWindowOption_Name_Good(t *testing.T) {
w := &Window{}
err := WithName("main")(w)
require.NoError(t, err)
func TestWindowSpec_Good(t *testing.T) {
w := Window{
Name: "main",
Title: "My App",
URL: "/dashboard",
Width: 1280,
Height: 720,
X: 100,
Y: 200,
}
assert.Equal(t, "main", w.Name)
}
func TestWindowOption_Title_Good(t *testing.T) {
w := &Window{}
err := WithTitle("My App")(w)
require.NoError(t, err)
assert.Equal(t, "My App", w.Title)
}
func TestWindowOption_URL_Good(t *testing.T) {
w := &Window{}
err := WithURL("/dashboard")(w)
require.NoError(t, err)
assert.Equal(t, "/dashboard", w.URL)
}
func TestWindowOption_Size_Good(t *testing.T) {
w := &Window{}
err := WithSize(1280, 720)(w)
require.NoError(t, err)
assert.Equal(t, 1280, w.Width)
assert.Equal(t, 720, w.Height)
}
func TestWindowOption_Position_Good(t *testing.T) {
w := &Window{}
err := WithPosition(100, 200)(w)
require.NoError(t, err)
assert.Equal(t, 100, w.X)
assert.Equal(t, 200, w.Y)
}
func TestApplyOptions_Good(t *testing.T) {
w, err := ApplyOptions(
WithName("test"),
WithTitle("Test Window"),
WithURL("/test"),
WithSize(800, 600),
)
require.NoError(t, err)
assert.Equal(t, "test", w.Name)
assert.Equal(t, "Test Window", w.Title)
assert.Equal(t, "/test", w.URL)
assert.Equal(t, 800, w.Width)
assert.Equal(t, 600, w.Height)
}
func TestApplyOptions_Bad(t *testing.T) {
_, err := ApplyOptions(func(w *Window) error {
return assert.AnError
})
assert.Error(t, err)
}
func TestApplyOptions_Empty_Good(t *testing.T) {
w, err := ApplyOptions()
require.NoError(t, err)
assert.NotNil(t, w)
}
// newTestManager creates a Manager with a mock platform and clean state for testing.
func newTestManager() (*Manager, *mockPlatform) {
p := newMockPlatform()
@ -93,7 +47,7 @@ func newTestManager() (*Manager, *mockPlatform) {
func TestManager_Open_Good(t *testing.T) {
m, p := newTestManager()
pw, err := m.Open(WithName("test"), WithTitle("Test"), WithURL("/test"), WithSize(800, 600))
pw, err := m.Open(Window{Name: "test", Title: "Test", URL: "/test", Width: 800, Height: 600})
require.NoError(t, err)
assert.NotNil(t, pw)
assert.Equal(t, "test", pw.Name())
@ -102,7 +56,7 @@ func TestManager_Open_Good(t *testing.T) {
func TestManager_Open_Defaults_Good(t *testing.T) {
m, _ := newTestManager()
pw, err := m.Open()
pw, err := m.Open(Window{})
require.NoError(t, err)
assert.Equal(t, "main", pw.Name())
w, h := pw.Size()
@ -115,7 +69,7 @@ func TestManager_Open_CustomDefaults_Good(t *testing.T) {
m.SetDefaultWidth(1440)
m.SetDefaultHeight(900)
pw, err := m.Open()
pw, err := m.Open(Window{})
require.NoError(t, err)
w, h := pw.Size()
@ -123,15 +77,9 @@ func TestManager_Open_CustomDefaults_Good(t *testing.T) {
assert.Equal(t, 900, h)
}
func TestManager_Open_Bad(t *testing.T) {
m, _ := newTestManager()
_, err := m.Open(func(w *Window) error { return assert.AnError })
assert.Error(t, err)
}
func TestManager_Get_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("findme"))
_, _ = m.Open(Window{Name: "findme"})
pw, ok := m.Get("findme")
assert.True(t, ok)
assert.Equal(t, "findme", pw.Name())
@ -145,8 +93,8 @@ func TestManager_Get_Bad(t *testing.T) {
func TestManager_List_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("a"))
_, _ = m.Open(WithName("b"))
_, _ = m.Open(Window{Name: "a"})
_, _ = m.Open(Window{Name: "b"})
names := m.List()
assert.Len(t, names, 2)
assert.Contains(t, names, "a")
@ -155,7 +103,7 @@ func TestManager_List_Good(t *testing.T) {
func TestManager_Remove_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("temp"))
_, _ = m.Open(Window{Name: "temp"})
m.Remove("temp")
_, ok := m.Get("temp")
assert.False(t, ok)
@ -170,8 +118,8 @@ func TestTileMode_String_Good(t *testing.T) {
func TestManager_TileWindows_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("a"), WithSize(800, 600))
_, _ = m.Open(WithName("b"), WithSize(800, 600))
_, _ = m.Open(Window{Name: "a", Width: 800, Height: 600})
_, _ = m.Open(Window{Name: "b", Width: 800, Height: 600})
err := m.TileWindows(TileModeLeftRight, []string{"a", "b"}, 1920, 1080)
require.NoError(t, err)
a, _ := m.Get("a")
@ -190,7 +138,7 @@ func TestManager_TileWindows_Bad(t *testing.T) {
func TestManager_SnapWindow_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("snap"), WithSize(800, 600))
_, _ = m.Open(Window{Name: "snap", Width: 800, Height: 600})
err := m.SnapWindow("snap", SnapLeft, 1920, 1080)
require.NoError(t, err)
w, _ := m.Get("snap")
@ -202,8 +150,8 @@ func TestManager_SnapWindow_Good(t *testing.T) {
func TestManager_StackWindows_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("s1"), WithSize(800, 600))
_, _ = m.Open(WithName("s2"), WithSize(800, 600))
_, _ = m.Open(Window{Name: "s1", Width: 800, Height: 600})
_, _ = m.Open(Window{Name: "s2", Width: 800, Height: 600})
err := m.StackWindows([]string{"s1", "s2"}, 30, 30)
require.NoError(t, err)
s2, _ := m.Get("s2")
@ -244,7 +192,7 @@ func TestTileWindows_AllModes_Good(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
m, _ := newTestManager()
_, err := m.Open(WithName("win"), WithSize(800, 600))
_, err := m.Open(Window{Name: "win", Width: 800, Height: 600})
require.NoError(t, err)
err = m.TileWindows(tc.mode, []string{"win"}, screenW, screenH)
@ -290,7 +238,7 @@ func TestSnapWindow_AllPositions_Good(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
m, _ := newTestManager()
_, err := m.Open(WithName("snap"), WithSize(tc.initW, tc.initH))
_, err := m.Open(Window{Name: "snap", Width: tc.initW, Height: tc.initH})
require.NoError(t, err)
err = m.SnapWindow("snap", tc.pos, screenW, screenH)
@ -313,7 +261,7 @@ func TestStackWindows_ThreeWindows_Good(t *testing.T) {
m, _ := newTestManager()
names := []string{"s1", "s2", "s3"}
for _, name := range names {
_, err := m.Open(WithName(name), WithSize(800, 600))
_, err := m.Open(Window{Name: name, Width: 800, Height: 600})
require.NoError(t, err)
}
@ -369,9 +317,9 @@ func TestApplyWorkflow_AllLayouts_Good(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
m, _ := newTestManager()
_, err := m.Open(WithName("editor"), WithSize(800, 600))
_, err := m.Open(Window{Name: "editor", Width: 800, Height: 600})
require.NoError(t, err)
_, err = m.Open(WithName("terminal"), WithSize(800, 600))
_, err = m.Open(Window{Name: "terminal", Width: 800, Height: 600})
require.NoError(t, err)
err = m.ApplyWorkflow(tc.workflow, []string{"editor", "terminal"}, screenW, screenH)