diff --git a/pkg/display/display.go b/pkg/display/display.go index e330ff0..3789e4b 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -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", diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index acb9c68..e666c68 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -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) diff --git a/pkg/mcp/tools_window.go b/pkg/mcp/tools_window.go index b735ed0..ac14cd2 100644 --- a/pkg/mcp/tools_window.go +++ b/pkg/mcp/tools_window.go @@ -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 diff --git a/pkg/window/messages.go b/pkg/window/messages.go index a14d464..9a79612 100644 --- a/pkg/window/messages.go +++ b/pkg/window/messages.go @@ -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 } diff --git a/pkg/window/options.go b/pkg/window/options.go deleted file mode 100644 index 38c5064..0000000 --- a/pkg/window/options.go +++ /dev/null @@ -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 } -} diff --git a/pkg/window/service.go b/pkg/window/service.go index 1ae5d8a..6192a89 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -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": diff --git a/pkg/window/service_screen_test.go b/pkg/window/service_screen_test.go index 117d70c..9501e45 100644 --- a/pkg/window/service_screen_test.go +++ b/pkg/window/service_screen_test.go @@ -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"}}) diff --git a/pkg/window/service_test.go b/pkg/window/service_test.go index 6cdfc1a..64a332d 100644 --- a/pkg/window/service_test.go +++ b/pkg/window/service_test.go @@ -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) diff --git a/pkg/window/window.go b/pkg/window/window.go index 5e73e59..9950940 100644 --- a/pkg/window/window.go +++ b/pkg/window/window.go @@ -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()) diff --git a/pkg/window/window_test.go b/pkg/window/window_test.go index f75fe46..d504e4f 100644 --- a/pkg/window/window_test.go +++ b/pkg/window/window_test.go @@ -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)