diff --git a/go.mod b/go.mod index c13dc8e..a3f7a45 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,7 @@ module forge.lthn.ai/core/gui go 1.26.0 require ( - forge.lthn.ai/core/config v0.1.8 - forge.lthn.ai/core/go v0.3.3 - forge.lthn.ai/core/go-io v0.1.7 - forge.lthn.ai/core/go-log v0.0.4 + dappco.re/go/core v0.8.0-alpha.1 forge.lthn.ai/core/go-webview v0.1.7 github.com/gorilla/websocket v1.5.3 github.com/modelcontextprotocol/go-sdk v1.4.1 @@ -17,12 +14,18 @@ require ( replace github.com/wailsapp/wails/v3 => ./stubs/wails require ( - dappco.re/go/core v0.8.0-alpha.1 // indirect + dappco.re/go/core/io v0.2.0 // indirect + dappco.re/go/core/log v0.1.0 // indirect + forge.lthn.ai/core/config v0.1.8 // indirect + forge.lthn.ai/core/go v0.3.3 // indirect + forge.lthn.ai/core/go-io v0.1.7 // indirect + forge.lthn.ai/core/go-log v0.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/jsonschema-go v0.4.2 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect @@ -38,5 +41,6 @@ require ( golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.43.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0618491..0bfaad3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= +dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4= +dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E= +dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc= +dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= forge.lthn.ai/core/config v0.1.8 h1:xP2hys7T94QGVF/OTh84/Zr5Dm/dL/0vzjht8zi+LOg= forge.lthn.ai/core/config v0.1.8/go.mod h1:8epZrkwoCt+5ayrqdinOUU/+w6UoxOyv9ZrdgVOgYfQ= forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0= @@ -10,10 +14,9 @@ forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= forge.lthn.ai/core/go-webview v0.1.7 h1:9+aEHeAvNcPX8Zwr+UGu0/T+menRm5T1YOmqZ9dawDc= forge.lthn.ai/core/go-webview v0.1.7/go.mod h1:5n1tECD1wBV/uFZRY9ZjfPFO5TYZrlaR3mQFwvO2nek= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= diff --git a/pkg/browser/register.go b/pkg/browser/register.go index bb9dffb..b11c9ab 100644 --- a/pkg/browser/register.go +++ b/pkg/browser/register.go @@ -1,14 +1,14 @@ package browser -import "forge.lthn.ai/core/go/pkg/core" +import core "dappco.re/go/core" // Register(p) binds the browser service to a Core instance. // core.WithService(browser.Register(wailsBrowser)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, - }, nil + }, OK: true} } } diff --git a/pkg/browser/service.go b/pkg/browser/service.go index 7d05721..b1b6a82 100644 --- a/pkg/browser/service.go +++ b/pkg/browser/service.go @@ -3,7 +3,7 @@ package browser import ( "context" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" ) type Options struct{} @@ -13,22 +13,22 @@ type Service struct { platform Platform } -func (s *Service) OnStartup(ctx context.Context) error { - s.Core().RegisterTask(s.handleTask) - return nil +func (s *Service) OnStartup(_ context.Context) core.Result { + s.Core().Action("browser.openURL", func(_ context.Context, opts core.Options) core.Result { + if err := s.platform.OpenURL(opts.String("url")); err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{OK: true} + }) + s.Core().Action("browser.openFile", func(_ context.Context, opts core.Options) core.Result { + if err := s.platform.OpenFile(opts.String("path")); err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{OK: true} + }) + return core.Result{OK: true} } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil -} - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskOpenURL: - return nil, true, s.platform.OpenURL(t.URL) - case TaskOpenFile: - return nil, true, s.platform.OpenFile(t.Path) - default: - return nil, false, nil - } +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } diff --git a/pkg/browser/service_test.go b/pkg/browser/service_test.go index b15c52c..9565c61 100644 --- a/pkg/browser/service_test.go +++ b/pkg/browser/service_test.go @@ -6,7 +6,7 @@ import ( "errors" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -30,12 +30,11 @@ func (m *mockPlatform) OpenFile(path string) error { func newTestBrowserService(t *testing.T, mp *mockPlatform) (*Service, *core.Core) { t.Helper() - c, err := core.New( + c := core.New( core.WithService(Register(mp)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "browser") return svc, c } @@ -51,9 +50,10 @@ func TestTaskOpenURL_Good(t *testing.T) { mp := &mockPlatform{} _, c := newTestBrowserService(t, mp) - _, handled, err := c.PERFORM(TaskOpenURL{URL: "https://example.com"}) - require.NoError(t, err) - assert.True(t, handled) + r := c.Action("browser.openURL").Run(context.Background(), core.NewOptions( + core.Option{Key: "url", Value: "https://example.com"}, + )) + require.True(t, r.OK) assert.Equal(t, "https://example.com", mp.lastURL) } @@ -61,18 +61,20 @@ func TestTaskOpenURL_Bad_PlatformError(t *testing.T) { mp := &mockPlatform{urlErr: errors.New("browser not found")} _, c := newTestBrowserService(t, mp) - _, handled, err := c.PERFORM(TaskOpenURL{URL: "https://example.com"}) - assert.True(t, handled) - assert.Error(t, err) + r := c.Action("browser.openURL").Run(context.Background(), core.NewOptions( + core.Option{Key: "url", Value: "https://example.com"}, + )) + assert.False(t, r.OK) } func TestTaskOpenFile_Good(t *testing.T) { mp := &mockPlatform{} _, c := newTestBrowserService(t, mp) - _, handled, err := c.PERFORM(TaskOpenFile{Path: "/tmp/readme.txt"}) - require.NoError(t, err) - assert.True(t, handled) + r := c.Action("browser.openFile").Run(context.Background(), core.NewOptions( + core.Option{Key: "path", Value: "/tmp/readme.txt"}, + )) + require.True(t, r.OK) assert.Equal(t, "/tmp/readme.txt", mp.lastPath) } @@ -80,13 +82,16 @@ func TestTaskOpenFile_Bad_PlatformError(t *testing.T) { mp := &mockPlatform{fileErr: errors.New("file not found")} _, c := newTestBrowserService(t, mp) - _, handled, err := c.PERFORM(TaskOpenFile{Path: "/nonexistent"}) - assert.True(t, handled) - assert.Error(t, err) + r := c.Action("browser.openFile").Run(context.Background(), core.NewOptions( + core.Option{Key: "path", Value: "/nonexistent"}, + )) + assert.False(t, r.OK) } func TestTaskOpenURL_Bad_NoService(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskOpenURL{URL: "https://example.com"}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("browser.openURL").Run(context.Background(), core.NewOptions( + core.Option{Key: "url", Value: "https://example.com"}, + )) + assert.False(t, r.OK) } diff --git a/pkg/clipboard/service.go b/pkg/clipboard/service.go index ee47af6..884e6f3 100644 --- a/pkg/clipboard/service.go +++ b/pkg/clipboard/service.go @@ -4,7 +4,7 @@ package clipboard import ( "context" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" ) type Options struct{} @@ -16,42 +16,38 @@ type Service struct { // Register(p) binds the clipboard service to a Core instance. // c.WithService(clipboard.Register(wailsClipboard)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, - }, nil + }, OK: true} } } -func (s *Service) OnStartup(ctx context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) - return nil + s.Core().Action("clipboard.setText", func(_ context.Context, opts core.Options) core.Result { + success := s.platform.SetText(opts.String("text")) + return core.Result{Value: success, OK: true} + }) + s.Core().Action("clipboard.clear", func(_ context.Context, _ core.Options) core.Result { + success := s.platform.SetText("") + return core.Result{Value: success, OK: true} + }) + return core.Result{OK: true} } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } -func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q.(type) { case QueryText: text, ok := s.platform.Text() - return ClipboardContent{Text: text, HasContent: ok && text != ""}, true, nil + return core.Result{Value: ClipboardContent{Text: text, HasContent: ok && text != ""}, OK: true} default: - return nil, false, nil - } -} - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskSetText: - return s.platform.SetText(t.Text), true, nil - case TaskClear: - return s.platform.SetText(""), true, nil - default: - return nil, false, nil + return core.Result{} } } diff --git a/pkg/clipboard/service_test.go b/pkg/clipboard/service_test.go index 63677df..894b663 100644 --- a/pkg/clipboard/service_test.go +++ b/pkg/clipboard/service_test.go @@ -5,7 +5,7 @@ import ( "context" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -24,12 +24,11 @@ func (m *mockPlatform) SetText(text string) bool { func newTestService(t *testing.T) (*Service, *core.Core) { t.Helper() - c, err := core.New( + c := core.New( core.WithService(Register(&mockPlatform{text: "hello", ok: true})), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "clipboard") return svc, c } @@ -41,41 +40,40 @@ func TestRegister_Good(t *testing.T) { func TestQueryText_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryText{}) - require.NoError(t, err) - assert.True(t, handled) - content := result.(ClipboardContent) + r := c.QUERY(QueryText{}) + require.True(t, r.OK) + content := r.Value.(ClipboardContent) assert.Equal(t, "hello", content.Text) assert.True(t, content.HasContent) } func TestQueryText_Bad(t *testing.T) { // No clipboard service registered - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.QUERY(QueryText{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(QueryText{}) + assert.False(t, r.OK) } func TestTaskSetText_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.PERFORM(TaskSetText{Text: "world"}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, true, result) + r := c.Action("clipboard.setText").Run(context.Background(), core.NewOptions( + core.Option{Key: "text", Value: "world"}, + )) + require.True(t, r.OK) + assert.Equal(t, true, r.Value) // Verify via query - r, _, _ := c.QUERY(QueryText{}) - assert.Equal(t, "world", r.(ClipboardContent).Text) + qr := c.QUERY(QueryText{}) + assert.Equal(t, "world", qr.Value.(ClipboardContent).Text) } func TestTaskClear_Good(t *testing.T) { _, c := newTestService(t) - _, handled, err := c.PERFORM(TaskClear{}) - require.NoError(t, err) - assert.True(t, handled) + r := c.Action("clipboard.clear").Run(context.Background(), core.NewOptions()) + require.True(t, r.OK) // Verify empty - r, _, _ := c.QUERY(QueryText{}) - assert.Equal(t, "", r.(ClipboardContent).Text) - assert.False(t, r.(ClipboardContent).HasContent) + qr := c.QUERY(QueryText{}) + assert.Equal(t, "", qr.Value.(ClipboardContent).Text) + assert.False(t, qr.Value.(ClipboardContent).HasContent) } diff --git a/pkg/contextmenu/register.go b/pkg/contextmenu/register.go index 548be98..b8faeb9 100644 --- a/pkg/contextmenu/register.go +++ b/pkg/contextmenu/register.go @@ -1,15 +1,15 @@ package contextmenu -import "forge.lthn.ai/core/go/pkg/core" +import core "dappco.re/go/core" // Register(p) binds the context menu service to a Core instance. // core.WithService(contextmenu.Register(wailsContextMenu)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, registeredMenus: make(map[string]ContextMenuDef), - }, nil + }, OK: true} } } diff --git a/pkg/contextmenu/service.go b/pkg/contextmenu/service.go index 0386fc3..61eb85c 100644 --- a/pkg/contextmenu/service.go +++ b/pkg/contextmenu/service.go @@ -4,8 +4,8 @@ package contextmenu import ( "context" - coreerr "forge.lthn.ai/core/go-log" - "forge.lthn.ai/core/go/pkg/core" + coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) type Options struct{} @@ -16,37 +16,52 @@ type Service struct { registeredMenus map[string]ContextMenuDef } -func (s *Service) OnStartup(ctx context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) - return nil + s.Core().Action("contextmenu.add", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskAdd) + return core.Result{Value: nil, OK: true}.New(s.taskAdd(t)) + }) + s.Core().Action("contextmenu.remove", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskRemove) + return core.Result{Value: nil, OK: true}.New(s.taskRemove(t)) + }) + s.Core().Action("contextmenu.update", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskUpdate) + return core.Result{Value: nil, OK: true}.New(s.taskUpdate(t)) + }) + s.Core().Action("contextmenu.destroy", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskDestroy) + return core.Result{Value: nil, OK: true}.New(s.taskDestroy(t)) + }) + return core.Result{OK: true} } -func (s *Service) OnShutdown(ctx context.Context) error { +func (s *Service) OnShutdown(_ context.Context) core.Result { // Destroy all registered menus on shutdown to release platform resources for name := range s.registeredMenus { _ = s.platform.Remove(name) } s.registeredMenus = make(map[string]ContextMenuDef) - return nil + return core.Result{OK: true} } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } // --- Query Handlers --- -func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q := q.(type) { case QueryGet: - return s.queryGet(q), true, nil + return core.Result{Value: s.queryGet(q), OK: true} case QueryList: - return s.queryList(), true, nil + return core.Result{Value: s.queryList(), OK: true} case QueryGetAll: - return s.queryList(), true, nil + return core.Result{Value: s.queryList(), OK: true} default: - return nil, false, nil + return core.Result{} } } @@ -66,23 +81,6 @@ func (s *Service) queryList() map[string]ContextMenuDef { return result } -// --- Task Handlers --- - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskAdd: - return nil, true, s.taskAdd(t) - case TaskRemove: - return nil, true, s.taskRemove(t) - case TaskUpdate: - return nil, true, s.taskUpdate(t) - case TaskDestroy: - return nil, true, s.taskDestroy(t) - default: - return nil, false, nil - } -} - func (s *Service) taskAdd(t TaskAdd) error { // If menu already exists, remove it first (replace semantics) if _, exists := s.registeredMenus[t.Name]; exists { diff --git a/pkg/contextmenu/service_test.go b/pkg/contextmenu/service_test.go index abd1e2b..4def986 100644 --- a/pkg/contextmenu/service_test.go +++ b/pkg/contextmenu/service_test.go @@ -6,7 +6,7 @@ import ( "sync" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -83,16 +83,22 @@ func (m *mockPlatform) simulateClick(menuName, actionID, data string) { func newTestContextMenuService(t *testing.T, mp *mockPlatform) (*Service, *core.Core) { t.Helper() - c, err := core.New( + c := core.New( core.WithService(Register(mp)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "contextmenu") return svc, c } +// taskRun runs a named action with a task struct and returns the result. +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + func TestRegister_Good(t *testing.T) { mp := newMockPlatform() svc, _ := newTestContextMenuService(t, mp) @@ -104,7 +110,7 @@ func TestTaskAdd_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, handled, err := c.PERFORM(TaskAdd{ + r := taskRun(c, "contextmenu.add", TaskAdd{ Name: "file-menu", Menu: ContextMenuDef{ Name: "file-menu", @@ -114,8 +120,7 @@ func TestTaskAdd_Good(t *testing.T) { }, }, }) - require.NoError(t, err) - assert.True(t, handled) + require.True(t, r.OK) // Verify menu registered on platform _, ok := mp.Get("file-menu") @@ -127,22 +132,22 @@ func TestTaskAdd_Good_ReplaceExisting(t *testing.T) { _, c := newTestContextMenuService(t, mp) // Add initial menu - _, _, _ = c.PERFORM(TaskAdd{ + _ = taskRun(c, "contextmenu.add", TaskAdd{ Name: "ctx", Menu: ContextMenuDef{Name: "ctx", Items: []MenuItemDef{{Label: "A", ActionID: "a"}}}, }) // Replace with new menu - _, handled, err := c.PERFORM(TaskAdd{ + r := taskRun(c, "contextmenu.add", TaskAdd{ Name: "ctx", Menu: ContextMenuDef{Name: "ctx", Items: []MenuItemDef{{Label: "B", ActionID: "b"}}}, }) - require.NoError(t, err) - assert.True(t, handled) + require.True(t, r.OK) // Verify registry has new menu - result, _, _ := c.QUERY(QueryGet{Name: "ctx"}) - def := result.(*ContextMenuDef) + qr := c.QUERY(QueryGet{Name: "ctx"}) + require.True(t, qr.OK) + def := qr.Value.(*ContextMenuDef) require.Len(t, def.Items, 1) assert.Equal(t, "B", def.Items[0].Label) } @@ -152,25 +157,25 @@ func TestTaskRemove_Good(t *testing.T) { _, c := newTestContextMenuService(t, mp) // Add then remove - _, _, _ = c.PERFORM(TaskAdd{ + _ = taskRun(c, "contextmenu.add", TaskAdd{ Name: "test", Menu: ContextMenuDef{Name: "test"}, }) - _, handled, err := c.PERFORM(TaskRemove{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "contextmenu.remove", TaskRemove{Name: "test"}) + require.True(t, r.OK) // Verify removed from registry - result, _, _ := c.QUERY(QueryGet{Name: "test"}) - assert.Nil(t, result) + qr := c.QUERY(QueryGet{Name: "test"}) + assert.Nil(t, qr.Value) } func TestTaskRemove_Bad_NotFound(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, handled, err := c.PERFORM(TaskRemove{Name: "nonexistent"}) - assert.True(t, handled) + r := taskRun(c, "contextmenu.remove", TaskRemove{Name: "nonexistent"}) + assert.False(t, r.OK) + err, _ := r.Value.(error) assert.ErrorIs(t, err, ErrorMenuNotFound) } @@ -178,7 +183,7 @@ func TestQueryGet_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{ + _ = taskRun(c, "contextmenu.add", TaskAdd{ Name: "my-menu", Menu: ContextMenuDef{ Name: "my-menu", @@ -186,10 +191,9 @@ func TestQueryGet_Good(t *testing.T) { }, }) - result, handled, err := c.QUERY(QueryGet{Name: "my-menu"}) - require.NoError(t, err) - assert.True(t, handled) - def := result.(*ContextMenuDef) + r := c.QUERY(QueryGet{Name: "my-menu"}) + require.True(t, r.OK) + def := r.Value.(*ContextMenuDef) assert.Equal(t, "my-menu", def.Name) assert.Len(t, def.Items, 1) } @@ -198,23 +202,21 @@ func TestQueryGet_Good_NotFound(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - result, handled, err := c.QUERY(QueryGet{Name: "missing"}) - require.NoError(t, err) - assert.True(t, handled) - assert.Nil(t, result) + r := c.QUERY(QueryGet{Name: "missing"}) + require.True(t, r.OK) + assert.Nil(t, r.Value) } func TestQueryList_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Name: "a", Menu: ContextMenuDef{Name: "a"}}) - _, _, _ = c.PERFORM(TaskAdd{Name: "b", Menu: ContextMenuDef{Name: "b"}}) + _ = taskRun(c, "contextmenu.add", TaskAdd{Name: "a", Menu: ContextMenuDef{Name: "a"}}) + _ = taskRun(c, "contextmenu.add", TaskAdd{Name: "b", Menu: ContextMenuDef{Name: "b"}}) - result, handled, err := c.QUERY(QueryList{}) - require.NoError(t, err) - assert.True(t, handled) - list := result.(map[string]ContextMenuDef) + r := c.QUERY(QueryList{}) + require.True(t, r.OK) + list := r.Value.(map[string]ContextMenuDef) assert.Len(t, list, 2) } @@ -222,10 +224,9 @@ func TestQueryList_Good_Empty(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - result, handled, err := c.QUERY(QueryList{}) - require.NoError(t, err) - assert.True(t, handled) - list := result.(map[string]ContextMenuDef) + r := c.QUERY(QueryList{}) + require.True(t, r.OK) + list := r.Value.(map[string]ContextMenuDef) assert.Len(t, list, 0) } @@ -236,16 +237,16 @@ func TestTaskAdd_Good_ClickBroadcast(t *testing.T) { // Capture broadcast actions var clicked ActionItemClicked var mu sync.Mutex - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionItemClicked); ok { mu.Lock() clicked = a mu.Unlock() } - return nil + return core.Result{OK: true} }) - _, _, _ = c.PERFORM(TaskAdd{ + _ = taskRun(c, "contextmenu.add", TaskAdd{ Name: "file-menu", Menu: ContextMenuDef{ Name: "file-menu", @@ -269,7 +270,7 @@ func TestTaskAdd_Good_SubmenuItems(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, handled, err := c.PERFORM(TaskAdd{ + r := taskRun(c, "contextmenu.add", TaskAdd{ Name: "nested", Menu: ContextMenuDef{ Name: "nested", @@ -283,19 +284,18 @@ func TestTaskAdd_Good_SubmenuItems(t *testing.T) { }, }, }) - require.NoError(t, err) - assert.True(t, handled) + require.True(t, r.OK) - result, _, _ := c.QUERY(QueryGet{Name: "nested"}) - def := result.(*ContextMenuDef) + qr := c.QUERY(QueryGet{Name: "nested"}) + def := qr.Value.(*ContextMenuDef) assert.Len(t, def.Items, 3) assert.Len(t, def.Items[0].Items, 2) // submenu children } func TestQueryList_Bad_NoService(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.QUERY(QueryList{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(QueryList{}) + assert.False(t, r.OK) } // --- TaskUpdate --- @@ -305,23 +305,22 @@ func TestTaskUpdate_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{ + _ = taskRun(c, "contextmenu.add", TaskAdd{ Name: "edit-menu", Menu: ContextMenuDef{Name: "edit-menu", Items: []MenuItemDef{{Label: "Cut", ActionID: "cut"}}}, }) - _, handled, err := c.PERFORM(TaskUpdate{ + r := taskRun(c, "contextmenu.update", TaskUpdate{ Name: "edit-menu", Menu: ContextMenuDef{Name: "edit-menu", Items: []MenuItemDef{ {Label: "Cut", ActionID: "cut"}, {Label: "Copy", ActionID: "copy"}, }}, }) - require.NoError(t, err) - assert.True(t, handled) + require.True(t, r.OK) - result, _, _ := c.QUERY(QueryGet{Name: "edit-menu"}) - def := result.(*ContextMenuDef) + qr := c.QUERY(QueryGet{Name: "edit-menu"}) + def := qr.Value.(*ContextMenuDef) assert.Len(t, def.Items, 2) assert.Equal(t, "Copy", def.Items[1].Label) } @@ -331,11 +330,12 @@ func TestTaskUpdate_Bad_NotFound(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, handled, err := c.PERFORM(TaskUpdate{ + r := taskRun(c, "contextmenu.update", TaskUpdate{ Name: "ghost", Menu: ContextMenuDef{Name: "ghost"}, }) - assert.True(t, handled) + assert.False(t, r.OK) + err, _ := r.Value.(error) assert.ErrorIs(t, err, ErrorMenuNotFound) } @@ -344,7 +344,7 @@ func TestTaskUpdate_Ugly_PlatformRemoveError(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{ + _ = taskRun(c, "contextmenu.add", TaskAdd{ Name: "tricky", Menu: ContextMenuDef{Name: "tricky"}, }) @@ -353,12 +353,11 @@ func TestTaskUpdate_Ugly_PlatformRemoveError(t *testing.T) { mp.removeErr = ErrorMenuNotFound // reuse sentinel as a platform-level error mp.mu.Unlock() - _, handled, err := c.PERFORM(TaskUpdate{ + r := taskRun(c, "contextmenu.update", TaskUpdate{ Name: "tricky", Menu: ContextMenuDef{Name: "tricky", Items: []MenuItemDef{{Label: "X", ActionID: "x"}}}, }) - assert.True(t, handled) - assert.Error(t, err) + assert.False(t, r.OK) } // --- TaskDestroy --- @@ -368,14 +367,13 @@ func TestTaskDestroy_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Name: "doomed", Menu: ContextMenuDef{Name: "doomed"}}) + _ = taskRun(c, "contextmenu.add", TaskAdd{Name: "doomed", Menu: ContextMenuDef{Name: "doomed"}}) - _, handled, err := c.PERFORM(TaskDestroy{Name: "doomed"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "contextmenu.destroy", TaskDestroy{Name: "doomed"}) + require.True(t, r.OK) - result, _, _ := c.QUERY(QueryGet{Name: "doomed"}) - assert.Nil(t, result) + qr := c.QUERY(QueryGet{Name: "doomed"}) + assert.Nil(t, qr.Value) _, ok := mp.Get("doomed") assert.False(t, ok) @@ -386,8 +384,9 @@ func TestTaskDestroy_Bad_NotFound(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, handled, err := c.PERFORM(TaskDestroy{Name: "nonexistent"}) - assert.True(t, handled) + r := taskRun(c, "contextmenu.destroy", TaskDestroy{Name: "nonexistent"}) + assert.False(t, r.OK) + err, _ := r.Value.(error) assert.ErrorIs(t, err, ErrorMenuNotFound) } @@ -396,15 +395,14 @@ func TestTaskDestroy_Ugly_PlatformError(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Name: "frail", Menu: ContextMenuDef{Name: "frail"}}) + _ = taskRun(c, "contextmenu.add", TaskAdd{Name: "frail", Menu: ContextMenuDef{Name: "frail"}}) mp.mu.Lock() mp.removeErr = ErrorMenuNotFound mp.mu.Unlock() - _, handled, err := c.PERFORM(TaskDestroy{Name: "frail"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "contextmenu.destroy", TaskDestroy{Name: "frail"}) + assert.False(t, r.OK) } // --- QueryGetAll --- @@ -414,13 +412,12 @@ func TestQueryGetAll_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Name: "x", Menu: ContextMenuDef{Name: "x"}}) - _, _, _ = c.PERFORM(TaskAdd{Name: "y", Menu: ContextMenuDef{Name: "y"}}) + _ = taskRun(c, "contextmenu.add", TaskAdd{Name: "x", Menu: ContextMenuDef{Name: "x"}}) + _ = taskRun(c, "contextmenu.add", TaskAdd{Name: "y", Menu: ContextMenuDef{Name: "y"}}) - result, handled, err := c.QUERY(QueryGetAll{}) - require.NoError(t, err) - assert.True(t, handled) - all := result.(map[string]ContextMenuDef) + r := c.QUERY(QueryGetAll{}) + require.True(t, r.OK) + all := r.Value.(map[string]ContextMenuDef) assert.Len(t, all, 2) assert.Contains(t, all, "x") assert.Contains(t, all, "y") @@ -431,18 +428,17 @@ func TestQueryGetAll_Bad_Empty(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - result, handled, err := c.QUERY(QueryGetAll{}) - require.NoError(t, err) - assert.True(t, handled) - all := result.(map[string]ContextMenuDef) + r := c.QUERY(QueryGetAll{}) + require.True(t, r.OK) + all := r.Value.(map[string]ContextMenuDef) assert.Len(t, all, 0) } func TestQueryGetAll_Ugly_NoService(t *testing.T) { // No contextmenu service — query is unhandled - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.QUERY(QueryGetAll{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(QueryGetAll{}) + assert.False(t, r.OK) } // --- OnShutdown --- @@ -452,10 +448,10 @@ func TestOnShutdown_Good_CleansUpMenus(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Name: "alpha", Menu: ContextMenuDef{Name: "alpha"}}) - _, _, _ = c.PERFORM(TaskAdd{Name: "beta", Menu: ContextMenuDef{Name: "beta"}}) + _ = taskRun(c, "contextmenu.add", TaskAdd{Name: "alpha", Menu: ContextMenuDef{Name: "alpha"}}) + _ = taskRun(c, "contextmenu.add", TaskAdd{Name: "beta", Menu: ContextMenuDef{Name: "beta"}}) - require.NoError(t, c.ServiceShutdown(t.Context())) + require.True(t, c.ServiceShutdown(t.Context()).OK) assert.Len(t, mp.menus, 0) } @@ -465,7 +461,7 @@ func TestOnShutdown_Bad_NothingRegistered(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - assert.NoError(t, c.ServiceShutdown(t.Context())) + assert.True(t, c.ServiceShutdown(t.Context()).OK) } func TestOnShutdown_Ugly_PlatformRemoveErrors(t *testing.T) { @@ -473,12 +469,12 @@ func TestOnShutdown_Ugly_PlatformRemoveErrors(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Name: "stubborn", Menu: ContextMenuDef{Name: "stubborn"}}) + _ = taskRun(c, "contextmenu.add", TaskAdd{Name: "stubborn", Menu: ContextMenuDef{Name: "stubborn"}}) mp.mu.Lock() mp.removeErr = ErrorMenuNotFound mp.mu.Unlock() // Shutdown must not return an error even if platform Remove fails - assert.NoError(t, c.ServiceShutdown(t.Context())) + assert.True(t, c.ServiceShutdown(t.Context()).OK) } diff --git a/pkg/dialog/service.go b/pkg/dialog/service.go index fcc48e1..989ef18 100644 --- a/pkg/dialog/service.go +++ b/pkg/dialog/service.go @@ -4,7 +4,7 @@ package dialog import ( "context" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" ) type Options struct{} @@ -17,95 +17,83 @@ type Service struct { // Register(p) binds the dialog service to a Core instance. // // c.WithService(dialog.Register(wailsDialog)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, - }, nil + }, OK: true} } } -func (s *Service) OnStartup(ctx context.Context) error { - s.Core().RegisterTask(s.handleTask) - return nil -} - -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil -} - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskOpenFile: - paths, err := s.platform.OpenFile(t.Options) - return paths, true, err - - case TaskOpenFileWithOptions: - options := OpenFileOptions{} - if t.Options != nil { - options = *t.Options +func (s *Service) OnStartup(_ context.Context) core.Result { + s.Core().Action("dialog.openFile", func(_ context.Context, opts core.Options) core.Result { + var openOpts OpenFileOptions + switch v := opts.Get("task").Value.(type) { + case TaskOpenFile: + openOpts = v.Options + case TaskOpenFileWithOptions: + if v.Options != nil { + openOpts = *v.Options + } } - paths, err := s.platform.OpenFile(options) - return paths, true, err - - case TaskSaveFile: - path, err := s.platform.SaveFile(t.Options) - return path, true, err - - case TaskSaveFileWithOptions: - options := SaveFileOptions{} - if t.Options != nil { - options = *t.Options + paths, err := s.platform.OpenFile(openOpts) + return core.Result{}.New(paths, err) + }) + s.Core().Action("dialog.saveFile", func(_ context.Context, opts core.Options) core.Result { + var saveOpts SaveFileOptions + switch v := opts.Get("task").Value.(type) { + case TaskSaveFile: + saveOpts = v.Options + case TaskSaveFileWithOptions: + if v.Options != nil { + saveOpts = *v.Options + } } - path, err := s.platform.SaveFile(options) - return path, true, err - - case TaskOpenDirectory: + path, err := s.platform.SaveFile(saveOpts) + return core.Result{}.New(path, err) + }) + s.Core().Action("dialog.openDirectory", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskOpenDirectory) path, err := s.platform.OpenDirectory(t.Options) - return path, true, err - - case TaskMessageDialog: + return core.Result{}.New(path, err) + }) + s.Core().Action("dialog.message", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskMessageDialog) button, err := s.platform.MessageDialog(t.Options) - return button, true, err - - case TaskInfo: + return core.Result{}.New(button, err) + }) + s.Core().Action("dialog.info", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskInfo) button, err := s.platform.MessageDialog(MessageDialogOptions{ - Type: DialogInfo, - Title: t.Title, - Message: t.Message, - Buttons: t.Buttons, + Type: DialogInfo, Title: t.Title, Message: t.Message, Buttons: t.Buttons, }) - return button, true, err - - case TaskQuestion: + return core.Result{}.New(button, err) + }) + s.Core().Action("dialog.question", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskQuestion) button, err := s.platform.MessageDialog(MessageDialogOptions{ - Type: DialogQuestion, - Title: t.Title, - Message: t.Message, - Buttons: t.Buttons, + Type: DialogQuestion, Title: t.Title, Message: t.Message, Buttons: t.Buttons, }) - return button, true, err - - case TaskWarning: + return core.Result{}.New(button, err) + }) + s.Core().Action("dialog.warning", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskWarning) button, err := s.platform.MessageDialog(MessageDialogOptions{ - Type: DialogWarning, - Title: t.Title, - Message: t.Message, - Buttons: t.Buttons, + Type: DialogWarning, Title: t.Title, Message: t.Message, Buttons: t.Buttons, }) - return button, true, err - - case TaskError: + return core.Result{}.New(button, err) + }) + s.Core().Action("dialog.error", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskError) button, err := s.platform.MessageDialog(MessageDialogOptions{ - Type: DialogError, - Title: t.Title, - Message: t.Message, - Buttons: t.Buttons, + Type: DialogError, Title: t.Title, Message: t.Message, Buttons: t.Buttons, }) - return button, true, err - - default: - return nil, false, nil - } + return core.Result{}.New(button, err) + }) + return core.Result{OK: true} +} + +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } diff --git a/pkg/dialog/service_test.go b/pkg/dialog/service_test.go index 4c3be0d..78ebdda 100644 --- a/pkg/dialog/service_test.go +++ b/pkg/dialog/service_test.go @@ -5,7 +5,7 @@ import ( "context" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -50,15 +50,20 @@ func newTestService(t *testing.T) (*mockPlatform, *core.Core) { openDirPath: "/tmp/dir", messageButton: "OK", } - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) return mock, c } +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + // --- Good path tests --- func TestService_Register_Good(t *testing.T) { @@ -71,12 +76,11 @@ func TestService_TaskOpenFile_Good(t *testing.T) { mock, c := newTestService(t) mock.openFilePaths = []string{"/a.txt", "/b.txt"} - result, handled, err := c.PERFORM(TaskOpenFile{ + r := taskRun(c, "dialog.openFile", TaskOpenFile{ Options: OpenFileOptions{Title: "Pick", AllowMultiple: true}, }) - require.NoError(t, err) - assert.True(t, handled) - paths := result.([]string) + require.True(t, r.OK) + paths := r.Value.([]string) assert.Equal(t, []string{"/a.txt", "/b.txt"}, paths) assert.Equal(t, "Pick", mock.lastOpenOpts.Title) assert.True(t, mock.lastOpenOpts.AllowMultiple) @@ -87,15 +91,14 @@ func TestService_TaskOpenFile_FileFilters_Good(t *testing.T) { mock.openFilePaths = []string{"/img.png"} filters := []FileFilter{{DisplayName: "Images", Pattern: "*.png;*.jpg"}} - result, handled, err := c.PERFORM(TaskOpenFile{ + r := taskRun(c, "dialog.openFile", TaskOpenFile{ Options: OpenFileOptions{ Title: "Select image", Filters: filters, }, }) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, []string{"/img.png"}, result.([]string)) + require.True(t, r.OK) + assert.Equal(t, []string{"/img.png"}, r.Value.([]string)) require.Len(t, mock.lastOpenOpts.Filters, 1) assert.Equal(t, "Images", mock.lastOpenOpts.Filters[0].DisplayName) assert.Equal(t, "*.png;*.jpg", mock.lastOpenOpts.Filters[0].Pattern) @@ -105,26 +108,25 @@ func TestService_TaskOpenFile_MultipleSelection_Good(t *testing.T) { mock, c := newTestService(t) mock.openFilePaths = []string{"/a.txt", "/b.txt", "/c.txt"} - result, handled, err := c.PERFORM(TaskOpenFile{ + r := taskRun(c, "dialog.openFile", TaskOpenFile{ Options: OpenFileOptions{AllowMultiple: true}, }) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, []string{"/a.txt", "/b.txt", "/c.txt"}, result.([]string)) + require.True(t, r.OK) + assert.Equal(t, []string{"/a.txt", "/b.txt", "/c.txt"}, r.Value.([]string)) assert.True(t, mock.lastOpenOpts.AllowMultiple) } func TestService_TaskOpenFile_CanChooseOptions_Good(t *testing.T) { mock, c := newTestService(t) - _, _, err := c.PERFORM(TaskOpenFile{ + r := taskRun(c, "dialog.openFile", TaskOpenFile{ Options: OpenFileOptions{ CanChooseFiles: true, CanChooseDirectories: true, ShowHiddenFiles: true, }, }) - require.NoError(t, err) + require.True(t, r.OK) assert.True(t, mock.lastOpenOpts.CanChooseFiles) assert.True(t, mock.lastOpenOpts.CanChooseDirectories) assert.True(t, mock.lastOpenOpts.ShowHiddenFiles) @@ -135,14 +137,13 @@ func TestService_TaskOpenFileWithOptions_Good(t *testing.T) { mock.openFilePaths = []string{"/log.txt"} opts := &OpenFileOptions{ - Title: "Select log", - AllowMultiple: false, + Title: "Select log", + AllowMultiple: false, ShowHiddenFiles: true, } - result, handled, err := c.PERFORM(TaskOpenFileWithOptions{Options: opts}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, []string{"/log.txt"}, result.([]string)) + r := taskRun(c, "dialog.openFile", TaskOpenFileWithOptions{Options: opts}) + require.True(t, r.OK) + assert.Equal(t, []string{"/log.txt"}, r.Value.([]string)) assert.Equal(t, "Select log", mock.lastOpenOpts.Title) assert.True(t, mock.lastOpenOpts.ShowHiddenFiles) } @@ -150,29 +151,27 @@ func TestService_TaskOpenFileWithOptions_Good(t *testing.T) { func TestService_TaskOpenFileWithOptions_NilOptions_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.PERFORM(TaskOpenFileWithOptions{Options: nil}) - require.NoError(t, err) - assert.True(t, handled) - assert.NotNil(t, result) + r := taskRun(c, "dialog.openFile", TaskOpenFileWithOptions{Options: nil}) + require.True(t, r.OK) + assert.NotNil(t, r.Value) } func TestService_TaskSaveFile_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.PERFORM(TaskSaveFile{ + r := taskRun(c, "dialog.saveFile", TaskSaveFile{ Options: SaveFileOptions{Filename: "out.txt"}, }) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "/tmp/save.txt", result) + require.True(t, r.OK) + assert.Equal(t, "/tmp/save.txt", r.Value) } func TestService_TaskSaveFile_ShowHidden_Good(t *testing.T) { mock, c := newTestService(t) - _, _, err := c.PERFORM(TaskSaveFile{ + r := taskRun(c, "dialog.saveFile", TaskSaveFile{ Options: SaveFileOptions{Filename: "out.txt", ShowHiddenFiles: true}, }) - require.NoError(t, err) + require.True(t, r.OK) assert.True(t, mock.lastSaveOpts.ShowHiddenFiles) } @@ -185,10 +184,9 @@ func TestService_TaskSaveFileWithOptions_Good(t *testing.T) { Filename: "data.json", Filters: []FileFilter{{DisplayName: "JSON", Pattern: "*.json"}}, } - result, handled, err := c.PERFORM(TaskSaveFileWithOptions{Options: opts}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "/exports/data.json", result.(string)) + r := taskRun(c, "dialog.saveFile", TaskSaveFileWithOptions{Options: opts}) + require.True(t, r.OK) + assert.Equal(t, "/exports/data.json", r.Value.(string)) assert.Equal(t, "Export data", mock.lastSaveOpts.Title) require.Len(t, mock.lastSaveOpts.Filters, 1) assert.Equal(t, "JSON", mock.lastSaveOpts.Filters[0].DisplayName) @@ -197,21 +195,19 @@ func TestService_TaskSaveFileWithOptions_Good(t *testing.T) { func TestService_TaskSaveFileWithOptions_NilOptions_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.PERFORM(TaskSaveFileWithOptions{Options: nil}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "/tmp/save.txt", result) + r := taskRun(c, "dialog.saveFile", TaskSaveFileWithOptions{Options: nil}) + require.True(t, r.OK) + assert.Equal(t, "/tmp/save.txt", r.Value) } func TestService_TaskOpenDirectory_Good(t *testing.T) { mock, c := newTestService(t) - result, handled, err := c.PERFORM(TaskOpenDirectory{ + r := taskRun(c, "dialog.openDirectory", TaskOpenDirectory{ Options: OpenDirectoryOptions{Title: "Pick Dir", ShowHiddenFiles: true}, }) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "/tmp/dir", result) + require.True(t, r.OK) + assert.Equal(t, "/tmp/dir", r.Value) assert.Equal(t, "Pick Dir", mock.lastDirOpts.Title) assert.True(t, mock.lastDirOpts.ShowHiddenFiles) } @@ -220,15 +216,14 @@ func TestService_TaskMessageDialog_Good(t *testing.T) { mock, c := newTestService(t) mock.messageButton = "Yes" - result, handled, err := c.PERFORM(TaskMessageDialog{ + r := taskRun(c, "dialog.message", TaskMessageDialog{ Options: MessageDialogOptions{ Type: DialogQuestion, Title: "Confirm", Message: "Sure?", Buttons: []string{"Yes", "No"}, }, }) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "Yes", result) + require.True(t, r.OK) + assert.Equal(t, "Yes", r.Value) assert.Equal(t, DialogQuestion, mock.lastMsgOpts.Type) } @@ -236,12 +231,11 @@ func TestService_TaskInfo_Good(t *testing.T) { mock, c := newTestService(t) mock.messageButton = "OK" - result, handled, err := c.PERFORM(TaskInfo{ + r := taskRun(c, "dialog.info", TaskInfo{ Title: "Done", Message: "File saved successfully.", }) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "OK", result.(string)) + require.True(t, r.OK) + assert.Equal(t, "OK", r.Value.(string)) assert.Equal(t, DialogInfo, mock.lastMsgOpts.Type) assert.Equal(t, "Done", mock.lastMsgOpts.Title) assert.Equal(t, "File saved successfully.", mock.lastMsgOpts.Message) @@ -251,12 +245,11 @@ func TestService_TaskInfo_WithButtons_Good(t *testing.T) { mock, c := newTestService(t) mock.messageButton = "Close" - result, handled, err := c.PERFORM(TaskInfo{ + r := taskRun(c, "dialog.info", TaskInfo{ Title: "Notice", Message: "Update available.", Buttons: []string{"Close", "Later"}, }) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "Close", result.(string)) + require.True(t, r.OK) + assert.Equal(t, "Close", r.Value.(string)) assert.Equal(t, []string{"Close", "Later"}, mock.lastMsgOpts.Buttons) } @@ -264,12 +257,11 @@ func TestService_TaskQuestion_Good(t *testing.T) { mock, c := newTestService(t) mock.messageButton = "Yes" - result, handled, err := c.PERFORM(TaskQuestion{ + r := taskRun(c, "dialog.question", TaskQuestion{ Title: "Confirm deletion", Message: "Delete file?", Buttons: []string{"Yes", "No"}, }) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "Yes", result.(string)) + require.True(t, r.OK) + assert.Equal(t, "Yes", r.Value.(string)) assert.Equal(t, DialogQuestion, mock.lastMsgOpts.Type) assert.Equal(t, "Confirm deletion", mock.lastMsgOpts.Title) } @@ -278,12 +270,11 @@ func TestService_TaskWarning_Good(t *testing.T) { mock, c := newTestService(t) mock.messageButton = "OK" - result, handled, err := c.PERFORM(TaskWarning{ + r := taskRun(c, "dialog.warning", TaskWarning{ Title: "Disk full", Message: "Storage is critically low.", Buttons: []string{"OK"}, }) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "OK", result.(string)) + require.True(t, r.OK) + assert.Equal(t, "OK", r.Value.(string)) assert.Equal(t, DialogWarning, mock.lastMsgOpts.Type) assert.Equal(t, "Disk full", mock.lastMsgOpts.Title) } @@ -292,12 +283,11 @@ func TestService_TaskError_Good(t *testing.T) { mock, c := newTestService(t) mock.messageButton = "OK" - result, handled, err := c.PERFORM(TaskError{ + r := taskRun(c, "dialog.error", TaskError{ Title: "Operation failed", Message: "could not write file: permission denied", }) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "OK", result.(string)) + require.True(t, r.OK) + assert.Equal(t, "OK", r.Value.(string)) assert.Equal(t, DialogError, mock.lastMsgOpts.Type) assert.Equal(t, "Operation failed", mock.lastMsgOpts.Title) assert.Equal(t, "could not write file: permission denied", mock.lastMsgOpts.Message) @@ -306,45 +296,45 @@ func TestService_TaskError_Good(t *testing.T) { // --- Bad path tests --- func TestService_TaskOpenFile_Bad(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskOpenFile{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("dialog.openFile").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestService_TaskOpenFileWithOptions_Bad(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskOpenFileWithOptions{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("dialog.openFile").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestService_TaskSaveFileWithOptions_Bad(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskSaveFileWithOptions{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("dialog.saveFile").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestService_TaskInfo_Bad(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskInfo{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("dialog.info").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestService_TaskQuestion_Bad(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskQuestion{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("dialog.question").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestService_TaskWarning_Bad(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskWarning{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("dialog.warning").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestService_TaskError_Bad(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskError{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("dialog.error").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } // --- Ugly path tests --- @@ -353,12 +343,11 @@ func TestService_TaskOpenFile_Ugly(t *testing.T) { mock, c := newTestService(t) mock.openFilePaths = nil - result, handled, err := c.PERFORM(TaskOpenFile{ + r := taskRun(c, "dialog.openFile", TaskOpenFile{ Options: OpenFileOptions{Title: "Pick"}, }) - require.NoError(t, err) - assert.True(t, handled) - assert.Nil(t, result.([]string)) + require.True(t, r.OK) + assert.Nil(t, r.Value.([]string)) } func TestService_TaskOpenFileWithOptions_MultipleFilters_Ugly(t *testing.T) { @@ -373,10 +362,9 @@ func TestService_TaskOpenFileWithOptions_MultipleFilters_Ugly(t *testing.T) { {DisplayName: "All files", Pattern: "*.*"}, }, } - result, handled, err := c.PERFORM(TaskOpenFileWithOptions{Options: opts}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, []string{"/doc.pdf"}, result.([]string)) + r := taskRun(c, "dialog.openFile", TaskOpenFileWithOptions{Options: opts}) + require.True(t, r.OK) + assert.Equal(t, []string{"/doc.pdf"}, r.Value.([]string)) assert.Len(t, mock.lastOpenOpts.Filters, 3) } @@ -389,15 +377,14 @@ func TestService_TaskSaveFileWithOptions_FiltersAndHidden_Ugly(t *testing.T) { ShowHiddenFiles: true, Filters: []FileFilter{{DisplayName: "CSV", Pattern: "*.csv"}}, } - _, _, err := c.PERFORM(TaskSaveFileWithOptions{Options: opts}) - require.NoError(t, err) + r := taskRun(c, "dialog.saveFile", TaskSaveFileWithOptions{Options: opts}) + require.True(t, r.OK) assert.True(t, mock.lastSaveOpts.ShowHiddenFiles) assert.Equal(t, "output.csv", mock.lastSaveOpts.Filename) } func TestService_UnknownTask_Ugly(t *testing.T) { - type unknownTask struct{} - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(unknownTask{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("dialog.nonexistent").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } diff --git a/pkg/display/display.go b/pkg/display/display.go index f6cca53..86fa317 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -8,10 +8,9 @@ import ( "runtime" "forge.lthn.ai/core/config" - coreerr "forge.lthn.ai/core/go-log" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" - "forge.lthn.ai/core/gui/pkg/browser" "forge.lthn.ai/core/gui/pkg/contextmenu" "forge.lthn.ai/core/gui/pkg/dialog" "forge.lthn.ai/core/gui/pkg/dock" @@ -58,26 +57,45 @@ func New() (*Service, error) { // Register binds the display service to a Core instance. // core.WithService(display.Register(app)) // production (Wails app) // core.WithService(display.Register(nil)) // tests (no Wails runtime) -func Register(wailsApp *application.App) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { +func Register(wailsApp *application.App) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { s, err := New() if err != nil { - return nil, err + return core.Result{Value: err, OK: false} } s.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{}) s.wailsApp = wailsApp - return s, nil + return core.Result{Value: s, OK: true} } } // OnStartup loads config and registers handlers before sub-services start. // Config handlers are registered first — sub-services query them during their own OnStartup. -func (s *Service) OnStartup(ctx context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { s.loadConfig() - // Register config query/task handlers — available NOW for sub-services + // Register config query handler — available NOW for sub-services s.Core().RegisterQuery(s.handleConfigQuery) - s.Core().RegisterTask(s.handleConfigTask) + + // Register config save actions + s.Core().Action("display.saveWindowConfig", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(window.TaskSaveConfig) + s.configData["window"] = t.Config + s.persistSection("window", t.Config) + return core.Result{OK: true} + }) + s.Core().Action("display.saveSystrayConfig", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(systray.TaskSaveConfig) + s.configData["systray"] = t.Config + s.persistSection("systray", t.Config) + return core.Result{OK: true} + }) + s.Core().Action("display.saveMenuConfig", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(menu.TaskSaveConfig) + s.configData["menu"] = t.Config + s.persistSection("menu", t.Config) + return core.Result{OK: true} + }) // Initialise Wails wrappers if app is available (nil in tests) if s.wailsApp != nil { @@ -85,14 +103,14 @@ func (s *Service) OnStartup(ctx context.Context) error { s.events = NewWSEventManager() } - return nil + return core.Result{OK: true} } // HandleIPCEvents bridges IPC actions from sub-services to WebSocket events for TS apps. -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { +func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result { switch m := msg.(type) { case core.ActionServiceStartup: - // All services have completed OnStartup — safe to PERFORM on sub-services + // All services have completed OnStartup — safe to call sub-services s.buildMenu() s.setupTray() case window.ActionWindowOpened: @@ -225,7 +243,7 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { Data: map[string]any{"command": m.Command}}) } } - return nil + return core.Result{OK: true} } // WSMessage represents a command received from a WebSocket client. @@ -244,150 +262,175 @@ func wsRequire(data map[string]any, key string) (string, error) { } // handleWSMessage bridges WebSocket commands to IPC calls. -func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) { - var result any - var handled bool - var err error +func (s *Service) handleWSMessage(msg WSMessage) core.Result { + ctx := context.Background() + c := s.Core() switch msg.Action { case "keybinding:add": accelerator, _ := msg.Data["accelerator"].(string) description, _ := msg.Data["description"].(string) - result, handled, err = s.Core().PERFORM(keybinding.TaskAdd{ - Accelerator: accelerator, Description: description, - }) + return c.Action("keybinding.add").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: keybinding.TaskAdd{Accelerator: accelerator, Description: description}}, + )) case "keybinding:remove": accelerator, _ := msg.Data["accelerator"].(string) - result, handled, err = s.Core().PERFORM(keybinding.TaskRemove{ - Accelerator: accelerator, - }) + return c.Action("keybinding.remove").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: keybinding.TaskRemove{Accelerator: accelerator}}, + )) case "keybinding:list": - result, handled, err = s.Core().QUERY(keybinding.QueryList{}) + return c.QUERY(keybinding.QueryList{}) case "browser:open-url": url, _ := msg.Data["url"].(string) - result, handled, err = s.Core().PERFORM(browser.TaskOpenURL{URL: url}) + return c.Action("browser.openURL").Run(ctx, core.NewOptions( + core.Option{Key: "url", Value: url}, + )) case "browser:open-file": path, _ := msg.Data["path"].(string) - result, handled, err = s.Core().PERFORM(browser.TaskOpenFile{Path: path}) + return c.Action("browser.openFile").Run(ctx, core.NewOptions( + core.Option{Key: "path", Value: path}, + )) case "dock:show": - result, handled, err = s.Core().PERFORM(dock.TaskShowIcon{}) + return c.Action("dock.showIcon").Run(ctx, core.NewOptions()) case "dock:hide": - result, handled, err = s.Core().PERFORM(dock.TaskHideIcon{}) + return c.Action("dock.hideIcon").Run(ctx, core.NewOptions()) case "dock:badge": label, _ := msg.Data["label"].(string) - result, handled, err = s.Core().PERFORM(dock.TaskSetBadge{Label: label}) + return c.Action("dock.setBadge").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: dock.TaskSetBadge{Label: label}}, + )) case "dock:badge-remove": - result, handled, err = s.Core().PERFORM(dock.TaskRemoveBadge{}) + return c.Action("dock.removeBadge").Run(ctx, core.NewOptions()) case "dock:visible": - result, handled, err = s.Core().QUERY(dock.QueryVisible{}) + return c.QUERY(dock.QueryVisible{}) case "contextmenu:add": name, _ := msg.Data["name"].(string) menuJSON, _ := json.Marshal(msg.Data["menu"]) var menuDef contextmenu.ContextMenuDef _ = json.Unmarshal(menuJSON, &menuDef) - result, handled, err = s.Core().PERFORM(contextmenu.TaskAdd{ - Name: name, Menu: menuDef, - }) + return c.Action("contextmenu.add").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: contextmenu.TaskAdd{Name: name, Menu: menuDef}}, + )) case "contextmenu:remove": name, _ := msg.Data["name"].(string) - result, handled, err = s.Core().PERFORM(contextmenu.TaskRemove{Name: name}) + return c.Action("contextmenu.remove").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: contextmenu.TaskRemove{Name: name}}, + )) case "contextmenu:get": name, _ := msg.Data["name"].(string) - result, handled, err = s.Core().QUERY(contextmenu.QueryGet{Name: name}) + return c.QUERY(contextmenu.QueryGet{Name: name}) case "contextmenu:list": - result, handled, err = s.Core().QUERY(contextmenu.QueryList{}) + return c.QUERY(contextmenu.QueryList{}) case "webview:eval": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } script, _ := msg.Data["script"].(string) - result, handled, err = s.Core().PERFORM(webview.TaskEvaluate{Window: w, Script: script}) + return c.Action("webview.evaluate").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskEvaluate{Window: w, Script: script}}, + )) case "webview:click": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } sel, e := wsRequire(msg.Data, "selector") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } - result, handled, err = s.Core().PERFORM(webview.TaskClick{Window: w, Selector: sel}) + return c.Action("webview.click").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskClick{Window: w, Selector: sel}}, + )) case "webview:type": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } sel, e := wsRequire(msg.Data, "selector") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } text, _ := msg.Data["text"].(string) - result, handled, err = s.Core().PERFORM(webview.TaskType{Window: w, Selector: sel, Text: text}) + return c.Action("webview.type").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskType{Window: w, Selector: sel, Text: text}}, + )) case "webview:navigate": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } url, e := wsRequire(msg.Data, "url") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } - result, handled, err = s.Core().PERFORM(webview.TaskNavigate{Window: w, URL: url}) + return c.Action("webview.navigate").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskNavigate{Window: w, URL: url}}, + )) case "webview:screenshot": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } - result, handled, err = s.Core().PERFORM(webview.TaskScreenshot{Window: w}) + return c.Action("webview.screenshot").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskScreenshot{Window: w}}, + )) case "webview:scroll": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } x, _ := msg.Data["x"].(float64) y, _ := msg.Data["y"].(float64) - result, handled, err = s.Core().PERFORM(webview.TaskScroll{Window: w, X: int(x), Y: int(y)}) + return c.Action("webview.scroll").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskScroll{Window: w, X: int(x), Y: int(y)}}, + )) case "webview:hover": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } sel, e := wsRequire(msg.Data, "selector") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } - result, handled, err = s.Core().PERFORM(webview.TaskHover{Window: w, Selector: sel}) + return c.Action("webview.hover").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskHover{Window: w, Selector: sel}}, + )) case "webview:select": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } sel, e := wsRequire(msg.Data, "selector") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } val, _ := msg.Data["value"].(string) - result, handled, err = s.Core().PERFORM(webview.TaskSelect{Window: w, Selector: sel, Value: val}) + return c.Action("webview.select").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskSelect{Window: w, Selector: sel, Value: val}}, + )) case "webview:check": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } sel, e := wsRequire(msg.Data, "selector") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } checked, _ := msg.Data["checked"].(bool) - result, handled, err = s.Core().PERFORM(webview.TaskCheck{Window: w, Selector: sel, Checked: checked}) + return c.Action("webview.check").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskCheck{Window: w, Selector: sel, Checked: checked}}, + )) case "webview:upload": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } sel, e := wsRequire(msg.Data, "selector") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } pathsRaw, _ := msg.Data["paths"].([]any) var paths []string @@ -396,102 +439,112 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) { paths = append(paths, ps) } } - result, handled, err = s.Core().PERFORM(webview.TaskUploadFile{Window: w, Selector: sel, Paths: paths}) + return c.Action("webview.uploadFile").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskUploadFile{Window: w, Selector: sel, Paths: paths}}, + )) case "webview:viewport": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } width, _ := msg.Data["width"].(float64) height, _ := msg.Data["height"].(float64) - result, handled, err = s.Core().PERFORM(webview.TaskSetViewport{Window: w, Width: int(width), Height: int(height)}) + return c.Action("webview.setViewport").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskSetViewport{Window: w, Width: int(width), Height: int(height)}}, + )) case "webview:clear-console": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } - result, handled, err = s.Core().PERFORM(webview.TaskClearConsole{Window: w}) + return c.Action("webview.clearConsole").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: webview.TaskClearConsole{Window: w}}, + )) case "webview:console": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } level, _ := msg.Data["level"].(string) limit := 100 if l, ok := msg.Data["limit"].(float64); ok { limit = int(l) } - result, handled, err = s.Core().QUERY(webview.QueryConsole{Window: w, Level: level, Limit: limit}) + return c.QUERY(webview.QueryConsole{Window: w, Level: level, Limit: limit}) case "webview:query": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } sel, e := wsRequire(msg.Data, "selector") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } - result, handled, err = s.Core().QUERY(webview.QuerySelector{Window: w, Selector: sel}) + return c.QUERY(webview.QuerySelector{Window: w, Selector: sel}) case "webview:query-all": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } sel, e := wsRequire(msg.Data, "selector") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } - result, handled, err = s.Core().QUERY(webview.QuerySelectorAll{Window: w, Selector: sel}) + return c.QUERY(webview.QuerySelectorAll{Window: w, Selector: sel}) case "webview:dom-tree": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } sel, _ := msg.Data["selector"].(string) // selector optional for dom-tree (defaults to root) - result, handled, err = s.Core().QUERY(webview.QueryDOMTree{Window: w, Selector: sel}) + return c.QUERY(webview.QueryDOMTree{Window: w, Selector: sel}) case "webview:url": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } - result, handled, err = s.Core().QUERY(webview.QueryURL{Window: w}) + return c.QUERY(webview.QueryURL{Window: w}) case "webview:title": w, e := wsRequire(msg.Data, "window") if e != nil { - return nil, false, e + return core.Result{Value: e, OK: false} } - result, handled, err = s.Core().QUERY(webview.QueryTitle{Window: w}) + return c.QUERY(webview.QueryTitle{Window: w}) default: - return nil, false, nil + return core.Result{} } - - return result, handled, err } // handleTrayAction processes tray menu item clicks. func (s *Service) handleTrayAction(actionID string) { + ctx := context.Background() + c := s.Core() switch actionID { case "open-desktop": // Show all windows infos := s.ListWindowInfos() for _, info := range infos { - _, _, _ = s.Core().PERFORM(window.TaskFocus{Name: info.Name}) + _ = c.Action("window.focus").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: window.TaskFocus{Name: info.Name}}, + )) } case "close-desktop": // Hide all windows — future: add TaskHideWindow case "env-info": // Query environment info via IPC and show as dialog - result, handled, _ := s.Core().QUERY(environment.QueryInfo{}) - if handled { - info := result.(environment.EnvironmentInfo) + r := c.QUERY(environment.QueryInfo{}) + if r.OK { + info, _ := r.Value.(environment.EnvironmentInfo) details := "OS: " + info.OS + "\nArch: " + info.Arch + "\nPlatform: " + info.Platform.Name + " " + info.Platform.Version - _, _, _ = s.Core().PERFORM(dialog.TaskMessageDialog{ - Options: dialog.MessageDialogOptions{ - Type: dialog.DialogInfo, Title: "Environment", - Message: details, Buttons: []string{"OK"}, - }, - }) + _ = c.Action("dialog.message").Run(ctx, core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskMessageDialog{ + Options: dialog.MessageDialogOptions{ + Type: dialog.DialogInfo, Title: "Environment", + Message: details, Buttons: []string{"OK"}, + }, + }}, + )) } case "quit": if s.app != nil { @@ -531,35 +584,16 @@ func (s *Service) loadConfigFrom(path string) { } } -func (s *Service) handleConfigQuery(c *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleConfigQuery(_ *core.Core, q core.Query) core.Result { switch q.(type) { case window.QueryConfig: - return s.configData["window"], true, nil + return core.Result{Value: s.configData["window"], OK: true} case systray.QueryConfig: - return s.configData["systray"], true, nil + return core.Result{Value: s.configData["systray"], OK: true} case menu.QueryConfig: - return s.configData["menu"], true, nil + return core.Result{Value: s.configData["menu"], OK: true} default: - return nil, false, nil - } -} - -func (s *Service) handleConfigTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case window.TaskSaveConfig: - s.configData["window"] = t.Config - s.persistSection("window", t.Config) - return nil, true, nil - case systray.TaskSaveConfig: - s.configData["systray"] = t.Config - s.persistSection("systray", t.Config) - return nil, true, nil - case menu.TaskSaveConfig: - s.configData["menu"] = t.Config - s.persistSection("menu", t.Config) - return nil, true, nil - default: - return nil, false, nil + return core.Result{} } } @@ -575,8 +609,8 @@ func (s *Service) persistSection(key string, value map[string]any) { // windowService returns the window service from Core, or nil if not registered. func (s *Service) windowService() *window.Service { - svc, err := core.ServiceFor[*window.Service](s.Core(), "window") - if err != nil { + svc, ok := core.ServiceFor[*window.Service](s.Core(), "window") + if !ok { return nil } return svc @@ -590,114 +624,202 @@ func (s *Service) OpenWindow(options ...window.WindowOption) error { if err != nil { return err } - _, _, err = s.Core().PERFORM(window.TaskOpenWindow{Window: spec}) - return err + r := s.Core().Action("window.open").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskOpenWindow{Window: spec}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + return coreerr.E("display.OpenWindow", "window.open action failed", nil) + } + return nil } // GetWindowInfo returns information about a window via IPC. func (s *Service) GetWindowInfo(name string) (*window.WindowInfo, error) { - result, handled, err := s.Core().QUERY(window.QueryWindowByName{Name: name}) - if err != nil { - return nil, err - } - if !handled { + r := s.Core().QUERY(window.QueryWindowByName{Name: name}) + if !r.OK { return nil, coreerr.E("display.GetWindowInfo", "window service not available", nil) } - info, _ := result.(*window.WindowInfo) + info, _ := r.Value.(*window.WindowInfo) return info, nil } // ListWindowInfos returns information about all tracked windows via IPC. func (s *Service) ListWindowInfos() []window.WindowInfo { - result, handled, _ := s.Core().QUERY(window.QueryWindowList{}) - if !handled { + r := s.Core().QUERY(window.QueryWindowList{}) + if !r.OK { return nil } - list, _ := result.([]window.WindowInfo) + list, _ := r.Value.([]window.WindowInfo) return list } // SetWindowPosition moves a window via IPC. func (s *Service) SetWindowPosition(name string, x, y int) error { - _, _, err := s.Core().PERFORM(window.TaskSetPosition{Name: name, X: x, Y: y}) - return err + r := s.Core().Action("window.setPosition").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetPosition{Name: name, X: x, Y: y}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // SetWindowSize resizes a window via IPC. func (s *Service) SetWindowSize(name string, width, height int) error { - _, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, Width: width, Height: height}) - return err + r := s.Core().Action("window.setSize").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetSize{Name: name, Width: width, Height: height}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // SetWindowBounds sets both position and size of a window via IPC. func (s *Service) SetWindowBounds(name string, x, y, width, height int) error { - if _, _, err := s.Core().PERFORM(window.TaskSetPosition{Name: name, X: x, Y: y}); err != nil { + if err := s.SetWindowPosition(name, x, y); err != nil { return err } - _, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, Width: width, Height: height}) - return err + return s.SetWindowSize(name, width, height) } // MaximizeWindow maximizes a window via IPC. func (s *Service) MaximizeWindow(name string) error { - _, _, err := s.Core().PERFORM(window.TaskMaximise{Name: name}) - return err + r := s.Core().Action("window.maximise").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskMaximise{Name: name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // MinimizeWindow minimizes a window via IPC. func (s *Service) MinimizeWindow(name string) error { - _, _, err := s.Core().PERFORM(window.TaskMinimise{Name: name}) - return err + r := s.Core().Action("window.minimise").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskMinimise{Name: name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // FocusWindow brings a window to the front via IPC. func (s *Service) FocusWindow(name string) error { - _, _, err := s.Core().PERFORM(window.TaskFocus{Name: name}) - return err + r := s.Core().Action("window.focus").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskFocus{Name: name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // CloseWindow closes a window via IPC. func (s *Service) CloseWindow(name string) error { - _, _, err := s.Core().PERFORM(window.TaskCloseWindow{Name: name}) - return err + r := s.Core().Action("window.close").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskCloseWindow{Name: name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // RestoreWindow restores a maximized/minimized window. func (s *Service) RestoreWindow(name string) error { - _, _, err := s.Core().PERFORM(window.TaskRestore{Name: name}) - return err + r := s.Core().Action("window.restore").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskRestore{Name: name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // SetWindowVisibility shows or hides a window. func (s *Service) SetWindowVisibility(name string, visible bool) error { - _, _, err := s.Core().PERFORM(window.TaskSetVisibility{Name: name, Visible: visible}) - return err + r := s.Core().Action("window.setVisibility").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetVisibility{Name: name, Visible: visible}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // SetWindowAlwaysOnTop sets whether a window stays on top. func (s *Service) SetWindowAlwaysOnTop(name string, alwaysOnTop bool) error { - _, _, err := s.Core().PERFORM(window.TaskSetAlwaysOnTop{Name: name, AlwaysOnTop: alwaysOnTop}) - return err + r := s.Core().Action("window.setAlwaysOnTop").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetAlwaysOnTop{Name: name, AlwaysOnTop: alwaysOnTop}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // SetWindowTitle changes a window's title. func (s *Service) SetWindowTitle(name string, title string) error { - _, _, err := s.Core().PERFORM(window.TaskSetTitle{Name: name, Title: title}) - return err + r := s.Core().Action("window.setTitle").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetTitle{Name: name, Title: title}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // SetWindowFullscreen sets a window to fullscreen mode. func (s *Service) SetWindowFullscreen(name string, fullscreen bool) error { - _, _, err := s.Core().PERFORM(window.TaskFullscreen{Name: name, Fullscreen: fullscreen}) - return err + r := s.Core().Action("window.fullscreen").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskFullscreen{Name: name, Fullscreen: fullscreen}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // SetWindowBackgroundColour sets the background colour of a window. func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error { - _, _, err := s.Core().PERFORM(window.TaskSetBackgroundColour{ - Name: name, Red: r, Green: g, Blue: b, Alpha: a, - }) - return err + res := s.Core().Action("window.setBackgroundColour").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetBackgroundColour{ + Name: name, Red: r, Green: g, Blue: b, Alpha: a, + }}, + )) + if !res.OK { + if e, ok := res.Value.(error); ok { + return e + } + } + return nil } // GetFocusedWindow returns the name of the currently focused window. @@ -763,21 +885,26 @@ func (s *Service) CreateWindow(options CreateWindowOptions) (*window.WindowInfo, if options.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, - }, - }) - if err != nil { - return nil, err + r := s.Core().Action("window.open").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: 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, + }, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, e + } + return nil, coreerr.E("display.CreateWindow", "window.open action failed", nil) } - info := result.(window.WindowInfo) + info, _ := r.Value.(window.WindowInfo) return &info, nil } @@ -785,39 +912,60 @@ func (s *Service) CreateWindow(options CreateWindowOptions) (*window.WindowInfo, // SaveLayout saves the current window arrangement as a named layout. func (s *Service) SaveLayout(name string) error { - _, _, err := s.Core().PERFORM(window.TaskSaveLayout{Name: name}) - return err + r := s.Core().Action("window.saveLayout").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSaveLayout{Name: name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // RestoreLayout applies a saved layout. func (s *Service) RestoreLayout(name string) error { - _, _, err := s.Core().PERFORM(window.TaskRestoreLayout{Name: name}) - return err + r := s.Core().Action("window.restoreLayout").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskRestoreLayout{Name: name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // ListLayouts returns all saved layout names with metadata. func (s *Service) ListLayouts() []window.LayoutInfo { - result, handled, _ := s.Core().QUERY(window.QueryLayoutList{}) - if !handled { + r := s.Core().QUERY(window.QueryLayoutList{}) + if !r.OK { return nil } - layouts, _ := result.([]window.LayoutInfo) + layouts, _ := r.Value.([]window.LayoutInfo) return layouts } // DeleteLayout removes a saved layout by name. func (s *Service) DeleteLayout(name string) error { - _, _, err := s.Core().PERFORM(window.TaskDeleteLayout{Name: name}) - return err + r := s.Core().Action("window.deleteLayout").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskDeleteLayout{Name: name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // GetLayout returns a specific layout by name. func (s *Service) GetLayout(name string) *window.Layout { - result, handled, _ := s.Core().QUERY(window.QueryLayoutGet{Name: name}) - if !handled { + r := s.Core().QUERY(window.QueryLayoutGet{Name: name}) + if !r.OK { return nil } - layout, _ := result.(*window.Layout) + layout, _ := r.Value.(*window.Layout) return layout } @@ -825,28 +973,54 @@ func (s *Service) GetLayout(name string) *window.Layout { // TileWindows arranges windows in a tiled layout. func (s *Service) TileWindows(mode window.TileMode, windowNames []string) error { - _, _, err := s.Core().PERFORM(window.TaskTileWindows{Mode: mode.String(), Windows: windowNames}) - return err + r := s.Core().Action("window.tileWindows").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskTileWindows{Mode: mode.String(), Windows: windowNames}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // SnapWindow snaps a window to a screen edge or corner. func (s *Service) SnapWindow(name string, position window.SnapPosition) error { - _, _, err := s.Core().PERFORM(window.TaskSnapWindow{Name: name, Position: position.String()}) - return err + r := s.Core().Action("window.snapWindow").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSnapWindow{Name: name, Position: position.String()}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // StackWindows arranges windows in a cascade pattern. func (s *Service) StackWindows(windowNames []string, offsetX, offsetY int) error { - _, _, err := s.Core().PERFORM(window.TaskStackWindows{Windows: windowNames, OffsetX: offsetX, OffsetY: offsetY}) - return err + r := s.Core().Action("window.stackWindows").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskStackWindows{Windows: windowNames, OffsetX: offsetX, OffsetY: offsetY}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // ApplyWorkflowLayout applies a predefined layout for a specific workflow. func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error { - _, _, err := s.Core().PERFORM(window.TaskApplyWorkflow{ - Workflow: workflow.String(), - }) - return err + r := s.Core().Action("window.applyWorkflow").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskApplyWorkflow{Workflow: workflow.String()}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return e + } + } + return nil } // GetEventManager returns the event manager for WebSocket event subscriptions. @@ -886,7 +1060,9 @@ func (s *Service) buildMenu() { items = items[1:] // skip AppMenu } - _, _, _ = s.Core().PERFORM(menu.TaskSetAppMenu{Items: items}) + _ = s.Core().Action("menu.setAppMenu").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: menu.TaskSetAppMenu{Items: items}}, + )) } func ptr[T any](v T) *T { return &v } @@ -894,23 +1070,25 @@ func ptr[T any](v T) *T { return &v } // --- Menu handler methods --- func (s *Service) handleNewWorkspace() { - _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ - Window: &window.Window{ - Name: "workspace-new", - Title: "New Workspace", - URL: "/workspace/new", - Width: 500, - Height: 400, - }, - }) + _ = s.Core().Action("window.open").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskOpenWindow{ + Window: &window.Window{ + Name: "workspace-new", + Title: "New Workspace", + URL: "/workspace/new", + Width: 500, + Height: 400, + }, + }}, + )) } func (s *Service) handleListWorkspaces() { - ws := s.Core().Service("workspace") - if ws == nil { + r := s.Core().Service("workspace") + if !r.OK || r.Value == nil { return } - lister, ok := ws.(interface{ ListWorkspaces() []string }) + lister, ok := r.Value.(interface{ ListWorkspaces() []string }) if !ok { return } @@ -918,64 +1096,75 @@ func (s *Service) handleListWorkspaces() { } func (s *Service) handleNewFile() { - _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ - Window: &window.Window{ - Name: "editor", - Title: "New File - Editor", - URL: "/#/developer/editor?new=true", - Width: 1200, - Height: 800, - }, - }) + _ = s.Core().Action("window.open").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskOpenWindow{ + Window: &window.Window{ + Name: "editor", + Title: "New File - Editor", + URL: "/#/developer/editor?new=true", + Width: 1200, + Height: 800, + }, + }}, + )) } func (s *Service) handleOpenFile() { - result, handled, err := s.Core().PERFORM(dialog.TaskOpenFile{ - Options: dialog.OpenFileOptions{ - Title: "Open File", - AllowMultiple: false, - }, - }) - if err != nil || !handled { + r := s.Core().Action("dialog.openFile").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskOpenFile{ + Options: dialog.OpenFileOptions{ + Title: "Open File", + AllowMultiple: false, + }, + }}, + )) + if !r.OK { return } - paths, ok := result.([]string) + paths, ok := r.Value.([]string) if !ok || len(paths) == 0 { return } - _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ - Window: &window.Window{ - Name: "editor", - Title: paths[0] + " - Editor", - URL: "/#/developer/editor?file=" + paths[0], - Width: 1200, - Height: 800, - }, - }) + _ = s.Core().Action("window.open").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskOpenWindow{ + Window: &window.Window{ + Name: "editor", + Title: paths[0] + " - Editor", + URL: "/#/developer/editor?file=" + paths[0], + Width: 1200, + Height: 800, + }, + }}, + )) } func (s *Service) handleSaveFile() { _ = s.Core().ACTION(ActionIDECommand{Command: "save"}) } func (s *Service) handleOpenEditor() { - _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ - Window: &window.Window{ - Name: "editor", - Title: "Editor", - URL: "/#/developer/editor", - Width: 1200, - Height: 800, - }, - }) + _ = s.Core().Action("window.open").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskOpenWindow{ + Window: &window.Window{ + Name: "editor", + Title: "Editor", + URL: "/#/developer/editor", + Width: 1200, + Height: 800, + }, + }}, + )) } + func (s *Service) handleOpenTerminal() { - _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ - Window: &window.Window{ - Name: "terminal", - Title: "Terminal", - URL: "/#/developer/terminal", - Width: 800, - Height: 500, - }, - }) + _ = s.Core().Action("window.open").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskOpenWindow{ + Window: &window.Window{ + Name: "terminal", + Title: "Terminal", + URL: "/#/developer/terminal", + Width: 800, + Height: 500, + }, + }}, + )) } func (s *Service) handleRun() { _ = s.Core().ACTION(ActionIDECommand{Command: "run"}) } func (s *Service) handleBuild() { _ = s.Core().ACTION(ActionIDECommand{Command: "build"}) } @@ -983,12 +1172,14 @@ func (s *Service) handleBuild() { _ = s.Core().ACTION(ActionIDECommand{Command: // --- Tray (setup delegated via IPC) --- func (s *Service) setupTray() { - _, _, _ = s.Core().PERFORM(systray.TaskSetTrayMenu{Items: []systray.TrayMenuItem{ - {Label: "Open Desktop", ActionID: "open-desktop"}, - {Label: "Close Desktop", ActionID: "close-desktop"}, - {Type: "separator"}, - {Label: "Environment Info", ActionID: "env-info"}, - {Type: "separator"}, - {Label: "Quit", ActionID: "quit"}, - }}) + _ = s.Core().Action("systray.setMenu").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: systray.TaskSetTrayMenu{Items: []systray.TrayMenuItem{ + {Label: "Open Desktop", ActionID: "open-desktop"}, + {Label: "Close Desktop", ActionID: "close-desktop"}, + {Type: "separator"}, + {Label: "Environment Info", ActionID: "env-info"}, + {Type: "separator"}, + {Label: "Quit", ActionID: "quit"}, + }}}, + )) } diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index 4b10798..fe99925 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/menu" "forge.lthn.ai/core/gui/pkg/systray" "forge.lthn.ai/core/gui/pkg/window" @@ -19,12 +19,11 @@ import ( // newTestDisplayService creates a display service registered with Core for IPC testing. func newTestDisplayService(t *testing.T) (*Service, *core.Core) { t.Helper() - c, err := core.New( + c := core.New( core.WithService(Register(nil)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "display") return svc, c } @@ -32,18 +31,23 @@ func newTestDisplayService(t *testing.T) (*Service, *core.Core) { // newTestConclave creates a full 4-service conclave for integration testing. func newTestConclave(t *testing.T) *core.Core { t.Helper() - c, err := core.New( + c := core.New( core.WithService(Register(nil)), core.WithService(window.Register(window.NewMockPlatform())), core.WithService(systray.Register(systray.NewMockPlatform())), core.WithService(menu.Register(menu.NewMockPlatform())), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) return c } +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + // --- Tests --- func TestNew_Good(t *testing.T) { @@ -64,12 +68,11 @@ func TestRegister_Good(t *testing.T) { factory := Register(nil) // nil wailsApp for testing assert.NotNil(t, factory) - c, err := core.New( + c := core.New( core.WithService(factory), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "display") assert.NotNil(t, svc) @@ -83,32 +86,29 @@ func TestConfigQuery_Good(t *testing.T) { "default_width": 1024, } - result, handled, err := c.QUERY(window.QueryConfig{}) - require.NoError(t, err) - assert.True(t, handled) - cfg := result.(map[string]any) + r := c.QUERY(window.QueryConfig{}) + require.True(t, r.OK) + cfg := r.Value.(map[string]any) assert.Equal(t, 1024, cfg["default_width"]) } func TestConfigQuery_Bad(t *testing.T) { // No display service — window config query returns handled=false - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.QUERY(window.QueryConfig{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(window.QueryConfig{}) + assert.False(t, r.OK) } func TestConfigTask_Good(t *testing.T) { _, c := newTestDisplayService(t) newCfg := map[string]any{"default_width": 800} - _, handled, err := c.PERFORM(window.TaskSaveConfig{Config: newCfg}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "display.saveWindowConfig", window.TaskSaveConfig{Config: newCfg}) + require.True(t, r.OK) // Verify config was saved - result, _, _ := c.QUERY(window.QueryConfig{}) - cfg := result.(map[string]any) + r2 := c.QUERY(window.QueryConfig{}) + cfg := r2.Value.(map[string]any) assert.Equal(t, 800, cfg["default_width"]) } @@ -118,45 +118,41 @@ func TestServiceConclave_Good(t *testing.T) { c := newTestConclave(t) // Open a window via IPC - result, handled, err := c.PERFORM(window.TaskOpenWindow{ + r := taskRun(c, "window.open", window.TaskOpenWindow{ Window: &window.Window{Name: "main"}, }) - require.NoError(t, err) - assert.True(t, handled) - info := result.(window.WindowInfo) + require.True(t, r.OK) + info := r.Value.(window.WindowInfo) assert.Equal(t, "main", info.Name) // Query window config from display - val, handled, err := c.QUERY(window.QueryConfig{}) - require.NoError(t, err) - assert.True(t, handled) - assert.NotNil(t, val) + r2 := c.QUERY(window.QueryConfig{}) + require.True(t, r2.OK) + assert.NotNil(t, r2.Value) // Set app menu via IPC - _, handled, err = c.PERFORM(menu.TaskSetAppMenu{Items: []menu.MenuItem{ + r3 := taskRun(c, "menu.setAppMenu", menu.TaskSetAppMenu{Items: []menu.MenuItem{ {Label: "File"}, }}) - require.NoError(t, err) - assert.True(t, handled) + require.True(t, r3.OK) // Query app menu via IPC - menuResult, handled, _ := c.QUERY(menu.QueryGetAppMenu{}) - assert.True(t, handled) - items := menuResult.([]menu.MenuItem) + r4 := c.QUERY(menu.QueryGetAppMenu{}) + assert.True(t, r4.OK) + items := r4.Value.([]menu.MenuItem) assert.Len(t, items, 1) } func TestServiceConclave_Bad(t *testing.T) { // Sub-service starts without display — config QUERY returns handled=false - c, err := core.New( + c := core.New( core.WithService(window.Register(window.NewMockPlatform())), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) - _, handled, _ := c.QUERY(window.QueryConfig{}) - assert.False(t, handled, "no display service means no config handler") + r := c.QUERY(window.QueryConfig{}) + assert.False(t, r.OK, "no display service means no config handler") } // --- IPC delegation tests (full conclave) --- @@ -183,8 +179,8 @@ func TestOpenWindow_Good(t *testing.T) { ) assert.NoError(t, err) - result, _, _ := c.QUERY(window.QueryWindowByName{Name: "custom-window"}) - info := result.(*window.WindowInfo) + r := c.QUERY(window.QueryWindowByName{Name: "custom-window"}) + info := r.Value.(*window.WindowInfo) assert.Equal(t, "custom-window", info.Name) }) } @@ -199,7 +195,7 @@ func TestGetWindowInfo_Good(t *testing.T) { ) // Modify position via IPC - _, _, _ = c.PERFORM(window.TaskSetPosition{Name: "test-win", X: 100, Y: 200}) + taskRun(c, "window.setPosition", window.TaskSetPosition{Name: "test-win", X: 100, Y: 200}) info, err := svc.GetWindowInfo("test-win") require.NoError(t, err) @@ -410,12 +406,11 @@ 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{ + r := taskRun(c, "window.open", window.TaskOpenWindow{ Window: &window.Window{Name: "test"}, }) - require.NoError(t, err) - assert.True(t, handled) - info := result.(window.WindowInfo) + require.True(t, r.OK) + info := r.Value.(window.WindowInfo) assert.Equal(t, "test", info.Name) } @@ -481,20 +476,19 @@ func TestHandleConfigTask_Persists_Good(t *testing.T) { s.loadConfigFrom(cfgPath) // Creates empty config (file doesn't exist yet) // Simulate a TaskSaveConfig through the handler - c, _ := core.New( - core.WithService(func(c *core.Core) (any, error) { + c := core.New( + core.WithService(func(c *core.Core) core.Result { s.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{}) - return s, nil + return core.Result{Value: s, OK: true} }), core.WithServiceLock(), ) c.ServiceStartup(context.Background(), nil) - _, handled, err := c.PERFORM(window.TaskSaveConfig{ + r := taskRun(c, "display.saveWindowConfig", window.TaskSaveConfig{ Config: map[string]any{"default_width": 1920}, }) - require.NoError(t, err) - assert.True(t, handled) + require.True(t, r.OK) // Verify file was written data, err := os.ReadFile(cfgPath) diff --git a/pkg/dock/register.go b/pkg/dock/register.go index 1d9c3cd..97539f4 100644 --- a/pkg/dock/register.go +++ b/pkg/dock/register.go @@ -1,14 +1,14 @@ package dock -import "forge.lthn.ai/core/go/pkg/core" +import core "dappco.re/go/core" // Register(p) binds the dock service to a Core instance. // core.WithService(dock.Register(wailsDock)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, - }, nil + }, OK: true} } } diff --git a/pkg/dock/service.go b/pkg/dock/service.go index 8d3c9f1..527edff 100644 --- a/pkg/dock/service.go +++ b/pkg/dock/service.go @@ -3,7 +3,7 @@ package dock import ( "context" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" ) type Options struct{} @@ -13,68 +13,70 @@ type Service struct { platform Platform } -func (s *Service) OnStartup(ctx context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) - return nil -} - -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil -} - -func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { - switch q.(type) { - case QueryVisible: - return s.platform.IsVisible(), true, nil - default: - return nil, false, nil - } -} - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskShowIcon: + s.Core().Action("dock.showIcon", func(_ context.Context, _ core.Options) core.Result { if err := s.platform.ShowIcon(); err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } _ = s.Core().ACTION(ActionVisibilityChanged{Visible: true}) - return nil, true, nil - case TaskHideIcon: + return core.Result{OK: true} + }) + s.Core().Action("dock.hideIcon", func(_ context.Context, _ core.Options) core.Result { if err := s.platform.HideIcon(); err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } _ = s.Core().ACTION(ActionVisibilityChanged{Visible: false}) - return nil, true, nil - case TaskSetBadge: - if err := s.platform.SetBadge(t.Label); err != nil { - return nil, true, err + return core.Result{OK: true} + }) + s.Core().Action("dock.setBadge", func(_ context.Context, opts core.Options) core.Result { + if err := s.platform.SetBadge(opts.String("label")); err != nil { + return core.Result{Value: err, OK: false} } - return nil, true, nil - case TaskRemoveBadge: + return core.Result{OK: true} + }) + s.Core().Action("dock.removeBadge", func(_ context.Context, _ core.Options) core.Result { if err := s.platform.RemoveBadge(); err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, nil - case TaskSetProgressBar: + return core.Result{OK: true} + }) + s.Core().Action("dock.setProgressBar", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskSetProgressBar) if err := s.platform.SetProgressBar(t.Progress); err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } _ = s.Core().ACTION(ActionProgressChanged{Progress: t.Progress}) - return nil, true, nil - case TaskBounce: + return core.Result{OK: true} + }) + s.Core().Action("dock.bounce", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskBounce) requestID, err := s.platform.Bounce(t.BounceType) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } _ = s.Core().ACTION(ActionBounceStarted{RequestID: requestID, BounceType: t.BounceType}) - return requestID, true, nil - case TaskStopBounce: + return core.Result{Value: requestID, OK: true} + }) + s.Core().Action("dock.stopBounce", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskStopBounce) if err := s.platform.StopBounce(t.RequestID); err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, nil + return core.Result{OK: true} + }) + return core.Result{OK: true} +} + +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.(type) { + case QueryVisible: + return core.Result{Value: s.platform.IsVisible(), OK: true} default: - return nil, false, nil + return core.Result{} } } diff --git a/pkg/dock/service_test.go b/pkg/dock/service_test.go index 4b35fe8..9be5077 100644 --- a/pkg/dock/service_test.go +++ b/pkg/dock/service_test.go @@ -5,7 +5,7 @@ import ( "context" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -13,21 +13,21 @@ import ( // --- Mock Platform --- type mockPlatform struct { - visible bool - badge string - hasBadge bool - progress float64 - bounceID int - bounceType BounceType - bounceCalled bool + visible bool + badge string + hasBadge bool + progress float64 + bounceID int + bounceType BounceType + bounceCalled bool stopBounceCalled bool - showErr error - hideErr error - badgeErr error - removeErr error - progressErr error - bounceErr error - stopBounceErr error + showErr error + hideErr error + badgeErr error + removeErr error + progressErr error + bounceErr error + stopBounceErr error } func (m *mockPlatform) ShowIcon() error { @@ -97,16 +97,27 @@ func (m *mockPlatform) StopBounce(requestID int) error { func newTestDockService(t *testing.T) (*Service, *core.Core, *mockPlatform) { t.Helper() mock := &mockPlatform{visible: true} - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "dock") return svc, c, mock } +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + +func setBadge(c *core.Core, label string) core.Result { + return c.Action("dock.setBadge").Run(context.Background(), core.NewOptions( + core.Option{Key: "label", Value: label}, + )) +} + // --- Tests --- func TestRegister_Good(t *testing.T) { @@ -116,18 +127,16 @@ func TestRegister_Good(t *testing.T) { func TestQueryVisible_Good(t *testing.T) { _, c, _ := newTestDockService(t) - result, handled, err := c.QUERY(QueryVisible{}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, true, result) + r := c.QUERY(QueryVisible{}) + require.True(t, r.OK) + assert.Equal(t, true, r.Value) } func TestQueryVisible_Bad(t *testing.T) { // No dock service registered — QUERY returns handled=false - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.QUERY(QueryVisible{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(QueryVisible{}) + assert.False(t, r.OK) } func TestTaskShowIcon_Good(t *testing.T) { @@ -135,16 +144,15 @@ func TestTaskShowIcon_Good(t *testing.T) { mock.visible = false // Start hidden var received *ActionVisibilityChanged - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionVisibilityChanged); ok { received = &a } - return nil + return core.Result{OK: true} }) - _, handled, err := c.PERFORM(TaskShowIcon{}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "dock.showIcon", TaskShowIcon{}) + require.True(t, r.OK) assert.True(t, mock.visible) require.NotNil(t, received) assert.True(t, received.Visible) @@ -155,16 +163,15 @@ func TestTaskHideIcon_Good(t *testing.T) { mock.visible = true // Start visible var received *ActionVisibilityChanged - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionVisibilityChanged); ok { received = &a } - return nil + return core.Result{OK: true} }) - _, handled, err := c.PERFORM(TaskHideIcon{}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "dock.hideIcon", TaskHideIcon{}) + require.True(t, r.OK) assert.False(t, mock.visible) require.NotNil(t, received) assert.False(t, received.Visible) @@ -172,18 +179,16 @@ func TestTaskHideIcon_Good(t *testing.T) { func TestTaskSetBadge_Good(t *testing.T) { _, c, mock := newTestDockService(t) - _, handled, err := c.PERFORM(TaskSetBadge{Label: "3"}) - require.NoError(t, err) - assert.True(t, handled) + r := setBadge(c, "3") + require.True(t, r.OK) assert.Equal(t, "3", mock.badge) assert.True(t, mock.hasBadge) } func TestTaskSetBadge_EmptyLabel_Good(t *testing.T) { _, c, mock := newTestDockService(t) - _, handled, err := c.PERFORM(TaskSetBadge{Label: ""}) - require.NoError(t, err) - assert.True(t, handled) + r := setBadge(c, "") + require.True(t, r.OK) assert.Equal(t, "", mock.badge) assert.True(t, mock.hasBadge) // Empty string = default system badge indicator } @@ -191,11 +196,10 @@ func TestTaskSetBadge_EmptyLabel_Good(t *testing.T) { func TestTaskRemoveBadge_Good(t *testing.T) { _, c, mock := newTestDockService(t) // Set a badge first - _, _, _ = c.PERFORM(TaskSetBadge{Label: "5"}) + _ = setBadge(c, "5") - _, handled, err := c.PERFORM(TaskRemoveBadge{}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "dock.removeBadge", TaskRemoveBadge{}) + require.True(t, r.OK) assert.Equal(t, "", mock.badge) assert.False(t, mock.hasBadge) } @@ -204,27 +208,24 @@ func TestTaskShowIcon_Bad(t *testing.T) { _, c, mock := newTestDockService(t) mock.showErr = assert.AnError - _, handled, err := c.PERFORM(TaskShowIcon{}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "dock.showIcon", TaskShowIcon{}) + assert.False(t, r.OK) } func TestTaskHideIcon_Bad(t *testing.T) { _, c, mock := newTestDockService(t) mock.hideErr = assert.AnError - _, handled, err := c.PERFORM(TaskHideIcon{}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "dock.hideIcon", TaskHideIcon{}) + assert.False(t, r.OK) } func TestTaskSetBadge_Bad(t *testing.T) { _, c, mock := newTestDockService(t) mock.badgeErr = assert.AnError - _, handled, err := c.PERFORM(TaskSetBadge{Label: "3"}) - assert.True(t, handled) - assert.Error(t, err) + r := setBadge(c, "3") + assert.False(t, r.OK) } // --- TaskSetProgressBar --- @@ -233,16 +234,15 @@ func TestTaskSetProgressBar_Good(t *testing.T) { _, c, mock := newTestDockService(t) var received *ActionProgressChanged - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionProgressChanged); ok { received = &a } - return nil + return core.Result{OK: true} }) - _, handled, err := c.PERFORM(TaskSetProgressBar{Progress: 0.5}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "dock.setProgressBar", TaskSetProgressBar{Progress: 0.5}) + require.True(t, r.OK) assert.Equal(t, 0.5, mock.progress) require.NotNil(t, received) assert.Equal(t, 0.5, received.Progress) @@ -251,9 +251,8 @@ func TestTaskSetProgressBar_Good(t *testing.T) { func TestTaskSetProgressBar_Hide_Good(t *testing.T) { // Progress -1.0 hides the indicator _, c, mock := newTestDockService(t) - _, handled, err := c.PERFORM(TaskSetProgressBar{Progress: -1.0}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "dock.setProgressBar", TaskSetProgressBar{Progress: -1.0}) + require.True(t, r.OK) assert.Equal(t, -1.0, mock.progress) } @@ -261,17 +260,15 @@ func TestTaskSetProgressBar_Bad(t *testing.T) { _, c, mock := newTestDockService(t) mock.progressErr = assert.AnError - _, handled, err := c.PERFORM(TaskSetProgressBar{Progress: 0.5}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "dock.setProgressBar", TaskSetProgressBar{Progress: 0.5}) + assert.False(t, r.OK) } func TestTaskSetProgressBar_Ugly(t *testing.T) { - // No dock service — PERFORM returns handled=false - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.PERFORM(TaskSetProgressBar{Progress: 0.5}) - assert.False(t, handled) + // No dock service — action is not registered + c := core.New(core.WithServiceLock()) + r := c.Action("dock.setProgressBar").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } // --- TaskBounce --- @@ -280,19 +277,18 @@ func TestTaskBounce_Good(t *testing.T) { _, c, mock := newTestDockService(t) var received *ActionBounceStarted - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionBounceStarted); ok { received = &a } - return nil + return core.Result{OK: true} }) - result, handled, err := c.PERFORM(TaskBounce{BounceType: BounceInformational}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "dock.bounce", TaskBounce{BounceType: BounceInformational}) + require.True(t, r.OK) assert.True(t, mock.bounceCalled) assert.Equal(t, BounceInformational, mock.bounceType) - requestID, ok := result.(int) + requestID, ok := r.Value.(int) require.True(t, ok) assert.Equal(t, 1, requestID) require.NotNil(t, received) @@ -301,11 +297,10 @@ func TestTaskBounce_Good(t *testing.T) { func TestTaskBounce_Critical_Good(t *testing.T) { _, c, mock := newTestDockService(t) - result, handled, err := c.PERFORM(TaskBounce{BounceType: BounceCritical}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "dock.bounce", TaskBounce{BounceType: BounceCritical}) + require.True(t, r.OK) assert.Equal(t, BounceCritical, mock.bounceType) - requestID, ok := result.(int) + requestID, ok := r.Value.(int) require.True(t, ok) assert.Equal(t, 1, requestID) } @@ -314,17 +309,15 @@ func TestTaskBounce_Bad(t *testing.T) { _, c, mock := newTestDockService(t) mock.bounceErr = assert.AnError - _, handled, err := c.PERFORM(TaskBounce{BounceType: BounceInformational}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "dock.bounce", TaskBounce{BounceType: BounceInformational}) + assert.False(t, r.OK) } func TestTaskBounce_Ugly(t *testing.T) { - // No dock service — PERFORM returns handled=false - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.PERFORM(TaskBounce{BounceType: BounceInformational}) - assert.False(t, handled) + // No dock service — action is not registered + c := core.New(core.WithServiceLock()) + r := c.Action("dock.bounce").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } // --- TaskStopBounce --- @@ -333,13 +326,12 @@ func TestTaskStopBounce_Good(t *testing.T) { _, c, mock := newTestDockService(t) // Start a bounce to get a requestID - result, _, err := c.PERFORM(TaskBounce{BounceType: BounceInformational}) - require.NoError(t, err) - requestID := result.(int) + r := taskRun(c, "dock.bounce", TaskBounce{BounceType: BounceInformational}) + require.True(t, r.OK) + requestID := r.Value.(int) - _, handled, err := c.PERFORM(TaskStopBounce{RequestID: requestID}) - require.NoError(t, err) - assert.True(t, handled) + r2 := taskRun(c, "dock.stopBounce", TaskStopBounce{RequestID: requestID}) + require.True(t, r2.OK) assert.True(t, mock.stopBounceCalled) } @@ -347,40 +339,35 @@ func TestTaskStopBounce_Bad(t *testing.T) { _, c, mock := newTestDockService(t) mock.stopBounceErr = assert.AnError - _, handled, err := c.PERFORM(TaskStopBounce{RequestID: 1}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "dock.stopBounce", TaskStopBounce{RequestID: 1}) + assert.False(t, r.OK) } func TestTaskStopBounce_Ugly(t *testing.T) { - // No dock service — PERFORM returns handled=false - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.PERFORM(TaskStopBounce{RequestID: 99}) - assert.False(t, handled) + // No dock service — action is not registered + c := core.New(core.WithServiceLock()) + r := c.Action("dock.stopBounce").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestTaskRemoveBadge_Bad(t *testing.T) { _, c, mock := newTestDockService(t) mock.removeErr = assert.AnError - _, handled, err := c.PERFORM(TaskRemoveBadge{}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "dock.removeBadge", TaskRemoveBadge{}) + assert.False(t, r.OK) } func TestQueryVisible_Ugly(t *testing.T) { // Dock icon initially hidden mock := &mockPlatform{visible: false} - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) - result, handled, err := c.QUERY(QueryVisible{}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, false, result) + r := c.QUERY(QueryVisible{}) + require.True(t, r.OK) + assert.Equal(t, false, r.Value) } diff --git a/pkg/environment/service.go b/pkg/environment/service.go index 6cc4192..a305c2d 100644 --- a/pkg/environment/service.go +++ b/pkg/environment/service.go @@ -4,7 +4,7 @@ package environment import ( "context" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" ) type Options struct{} @@ -17,38 +17,44 @@ type Service struct { // Register(p) binds the environment service to a Core instance. // core.WithService(environment.Register(wailsEnvironment)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, - }, nil + }, OK: true} } } -func (s *Service) OnStartup(ctx context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) + s.Core().Action("environment.openFileManager", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskOpenFileManager) + if err := s.platform.OpenFileManager(t.Path, t.Select); err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{OK: true} + }) // Register theme change callback — broadcasts ActionThemeChanged via IPC s.cancelTheme = s.platform.OnThemeChange(func(isDark bool) { _ = s.Core().ACTION(ActionThemeChanged{IsDark: isDark}) }) - return nil + return core.Result{OK: true} } -func (s *Service) OnShutdown(ctx context.Context) error { +func (s *Service) OnShutdown(_ context.Context) core.Result { if s.cancelTheme != nil { s.cancelTheme() } - return nil + return core.Result{OK: true} } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } -func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q.(type) { case QueryTheme: isDark := s.platform.IsDarkMode() @@ -56,23 +62,14 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { if isDark { theme = "dark" } - return ThemeInfo{IsDark: isDark, Theme: theme}, true, nil + return core.Result{Value: ThemeInfo{IsDark: isDark, Theme: theme}, OK: true} case QueryInfo: - return s.platform.Info(), true, nil + return core.Result{Value: s.platform.Info(), OK: true} case QueryAccentColour: - return s.platform.AccentColour(), true, nil + return core.Result{Value: s.platform.AccentColour(), OK: true} case QueryFocusFollowsMouse: - return s.platform.HasFocusFollowsMouse(), true, nil + return core.Result{Value: s.platform.HasFocusFollowsMouse(), OK: true} default: - return nil, false, nil - } -} - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskOpenFileManager: - return nil, true, s.platform.OpenFileManager(t.Path, t.Select) - default: - return nil, false, nil + return core.Result{} } } diff --git a/pkg/environment/service_test.go b/pkg/environment/service_test.go index 45fcc15..2b1d650 100644 --- a/pkg/environment/service_test.go +++ b/pkg/environment/service_test.go @@ -6,25 +6,25 @@ import ( "sync" "testing" - coreerr "forge.lthn.ai/core/go-log" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type mockPlatform struct { - isDark bool - info EnvironmentInfo - accentColour string - openFMErr error - focusFollowsMouse bool - themeHandler func(isDark bool) - mu sync.Mutex + isDark bool + info EnvironmentInfo + accentColour string + openFMErr error + focusFollowsMouse bool + themeHandler func(isDark bool) + mu sync.Mutex } -func (m *mockPlatform) IsDarkMode() bool { return m.isDark } -func (m *mockPlatform) Info() EnvironmentInfo { return m.info } -func (m *mockPlatform) AccentColour() string { return m.accentColour } +func (m *mockPlatform) IsDarkMode() bool { return m.isDark } +func (m *mockPlatform) Info() EnvironmentInfo { return m.info } +func (m *mockPlatform) AccentColour() string { return m.accentColour } func (m *mockPlatform) HasFocusFollowsMouse() bool { return m.focusFollowsMouse } func (m *mockPlatform) OpenFileManager(path string, selectFile bool) error { return m.openFMErr @@ -60,12 +60,11 @@ func newTestService(t *testing.T) (*mockPlatform, *core.Core) { Platform: PlatformInfo{Name: "macOS", Version: "14.0"}, }, } - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) return mock, c } @@ -77,37 +76,35 @@ func TestRegister_Good(t *testing.T) { func TestQueryTheme_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryTheme{}) - require.NoError(t, err) - assert.True(t, handled) - theme := result.(ThemeInfo) + r := c.QUERY(QueryTheme{}) + require.True(t, r.OK) + theme := r.Value.(ThemeInfo) assert.True(t, theme.IsDark) assert.Equal(t, "dark", theme.Theme) } func TestQueryInfo_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryInfo{}) - require.NoError(t, err) - assert.True(t, handled) - info := result.(EnvironmentInfo) + r := c.QUERY(QueryInfo{}) + require.True(t, r.OK) + info := r.Value.(EnvironmentInfo) assert.Equal(t, "darwin", info.OS) assert.Equal(t, "arm64", info.Arch) } func TestQueryAccentColour_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryAccentColour{}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "rgb(0,122,255)", result) + r := c.QUERY(QueryAccentColour{}) + require.True(t, r.OK) + assert.Equal(t, "rgb(0,122,255)", r.Value) } func TestTaskOpenFileManager_Good(t *testing.T) { _, c := newTestService(t) - _, handled, err := c.PERFORM(TaskOpenFileManager{Path: "/tmp", Select: true}) - require.NoError(t, err) - assert.True(t, handled) + r := c.Action("environment.openFileManager").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: TaskOpenFileManager{Path: "/tmp", Select: true}}, + )) + require.True(t, r.OK) } func TestThemeChange_ActionBroadcast_Good(t *testing.T) { @@ -116,13 +113,13 @@ func TestThemeChange_ActionBroadcast_Good(t *testing.T) { // Register a listener that captures the action var received *ActionThemeChanged var mu sync.Mutex - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionThemeChanged); ok { mu.Lock() received = &a mu.Unlock() } - return nil + return core.Result{OK: true} }) // Simulate theme change @@ -144,21 +141,19 @@ func TestQueryAccentColour_Bad_Empty(t *testing.T) { accentColour: "", info: EnvironmentInfo{OS: "linux", Arch: "amd64"}, } - c, err := core.New(core.WithService(Register(mock)), core.WithServiceLock()) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(t.Context(), nil)) + c := core.New(core.WithService(Register(mock)), core.WithServiceLock()) + require.True(t, c.ServiceStartup(t.Context(), nil).OK) - result, handled, err := c.QUERY(QueryAccentColour{}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "", result) + r := c.QUERY(QueryAccentColour{}) + require.True(t, r.OK) + assert.Equal(t, "", r.Value) } func TestQueryAccentColour_Ugly_NoService(t *testing.T) { // No environment service — query is unhandled - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.QUERY(QueryAccentColour{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(QueryAccentColour{}) + assert.False(t, r.OK) } // --- OpenFileManager --- @@ -167,20 +162,22 @@ func TestTaskOpenFileManager_Bad_Error(t *testing.T) { // platform returns an error on open openErr := coreerr.E("test", "file manager unavailable", nil) mock := &mockPlatform{openFMErr: openErr} - c, err := core.New(core.WithService(Register(mock)), core.WithServiceLock()) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(t.Context(), nil)) + c := core.New(core.WithService(Register(mock)), core.WithServiceLock()) + require.True(t, c.ServiceStartup(t.Context(), nil).OK) - _, handled, err := c.PERFORM(TaskOpenFileManager{Path: "/missing", Select: false}) - assert.True(t, handled) + r := c.Action("environment.openFileManager").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: TaskOpenFileManager{Path: "/missing", Select: false}}, + )) + assert.False(t, r.OK) + err, _ := r.Value.(error) assert.ErrorIs(t, err, openErr) } func TestTaskOpenFileManager_Ugly_NoService(t *testing.T) { - // No environment service — task is unhandled - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskOpenFileManager{Path: "/tmp", Select: false}) - assert.False(t, handled) + // No environment service — action is not registered + c := core.New(core.WithServiceLock()) + r := c.Action("environment.openFileManager").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } // --- HasFocusFollowsMouse --- @@ -188,32 +185,28 @@ func TestTaskOpenFileManager_Ugly_NoService(t *testing.T) { func TestQueryFocusFollowsMouse_Good_True(t *testing.T) { // platform reports focus-follows-mouse enabled (Linux/X11 sloppy focus) mock := &mockPlatform{focusFollowsMouse: true} - c, err := core.New(core.WithService(Register(mock)), core.WithServiceLock()) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(t.Context(), nil)) + c := core.New(core.WithService(Register(mock)), core.WithServiceLock()) + require.True(t, c.ServiceStartup(t.Context(), nil).OK) - result, handled, err := c.QUERY(QueryFocusFollowsMouse{}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, true, result) + r := c.QUERY(QueryFocusFollowsMouse{}) + require.True(t, r.OK) + assert.Equal(t, true, r.Value) } func TestQueryFocusFollowsMouse_Bad_False(t *testing.T) { // platform reports focus-follows-mouse disabled (Windows/macOS default) mock := &mockPlatform{focusFollowsMouse: false} - c, err := core.New(core.WithService(Register(mock)), core.WithServiceLock()) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(t.Context(), nil)) + c := core.New(core.WithService(Register(mock)), core.WithServiceLock()) + require.True(t, c.ServiceStartup(t.Context(), nil).OK) - result, handled, err := c.QUERY(QueryFocusFollowsMouse{}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, false, result) + r := c.QUERY(QueryFocusFollowsMouse{}) + require.True(t, r.OK) + assert.Equal(t, false, r.Value) } func TestQueryFocusFollowsMouse_Ugly_NoService(t *testing.T) { // No environment service — query is unhandled - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.QUERY(QueryFocusFollowsMouse{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(QueryFocusFollowsMouse{}) + assert.False(t, r.OK) } diff --git a/pkg/events/register.go b/pkg/events/register.go index afa2455..4bd4c70 100644 --- a/pkg/events/register.go +++ b/pkg/events/register.go @@ -1,18 +1,18 @@ // pkg/events/register.go package events -import "forge.lthn.ai/core/go/pkg/core" +import core "dappco.re/go/core" // Register binds the events service to a Core instance. // // core.WithService(events.Register(wailsEventPlatform)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, listeners: make(map[string][]func()), counts: make(map[string]int), - }, nil + }, OK: true} } } diff --git a/pkg/events/service.go b/pkg/events/service.go index 72cba38..133c552 100644 --- a/pkg/events/service.go +++ b/pkg/events/service.go @@ -5,8 +5,8 @@ import ( "context" "sync" - coreerr "forge.lthn.ai/core/go-log" - "forge.lthn.ai/core/go/pkg/core" + coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) // Options holds configuration for the events service (currently empty). @@ -23,15 +23,45 @@ type Service struct { counts map[string]int // listener counts per event name } -// OnStartup registers query and task handlers. -func (s *Service) OnStartup(ctx context.Context) error { +// OnStartup registers query and action handlers. +func (s *Service) OnStartup(_ context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) - return nil + s.Core().Action("events.emit", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskEmit) + cancelled := s.platform.Emit(t.Name, t.Data) + return core.Result{Value: cancelled, OK: true} + }) + s.Core().Action("events.on", func(ctx context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskOn) + if t.Name == "" { + return core.Result{Value: coreerr.E("events.on", "event name must not be empty", nil), OK: false} + } + cancel := s.platform.On(t.Name, func(event *CustomEvent) { + _ = s.Core().ACTION(ActionEventFired{Event: *event}) + }) + s.mu.Lock() + s.listeners[t.Name] = append(s.listeners[t.Name], cancel) + s.counts[t.Name]++ + s.mu.Unlock() + return core.Result{OK: true} + }) + s.Core().Action("events.off", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskOff) + s.platform.Off(t.Name) + s.mu.Lock() + for _, cancel := range s.listeners[t.Name] { + cancel() + } + delete(s.listeners, t.Name) + delete(s.counts, t.Name) + s.mu.Unlock() + return core.Result{OK: true} + }) + return core.Result{OK: true} } // OnShutdown cancels all IPC-registered platform listeners. -func (s *Service) OnShutdown(ctx context.Context) error { +func (s *Service) OnShutdown(_ context.Context) core.Result { s.mu.Lock() defer s.mu.Unlock() for _, cancels := range s.listeners { @@ -41,55 +71,20 @@ func (s *Service) OnShutdown(ctx context.Context) error { } s.listeners = make(map[string][]func()) s.counts = make(map[string]int) - return nil + return core.Result{OK: true} } // HandleIPCEvents satisfies the core.Service interface (no-op for now). -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } -func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q.(type) { case QueryListeners: - return s.listenerSnapshot(), true, nil + return core.Result{Value: s.listenerSnapshot(), OK: true} default: - return nil, false, nil - } -} - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskEmit: - cancelled := s.platform.Emit(t.Name, t.Data) - return cancelled, true, nil - - case TaskOn: - if t.Name == "" { - return nil, true, coreerr.E("events.taskOn", "event name must not be empty", nil) - } - cancel := s.platform.On(t.Name, func(event *CustomEvent) { - _ = c.ACTION(ActionEventFired{Event: *event}) - }) - s.mu.Lock() - s.listeners[t.Name] = append(s.listeners[t.Name], cancel) - s.counts[t.Name]++ - s.mu.Unlock() - return nil, true, nil - - case TaskOff: - s.platform.Off(t.Name) - s.mu.Lock() - for _, cancel := range s.listeners[t.Name] { - cancel() - } - delete(s.listeners, t.Name) - delete(s.counts, t.Name) - s.mu.Unlock() - return nil, true, nil - - default: - return nil, false, nil + return core.Result{} } } diff --git a/pkg/events/service_test.go b/pkg/events/service_test.go index 58a4c48..0d20cac 100644 --- a/pkg/events/service_test.go +++ b/pkg/events/service_test.go @@ -6,7 +6,7 @@ import ( "sync" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -14,9 +14,9 @@ import ( // --- Mock Platform --- type mockPlatform struct { - mu sync.Mutex - listeners map[string][]*mockListener - emitted []CustomEvent + mu sync.Mutex + listeners map[string][]*mockListener + emitted []CustomEvent resetCalled bool } @@ -116,16 +116,21 @@ func (m *mockPlatform) listenerCount() int { func newTestService(t *testing.T) (*Service, *core.Core, *mockPlatform) { t.Helper() mock := newMockPlatform() - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "events") return svc, c, mock } +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + // --- Good path tests --- func TestRegister_Good(t *testing.T) { @@ -136,10 +141,9 @@ func TestRegister_Good(t *testing.T) { func TestTaskEmit_Good(t *testing.T) { _, c, mock := newTestService(t) - result, handled, err := c.PERFORM(TaskEmit{Name: "user:login", Data: "alice"}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, false, result) // not cancelled + r := taskRun(c, "events.emit", TaskEmit{Name: "user:login", Data: "alice"}) + require.True(t, r.OK) + assert.Equal(t, false, r.Value) // not cancelled assert.Len(t, mock.emitted, 1) assert.Equal(t, "user:login", mock.emitted[0].Name) @@ -149,9 +153,8 @@ func TestTaskEmit_Good(t *testing.T) { func TestTaskEmit_NoData_Good(t *testing.T) { _, c, mock := newTestService(t) - _, handled, err := c.PERFORM(TaskEmit{Name: "ping"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "events.emit", TaskEmit{Name: "ping"}) + require.True(t, r.OK) assert.Len(t, mock.emitted, 1) assert.Nil(t, mock.emitted[0].Data) } @@ -160,16 +163,15 @@ func TestTaskOn_Good(t *testing.T) { _, c, mock := newTestService(t) var received []ActionEventFired - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if fired, ok := msg.(ActionEventFired); ok { received = append(received, fired) } - return nil + return core.Result{OK: true} }) - _, handled, err := c.PERFORM(TaskOn{Name: "theme:changed"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "events.on", TaskOn{Name: "theme:changed"}) + require.True(t, r.OK) mock.simulateEvent("theme:changed", "dark") @@ -182,31 +184,26 @@ func TestTaskOff_Good(t *testing.T) { _, c, mock := newTestService(t) // Register via IPC then remove - _, _, err := c.PERFORM(TaskOn{Name: "file:saved"}) - require.NoError(t, err) + r := taskRun(c, "events.on", TaskOn{Name: "file:saved"}) + require.True(t, r.OK) assert.Equal(t, 1, mock.listenerCount()) - _, handled, err := c.PERFORM(TaskOff{Name: "file:saved"}) - require.NoError(t, err) - assert.True(t, handled) + r2 := taskRun(c, "events.off", TaskOff{Name: "file:saved"}) + require.True(t, r2.OK) assert.Equal(t, 0, mock.listenerCount()) } func TestQueryListeners_Good(t *testing.T) { _, c, _ := newTestService(t) - _, _, err := c.PERFORM(TaskOn{Name: "user:login"}) - require.NoError(t, err) - _, _, err = c.PERFORM(TaskOn{Name: "user:login"}) - require.NoError(t, err) - _, _, err = c.PERFORM(TaskOn{Name: "theme:changed"}) - require.NoError(t, err) + require.True(t, taskRun(c, "events.on", TaskOn{Name: "user:login"}).OK) + require.True(t, taskRun(c, "events.on", TaskOn{Name: "user:login"}).OK) + require.True(t, taskRun(c, "events.on", TaskOn{Name: "theme:changed"}).OK) - result, handled, err := c.QUERY(QueryListeners{}) - require.NoError(t, err) - assert.True(t, handled) + r := c.QUERY(QueryListeners{}) + require.True(t, r.OK) - infos := result.([]ListenerInfo) + infos := r.Value.([]ListenerInfo) counts := make(map[string]int) for _, info := range infos { counts[info.EventName] = info.Count @@ -218,25 +215,21 @@ func TestQueryListeners_Good(t *testing.T) { func TestQueryListeners_Empty_Good(t *testing.T) { _, c, _ := newTestService(t) - result, handled, err := c.QUERY(QueryListeners{}) - require.NoError(t, err) - assert.True(t, handled) + r := c.QUERY(QueryListeners{}) + require.True(t, r.OK) - infos := result.([]ListenerInfo) + infos := r.Value.([]ListenerInfo) assert.Empty(t, infos) } func TestOnShutdown_CancelsAll_Good(t *testing.T) { svc, _, mock := newTestService(t) - _, _, err := svc.Core().PERFORM(TaskOn{Name: "a:b"}) - require.NoError(t, err) - _, _, err = svc.Core().PERFORM(TaskOn{Name: "c:d"}) - require.NoError(t, err) + require.True(t, taskRun(svc.Core(), "events.on", TaskOn{Name: "a:b"}).OK) + require.True(t, taskRun(svc.Core(), "events.on", TaskOn{Name: "c:d"}).OK) assert.Equal(t, 2, mock.listenerCount()) - err = svc.OnShutdown(context.Background()) - require.NoError(t, err) + require.True(t, svc.OnShutdown(context.Background()).OK) assert.Equal(t, 0, mock.listenerCount()) } @@ -244,15 +237,14 @@ func TestActionEventFired_BroadcastOnSimulate_Good(t *testing.T) { _, c, mock := newTestService(t) var receivedEvents []CustomEvent - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if fired, ok := msg.(ActionEventFired); ok { receivedEvents = append(receivedEvents, fired.Event) } - return nil + return core.Result{OK: true} }) - _, _, err := c.PERFORM(TaskOn{Name: "data:ready"}) - require.NoError(t, err) + require.True(t, taskRun(c, "events.on", TaskOn{Name: "data:ready"}).OK) mock.simulateEvent("data:ready", map[string]any{"rows": 42}) @@ -265,37 +257,33 @@ func TestActionEventFired_BroadcastOnSimulate_Good(t *testing.T) { func TestTaskOn_EmptyName_Bad(t *testing.T) { _, c, _ := newTestService(t) - _, handled, err := c.PERFORM(TaskOn{Name: ""}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "events.on", TaskOn{Name: ""}) + assert.False(t, r.OK) } func TestTaskEmit_UnknownEvent_Bad(t *testing.T) { // Emitting an event with no listeners is valid — returns not-cancelled. _, c, mock := newTestService(t) - result, handled, err := c.PERFORM(TaskEmit{Name: "no:listeners"}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, false, result) + r := taskRun(c, "events.emit", TaskEmit{Name: "no:listeners"}) + require.True(t, r.OK) + assert.Equal(t, false, r.Value) assert.Len(t, mock.emitted, 1) // still recorded as emitted } func TestQueryListeners_NoService_Bad(t *testing.T) { // No events service registered — query is not handled. - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) + c := core.New(core.WithServiceLock()) - _, handled, _ := c.QUERY(QueryListeners{}) - assert.False(t, handled) + r := c.QUERY(QueryListeners{}) + assert.False(t, r.OK) } func TestTaskEmit_NoService_Bad(t *testing.T) { - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) + c := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskEmit{Name: "x"}) - assert.False(t, handled) + r := c.Action("events.emit").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } // --- Ugly path tests --- @@ -304,9 +292,8 @@ func TestTaskOff_NeverRegistered_Ugly(t *testing.T) { // Off on a name that was never registered is a no-op — must not panic. _, c, _ := newTestService(t) - _, handled, err := c.PERFORM(TaskOff{Name: "nonexistent:event"}) - assert.True(t, handled) - assert.NoError(t, err) + r := taskRun(c, "events.off", TaskOff{Name: "nonexistent:event"}) + assert.True(t, r.OK) } func TestTaskOn_MultipleListeners_Ugly(t *testing.T) { @@ -315,18 +302,18 @@ func TestTaskOn_MultipleListeners_Ugly(t *testing.T) { var mu sync.Mutex var fireCount int - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if _, ok := msg.(ActionEventFired); ok { mu.Lock() fireCount++ mu.Unlock() } - return nil + return core.Result{OK: true} }) - _, _, _ = c.PERFORM(TaskOn{Name: "flood"}) - _, _, _ = c.PERFORM(TaskOn{Name: "flood"}) - _, _, _ = c.PERFORM(TaskOn{Name: "flood"}) + taskRun(c, "events.on", TaskOn{Name: "flood"}) + taskRun(c, "events.on", TaskOn{Name: "flood"}) + taskRun(c, "events.on", TaskOn{Name: "flood"}) mock.simulateEvent("flood", nil) @@ -341,15 +328,15 @@ func TestTaskOff_ThenEmit_Ugly(t *testing.T) { _, c, mock := newTestService(t) var received bool - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if _, ok := msg.(ActionEventFired); ok { received = true } - return nil + return core.Result{OK: true} }) - _, _, _ = c.PERFORM(TaskOn{Name: "transient"}) - _, _, _ = c.PERFORM(TaskOff{Name: "transient"}) + taskRun(c, "events.on", TaskOn{Name: "transient"}) + taskRun(c, "events.off", TaskOff{Name: "transient"}) mock.simulateEvent("transient", "late-data") assert.False(t, received) @@ -359,11 +346,11 @@ func TestQueryListeners_AfterOff_Ugly(t *testing.T) { // After Off, the event name must not appear in QueryListeners results. _, c, _ := newTestService(t) - _, _, _ = c.PERFORM(TaskOn{Name: "ephemeral"}) - _, _, _ = c.PERFORM(TaskOff{Name: "ephemeral"}) + taskRun(c, "events.on", TaskOn{Name: "ephemeral"}) + taskRun(c, "events.off", TaskOff{Name: "ephemeral"}) - result, _, _ := c.QUERY(QueryListeners{}) - infos := result.([]ListenerInfo) + r := c.QUERY(QueryListeners{}) + infos := r.Value.([]ListenerInfo) for _, info := range infos { assert.NotEqual(t, "ephemeral", info.EventName) diff --git a/pkg/keybinding/register.go b/pkg/keybinding/register.go index 8e58484..73f07a3 100644 --- a/pkg/keybinding/register.go +++ b/pkg/keybinding/register.go @@ -1,15 +1,15 @@ package keybinding -import "forge.lthn.ai/core/go/pkg/core" +import core "dappco.re/go/core" // Register(p) binds the keybinding service to a Core instance. // core.WithService(keybinding.Register(wailsKeybinding)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, registeredBindings: make(map[string]BindingInfo), - }, nil + }, OK: true} } } diff --git a/pkg/keybinding/service.go b/pkg/keybinding/service.go index c88fb6b..2a113b7 100644 --- a/pkg/keybinding/service.go +++ b/pkg/keybinding/service.go @@ -4,8 +4,8 @@ package keybinding import ( "context" - coreerr "forge.lthn.ai/core/go-log" - "forge.lthn.ai/core/go/pkg/core" + coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" ) type Options struct{} @@ -16,24 +16,35 @@ type Service struct { registeredBindings map[string]BindingInfo } -func (s *Service) OnStartup(ctx context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) - return nil + s.Core().Action("keybinding.add", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskAdd) + return core.Result{Value: nil, OK: true}.New(s.taskAdd(t)) + }) + s.Core().Action("keybinding.remove", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskRemove) + return core.Result{Value: nil, OK: true}.New(s.taskRemove(t)) + }) + s.Core().Action("keybinding.process", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskProcess) + return core.Result{Value: nil, OK: true}.New(s.taskProcess(t)) + }) + return core.Result{OK: true} } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } // --- Query Handlers --- -func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q.(type) { case QueryList: - return s.queryList(), true, nil + return core.Result{Value: s.queryList(), OK: true} default: - return nil, false, nil + return core.Result{} } } @@ -45,21 +56,6 @@ func (s *Service) queryList() []BindingInfo { return result } -// --- Task Handlers --- - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskAdd: - return nil, true, s.taskAdd(t) - case TaskRemove: - return nil, true, s.taskRemove(t) - case TaskProcess: - return nil, true, s.taskProcess(t) - default: - return nil, false, nil - } -} - func (s *Service) taskAdd(t TaskAdd) error { if _, exists := s.registeredBindings[t.Accelerator]; exists { return ErrorAlreadyRegistered @@ -97,7 +93,7 @@ func (s *Service) taskRemove(t TaskRemove) error { // taskProcess triggers the registered handler for the given accelerator programmatically. // Broadcasts ActionTriggered if handled; returns ErrorNotRegistered if the accelerator is unknown. // -// c.PERFORM(keybinding.TaskProcess{Accelerator: "Ctrl+S"}) +// c.Action("keybinding.process").Run(ctx, core.NewOptions(core.Option{Key:"task", Value:keybinding.TaskProcess{Accelerator:"Ctrl+S"}})) func (s *Service) taskProcess(t TaskProcess) error { if _, exists := s.registeredBindings[t.Accelerator]; !exists { return coreerr.E("keybinding.taskProcess", "not registered: "+t.Accelerator, ErrorNotRegistered) diff --git a/pkg/keybinding/service_test.go b/pkg/keybinding/service_test.go index bf8766e..1eeb6cc 100644 --- a/pkg/keybinding/service_test.go +++ b/pkg/keybinding/service_test.go @@ -6,7 +6,7 @@ import ( "sync" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -70,16 +70,21 @@ func (m *mockPlatform) trigger(accelerator string) { func newTestKeybindingService(t *testing.T, mp *mockPlatform) (*Service, *core.Core) { t.Helper() - c, err := core.New( + c := core.New( core.WithService(Register(mp)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "keybinding") return svc, c } +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + func TestRegister_Good(t *testing.T) { mp := newMockPlatform() svc, _ := newTestKeybindingService(t, mp) @@ -91,11 +96,10 @@ func TestTaskAdd_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - _, handled, err := c.PERFORM(TaskAdd{ + r := taskRun(c, "keybinding.add", TaskAdd{ Accelerator: "Ctrl+S", Description: "Save", }) - require.NoError(t, err) - assert.True(t, handled) + require.True(t, r.OK) // Verify binding registered on platform assert.Contains(t, mp.GetAll(), "Ctrl+S") @@ -105,11 +109,12 @@ func TestTaskAdd_Bad_Duplicate(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Accelerator: "Ctrl+S", Description: "Save"}) + taskRun(c, "keybinding.add", TaskAdd{Accelerator: "Ctrl+S", Description: "Save"}) // Second add with same accelerator should fail - _, handled, err := c.PERFORM(TaskAdd{Accelerator: "Ctrl+S", Description: "Save Again"}) - assert.True(t, handled) + r := taskRun(c, "keybinding.add", TaskAdd{Accelerator: "Ctrl+S", Description: "Save Again"}) + assert.False(t, r.OK) + err, _ := r.Value.(error) assert.ErrorIs(t, err, ErrorAlreadyRegistered) } @@ -117,10 +122,9 @@ func TestTaskRemove_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Accelerator: "Ctrl+S", Description: "Save"}) - _, handled, err := c.PERFORM(TaskRemove{Accelerator: "Ctrl+S"}) - require.NoError(t, err) - assert.True(t, handled) + taskRun(c, "keybinding.add", TaskAdd{Accelerator: "Ctrl+S", Description: "Save"}) + r := taskRun(c, "keybinding.remove", TaskRemove{Accelerator: "Ctrl+S"}) + require.True(t, r.OK) // Verify removed from platform assert.NotContains(t, mp.GetAll(), "Ctrl+S") @@ -130,22 +134,20 @@ func TestTaskRemove_Bad_NotFound(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - _, handled, err := c.PERFORM(TaskRemove{Accelerator: "Ctrl+X"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "keybinding.remove", TaskRemove{Accelerator: "Ctrl+X"}) + assert.False(t, r.OK) } func TestQueryList_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Accelerator: "Ctrl+S", Description: "Save"}) - _, _, _ = c.PERFORM(TaskAdd{Accelerator: "Ctrl+Z", Description: "Undo"}) + taskRun(c, "keybinding.add", TaskAdd{Accelerator: "Ctrl+S", Description: "Save"}) + taskRun(c, "keybinding.add", TaskAdd{Accelerator: "Ctrl+Z", Description: "Undo"}) - result, handled, err := c.QUERY(QueryList{}) - require.NoError(t, err) - assert.True(t, handled) - list := result.([]BindingInfo) + r := c.QUERY(QueryList{}) + require.True(t, r.OK) + list := r.Value.([]BindingInfo) assert.Len(t, list, 2) } @@ -153,10 +155,9 @@ func TestQueryList_Good_Empty(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - result, handled, err := c.QUERY(QueryList{}) - require.NoError(t, err) - assert.True(t, handled) - list := result.([]BindingInfo) + r := c.QUERY(QueryList{}) + require.True(t, r.OK) + list := r.Value.([]BindingInfo) assert.Len(t, list, 0) } @@ -167,16 +168,16 @@ func TestTaskAdd_Good_TriggerBroadcast(t *testing.T) { // Capture broadcast actions var triggered ActionTriggered var mu sync.Mutex - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionTriggered); ok { mu.Lock() triggered = a mu.Unlock() } - return nil + return core.Result{OK: true} }) - _, _, _ = c.PERFORM(TaskAdd{Accelerator: "Ctrl+S", Description: "Save"}) + taskRun(c, "keybinding.add", TaskAdd{Accelerator: "Ctrl+S", Description: "Save"}) // Simulate shortcut trigger via mock mp.trigger("Ctrl+S") @@ -190,25 +191,24 @@ func TestTaskAdd_Good_RebindAfterRemove(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Accelerator: "Ctrl+S", Description: "Save"}) - _, _, _ = c.PERFORM(TaskRemove{Accelerator: "Ctrl+S"}) + taskRun(c, "keybinding.add", TaskAdd{Accelerator: "Ctrl+S", Description: "Save"}) + taskRun(c, "keybinding.remove", TaskRemove{Accelerator: "Ctrl+S"}) // Should succeed after remove - _, handled, err := c.PERFORM(TaskAdd{Accelerator: "Ctrl+S", Description: "Save v2"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "keybinding.add", TaskAdd{Accelerator: "Ctrl+S", Description: "Save v2"}) + require.True(t, r.OK) // Verify new description - result, _, _ := c.QUERY(QueryList{}) - list := result.([]BindingInfo) + r2 := c.QUERY(QueryList{}) + list := r2.Value.([]BindingInfo) assert.Len(t, list, 1) assert.Equal(t, "Save v2", list[0].Description) } func TestQueryList_Bad_NoService(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.QUERY(QueryList{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(QueryList{}) + assert.False(t, r.OK) } // --- TaskProcess tests --- @@ -217,22 +217,21 @@ func TestTaskProcess_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Accelerator: "Ctrl+P", Description: "Print"}) + taskRun(c, "keybinding.add", TaskAdd{Accelerator: "Ctrl+P", Description: "Print"}) var triggered ActionTriggered var mu sync.Mutex - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionTriggered); ok { mu.Lock() triggered = a mu.Unlock() } - return nil + return core.Result{OK: true} }) - _, handled, err := c.PERFORM(TaskProcess{Accelerator: "Ctrl+P"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "keybinding.process", TaskProcess{Accelerator: "Ctrl+P"}) + require.True(t, r.OK) mu.Lock() assert.Equal(t, "Ctrl+P", triggered.Accelerator) @@ -243,8 +242,9 @@ func TestTaskProcess_Bad_NotRegistered(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - _, handled, err := c.PERFORM(TaskProcess{Accelerator: "Ctrl+P"}) - assert.True(t, handled) + r := taskRun(c, "keybinding.process", TaskProcess{Accelerator: "Ctrl+P"}) + assert.False(t, r.OK) + err, _ := r.Value.(error) assert.ErrorIs(t, err, ErrorNotRegistered) } @@ -252,12 +252,13 @@ func TestTaskProcess_Ugly_RemovedBinding(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - _, _, _ = c.PERFORM(TaskAdd{Accelerator: "Ctrl+P", Description: "Print"}) - _, _, _ = c.PERFORM(TaskRemove{Accelerator: "Ctrl+P"}) + taskRun(c, "keybinding.add", TaskAdd{Accelerator: "Ctrl+P", Description: "Print"}) + taskRun(c, "keybinding.remove", TaskRemove{Accelerator: "Ctrl+P"}) // After remove, process should fail with ErrorNotRegistered - _, handled, err := c.PERFORM(TaskProcess{Accelerator: "Ctrl+P"}) - assert.True(t, handled) + r := taskRun(c, "keybinding.process", TaskProcess{Accelerator: "Ctrl+P"}) + assert.False(t, r.OK) + err, _ := r.Value.(error) assert.ErrorIs(t, err, ErrorNotRegistered) } @@ -267,8 +268,9 @@ func TestTaskRemove_Bad_ErrorSentinel(t *testing.T) { mp := newMockPlatform() _, c := newTestKeybindingService(t, mp) - _, handled, err := c.PERFORM(TaskRemove{Accelerator: "Ctrl+X"}) - assert.True(t, handled) + r := taskRun(c, "keybinding.remove", TaskRemove{Accelerator: "Ctrl+X"}) + assert.False(t, r.OK) + err, _ := r.Value.(error) assert.ErrorIs(t, err, ErrorNotRegistered) } @@ -284,14 +286,13 @@ func TestQueryList_Ugly_ConcurrentAdds(t *testing.T) { wg.Add(1) go func(acc string) { defer wg.Done() - _, _, _ = c.PERFORM(TaskAdd{Accelerator: acc, Description: acc}) + taskRun(c, "keybinding.add", TaskAdd{Accelerator: acc, Description: acc}) }(accelerator) } wg.Wait() - result, handled, err := c.QUERY(QueryList{}) - require.NoError(t, err) - assert.True(t, handled) - list := result.([]BindingInfo) + r := c.QUERY(QueryList{}) + require.True(t, r.OK) + list := r.Value.([]BindingInfo) assert.Len(t, list, len(accelerators)) } diff --git a/pkg/lifecycle/register.go b/pkg/lifecycle/register.go index 55031e0..ab1e7e6 100644 --- a/pkg/lifecycle/register.go +++ b/pkg/lifecycle/register.go @@ -1,14 +1,14 @@ package lifecycle -import "forge.lthn.ai/core/go/pkg/core" +import core "dappco.re/go/core" // Register(p) binds the lifecycle service to a Core instance. // core.WithService(lifecycle.Register(wailsLifecycle)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, - }, nil + }, OK: true} } } diff --git a/pkg/lifecycle/service.go b/pkg/lifecycle/service.go index 3ba4255..1338e3e 100644 --- a/pkg/lifecycle/service.go +++ b/pkg/lifecycle/service.go @@ -3,7 +3,7 @@ package lifecycle import ( "context" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" ) type Options struct{} @@ -14,7 +14,7 @@ type Service struct { cancels []func() } -func (s *Service) OnStartup(ctx context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { eventActions := map[EventType]func(){ EventApplicationStarted: func() { _ = s.Core().ACTION(ActionApplicationStarted{}) }, EventWillTerminate: func() { _ = s.Core().ACTION(ActionWillTerminate{}) }, @@ -35,17 +35,17 @@ func (s *Service) OnStartup(ctx context.Context) error { }) s.cancels = append(s.cancels, cancel) - return nil + return core.Result{OK: true} } -func (s *Service) OnShutdown(ctx context.Context) error { +func (s *Service) OnShutdown(_ context.Context) core.Result { for _, cancel := range s.cancels { cancel() } s.cancels = nil - return nil + return core.Result{OK: true} } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } diff --git a/pkg/lifecycle/service_test.go b/pkg/lifecycle/service_test.go index 767aefa..359591e 100644 --- a/pkg/lifecycle/service_test.go +++ b/pkg/lifecycle/service_test.go @@ -6,7 +6,7 @@ import ( "sync" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -91,12 +91,11 @@ func (m *mockPlatform) handlerCount() int { func newTestLifecycleService(t *testing.T) (*Service, *core.Core, *mockPlatform) { t.Helper() mock := newMockPlatform() - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "lifecycle") return svc, c, mock } @@ -112,11 +111,11 @@ func TestApplicationStarted_Good(t *testing.T) { _, c, mock := newTestLifecycleService(t) var received bool - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if _, ok := msg.(ActionApplicationStarted); ok { received = true } - return nil + return core.Result{OK: true} }) mock.simulateEvent(EventApplicationStarted) @@ -127,11 +126,11 @@ func TestDidBecomeActive_Good(t *testing.T) { _, c, mock := newTestLifecycleService(t) var received bool - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if _, ok := msg.(ActionDidBecomeActive); ok { received = true } - return nil + return core.Result{OK: true} }) mock.simulateEvent(EventDidBecomeActive) @@ -142,11 +141,11 @@ func TestDidResignActive_Good(t *testing.T) { _, c, mock := newTestLifecycleService(t) var received bool - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if _, ok := msg.(ActionDidResignActive); ok { received = true } - return nil + return core.Result{OK: true} }) mock.simulateEvent(EventDidResignActive) @@ -157,11 +156,11 @@ func TestWillTerminate_Good(t *testing.T) { _, c, mock := newTestLifecycleService(t) var received bool - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if _, ok := msg.(ActionWillTerminate); ok { received = true } - return nil + return core.Result{OK: true} }) mock.simulateEvent(EventWillTerminate) @@ -172,11 +171,11 @@ func TestPowerStatusChanged_Good(t *testing.T) { _, c, mock := newTestLifecycleService(t) var received bool - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if _, ok := msg.(ActionPowerStatusChanged); ok { received = true } - return nil + return core.Result{OK: true} }) mock.simulateEvent(EventPowerStatusChanged) @@ -187,11 +186,11 @@ func TestSystemSuspend_Good(t *testing.T) { _, c, mock := newTestLifecycleService(t) var received bool - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if _, ok := msg.(ActionSystemSuspend); ok { received = true } - return nil + return core.Result{OK: true} }) mock.simulateEvent(EventSystemSuspend) @@ -202,11 +201,11 @@ func TestSystemResume_Good(t *testing.T) { _, c, mock := newTestLifecycleService(t) var received bool - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if _, ok := msg.(ActionSystemResume); ok { received = true } - return nil + return core.Result{OK: true} }) mock.simulateEvent(EventSystemResume) @@ -217,11 +216,11 @@ func TestOpenedWithFile_Good(t *testing.T) { _, c, mock := newTestLifecycleService(t) var receivedPath string - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionOpenedWithFile); ok { receivedPath = a.Path } - return nil + return core.Result{OK: true} }) mock.simulateFileOpen("/Users/snider/Documents/test.txt") @@ -235,23 +234,21 @@ func TestOnShutdown_CancelsAll_Good(t *testing.T) { assert.Greater(t, mock.handlerCount(), 0, "handlers should be registered after OnStartup") // Shutdown should cancel all registrations - err := svc.OnShutdown(context.Background()) - require.NoError(t, err) + require.True(t, svc.OnShutdown(context.Background()).OK) assert.Equal(t, 0, mock.handlerCount(), "all handlers should be cancelled after OnShutdown") } func TestRegister_Bad(t *testing.T) { // No lifecycle service registered — actions are not received - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) + c := core.New(core.WithServiceLock()) var received bool - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if _, ok := msg.(ActionApplicationStarted); ok { received = true } - return nil + return core.Result{OK: true} }) // No way to trigger events without the service diff --git a/pkg/mcp/mcp_test.go b/pkg/mcp/mcp_test.go index d3a3453..fa3de9e 100644 --- a/pkg/mcp/mcp_test.go +++ b/pkg/mcp/mcp_test.go @@ -5,7 +5,7 @@ import ( "context" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/clipboard" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" @@ -13,13 +13,13 @@ import ( ) func TestSubsystem_Good_Name(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) + c := core.New(core.WithServiceLock()) sub := New(c) assert.Equal(t, "display", sub.Name()) } func TestSubsystem_Good_RegisterTools(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) + c := core.New(core.WithServiceLock()) sub := New(c) // RegisterTools should not panic with a real mcp.Server server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil) @@ -37,25 +37,23 @@ func (m *mockClipPlatform) Text() (string, bool) { return m.text, m.ok } func (m *mockClipPlatform) SetText(t string) bool { m.text = t; m.ok = t != ""; return true } func TestMCP_Good_ClipboardRoundTrip(t *testing.T) { - c, err := core.New( + c := core.New( core.WithService(clipboard.Register(&mockClipPlatform{text: "hello", ok: true})), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) // Verify the IPC path that clipboard_read tool handler uses - result, handled, err := c.QUERY(clipboard.QueryText{}) - require.NoError(t, err) - assert.True(t, handled) - content, ok := result.(clipboard.ClipboardContent) + r := c.QUERY(clipboard.QueryText{}) + require.True(t, r.OK) + content, ok := r.Value.(clipboard.ClipboardContent) require.True(t, ok, "expected ClipboardContent type") assert.Equal(t, "hello", content.Text) } func TestMCP_Bad_NoServices(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - // Without any services, QUERY should return handled=false - _, handled, _ := c.QUERY(clipboard.QueryText{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + // Without any services, QUERY should return OK=false + r := c.QUERY(clipboard.QueryText{}) + assert.False(t, r.OK) } diff --git a/pkg/mcp/subsystem.go b/pkg/mcp/subsystem.go index c7e1109..7803ccb 100644 --- a/pkg/mcp/subsystem.go +++ b/pkg/mcp/subsystem.go @@ -2,7 +2,7 @@ package mcp import ( - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/tools_browser.go b/pkg/mcp/tools_browser.go index 18f670c..135e7e0 100644 --- a/pkg/mcp/tools_browser.go +++ b/pkg/mcp/tools_browser.go @@ -4,7 +4,7 @@ package mcp import ( "context" - "forge.lthn.ai/core/gui/pkg/browser" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -18,9 +18,14 @@ type BrowserOpenURLOutput struct { } func (s *Subsystem) browserOpenURL(_ context.Context, _ *mcp.CallToolRequest, input BrowserOpenURLInput) (*mcp.CallToolResult, BrowserOpenURLOutput, error) { - _, _, err := s.core.PERFORM(browser.TaskOpenURL{URL: input.URL}) - if err != nil { - return nil, BrowserOpenURLOutput{}, err + r := s.core.Action("browser.openURL").Run(context.Background(), core.NewOptions( + core.Option{Key: "url", Value: input.URL}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, BrowserOpenURLOutput{}, e + } + return nil, BrowserOpenURLOutput{}, nil } return nil, BrowserOpenURLOutput{Success: true}, nil } diff --git a/pkg/mcp/tools_clipboard.go b/pkg/mcp/tools_clipboard.go index 82aa435..a44e7f9 100644 --- a/pkg/mcp/tools_clipboard.go +++ b/pkg/mcp/tools_clipboard.go @@ -4,7 +4,8 @@ package mcp import ( "context" - coreerr "forge.lthn.ai/core/go-log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/clipboard" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -17,11 +18,14 @@ type ClipboardReadOutput struct { } func (s *Subsystem) clipboardRead(_ context.Context, _ *mcp.CallToolRequest, _ ClipboardReadInput) (*mcp.CallToolResult, ClipboardReadOutput, error) { - result, _, err := s.core.QUERY(clipboard.QueryText{}) - if err != nil { - return nil, ClipboardReadOutput{}, err + r := s.core.QUERY(clipboard.QueryText{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ClipboardReadOutput{}, e + } + return nil, ClipboardReadOutput{}, nil } - content, ok := result.(clipboard.ClipboardContent) + content, ok := r.Value.(clipboard.ClipboardContent) if !ok { return nil, ClipboardReadOutput{}, coreerr.E("mcp.clipboardRead", "unexpected result type", nil) } @@ -38,15 +42,16 @@ type ClipboardWriteOutput struct { } func (s *Subsystem) clipboardWrite(_ context.Context, _ *mcp.CallToolRequest, input ClipboardWriteInput) (*mcp.CallToolResult, ClipboardWriteOutput, error) { - result, _, err := s.core.PERFORM(clipboard.TaskSetText{Text: input.Text}) - if err != nil { - return nil, ClipboardWriteOutput{}, err + r := s.core.Action("clipboard.setText").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: clipboard.TaskSetText{Text: input.Text}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ClipboardWriteOutput{}, e + } + return nil, ClipboardWriteOutput{}, nil } - success, ok := result.(bool) - if !ok { - return nil, ClipboardWriteOutput{}, coreerr.E("mcp.clipboardWrite", "unexpected result type", nil) - } - return nil, ClipboardWriteOutput{Success: success}, nil + return nil, ClipboardWriteOutput{Success: true}, nil } // --- clipboard_has --- @@ -57,11 +62,11 @@ type ClipboardHasOutput struct { } func (s *Subsystem) clipboardHas(_ context.Context, _ *mcp.CallToolRequest, _ ClipboardHasInput) (*mcp.CallToolResult, ClipboardHasOutput, error) { - result, _, err := s.core.QUERY(clipboard.QueryText{}) - if err != nil { - return nil, ClipboardHasOutput{}, err + r := s.core.QUERY(clipboard.QueryText{}) + if !r.OK { + return nil, ClipboardHasOutput{}, nil } - content, ok := result.(clipboard.ClipboardContent) + content, ok := r.Value.(clipboard.ClipboardContent) if !ok { return nil, ClipboardHasOutput{}, coreerr.E("mcp.clipboardHas", "unexpected result type", nil) } @@ -76,15 +81,14 @@ type ClipboardClearOutput struct { } func (s *Subsystem) clipboardClear(_ context.Context, _ *mcp.CallToolRequest, _ ClipboardClearInput) (*mcp.CallToolResult, ClipboardClearOutput, error) { - result, _, err := s.core.PERFORM(clipboard.TaskClear{}) - if err != nil { - return nil, ClipboardClearOutput{}, err + r := s.core.Action("clipboard.clear").Run(context.Background(), core.NewOptions()) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ClipboardClearOutput{}, e + } + return nil, ClipboardClearOutput{}, nil } - success, ok := result.(bool) - if !ok { - return nil, ClipboardClearOutput{}, coreerr.E("mcp.clipboardClear", "unexpected result type", nil) - } - return nil, ClipboardClearOutput{Success: success}, nil + return nil, ClipboardClearOutput{Success: true}, nil } // --- Registration --- diff --git a/pkg/mcp/tools_contextmenu.go b/pkg/mcp/tools_contextmenu.go index d6da3a5..9c8baf2 100644 --- a/pkg/mcp/tools_contextmenu.go +++ b/pkg/mcp/tools_contextmenu.go @@ -5,7 +5,8 @@ import ( "context" "encoding/json" - coreerr "forge.lthn.ai/core/go-log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/contextmenu" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -33,9 +34,14 @@ func (s *Subsystem) contextMenuAdd(_ context.Context, _ *mcp.CallToolRequest, in if err := json.Unmarshal(menuJSON, &menuDef); err != nil { return nil, ContextMenuAddOutput{}, coreerr.E("mcp.contextMenuAdd", "failed to unmarshal menu definition", err) } - _, _, err = s.core.PERFORM(contextmenu.TaskAdd{Name: input.Name, Menu: menuDef}) - if err != nil { - return nil, ContextMenuAddOutput{}, err + r := s.core.Action("contextmenu.add").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: contextmenu.TaskAdd{Name: input.Name, Menu: menuDef}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ContextMenuAddOutput{}, e + } + return nil, ContextMenuAddOutput{}, nil } return nil, ContextMenuAddOutput{Success: true}, nil } @@ -50,9 +56,14 @@ type ContextMenuRemoveOutput struct { } func (s *Subsystem) contextMenuRemove(_ context.Context, _ *mcp.CallToolRequest, input ContextMenuRemoveInput) (*mcp.CallToolResult, ContextMenuRemoveOutput, error) { - _, _, err := s.core.PERFORM(contextmenu.TaskRemove{Name: input.Name}) - if err != nil { - return nil, ContextMenuRemoveOutput{}, err + r := s.core.Action("contextmenu.remove").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: contextmenu.TaskRemove{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ContextMenuRemoveOutput{}, e + } + return nil, ContextMenuRemoveOutput{}, nil } return nil, ContextMenuRemoveOutput{Success: true}, nil } @@ -67,11 +78,14 @@ type ContextMenuGetOutput struct { } func (s *Subsystem) contextMenuGet(_ context.Context, _ *mcp.CallToolRequest, input ContextMenuGetInput) (*mcp.CallToolResult, ContextMenuGetOutput, error) { - result, _, err := s.core.QUERY(contextmenu.QueryGet{Name: input.Name}) - if err != nil { - return nil, ContextMenuGetOutput{}, err + r := s.core.QUERY(contextmenu.QueryGet{Name: input.Name}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ContextMenuGetOutput{}, e + } + return nil, ContextMenuGetOutput{}, nil } - menu, ok := result.(*contextmenu.ContextMenuDef) + menu, ok := r.Value.(*contextmenu.ContextMenuDef) if !ok { return nil, ContextMenuGetOutput{}, coreerr.E("mcp.contextMenuGet", "unexpected result type", nil) } @@ -98,11 +112,14 @@ type ContextMenuListOutput struct { } func (s *Subsystem) contextMenuList(_ context.Context, _ *mcp.CallToolRequest, _ ContextMenuListInput) (*mcp.CallToolResult, ContextMenuListOutput, error) { - result, _, err := s.core.QUERY(contextmenu.QueryList{}) - if err != nil { - return nil, ContextMenuListOutput{}, err + r := s.core.QUERY(contextmenu.QueryList{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ContextMenuListOutput{}, e + } + return nil, ContextMenuListOutput{}, nil } - menus, ok := result.(map[string]contextmenu.ContextMenuDef) + menus, ok := r.Value.(map[string]contextmenu.ContextMenuDef) if !ok { return nil, ContextMenuListOutput{}, coreerr.E("mcp.contextMenuList", "unexpected result type", nil) } diff --git a/pkg/mcp/tools_dialog.go b/pkg/mcp/tools_dialog.go index 5c47e54..3d79c90 100644 --- a/pkg/mcp/tools_dialog.go +++ b/pkg/mcp/tools_dialog.go @@ -4,7 +4,8 @@ package mcp import ( "context" - coreerr "forge.lthn.ai/core/go-log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/dialog" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -25,19 +26,24 @@ type DialogOpenFileOutput struct { } func (s *Subsystem) dialogOpenFile(_ context.Context, _ *mcp.CallToolRequest, input DialogOpenFileInput) (*mcp.CallToolResult, DialogOpenFileOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskOpenFile{Options: dialog.OpenFileOptions{ - Title: input.Title, - Directory: input.Directory, - Filters: input.Filters, - AllowMultiple: input.AllowMultiple, - CanChooseDirectories: input.CanChooseDirectories, - CanChooseFiles: input.CanChooseFiles, - ShowHiddenFiles: input.ShowHiddenFiles, - }}) - if err != nil { - return nil, DialogOpenFileOutput{}, err + r := s.core.Action("dialog.openFile").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskOpenFile{Options: dialog.OpenFileOptions{ + Title: input.Title, + Directory: input.Directory, + Filters: input.Filters, + AllowMultiple: input.AllowMultiple, + CanChooseDirectories: input.CanChooseDirectories, + CanChooseFiles: input.CanChooseFiles, + ShowHiddenFiles: input.ShowHiddenFiles, + }}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DialogOpenFileOutput{}, e + } + return nil, DialogOpenFileOutput{}, nil } - paths, ok := result.([]string) + paths, ok := r.Value.([]string) if !ok { return nil, DialogOpenFileOutput{}, coreerr.E("mcp.dialogOpenFile", "unexpected result type", nil) } @@ -58,17 +64,22 @@ type DialogSaveFileOutput struct { } func (s *Subsystem) dialogSaveFile(_ context.Context, _ *mcp.CallToolRequest, input DialogSaveFileInput) (*mcp.CallToolResult, DialogSaveFileOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskSaveFile{Options: dialog.SaveFileOptions{ - Title: input.Title, - Directory: input.Directory, - Filename: input.Filename, - Filters: input.Filters, - ShowHiddenFiles: input.ShowHiddenFiles, - }}) - if err != nil { - return nil, DialogSaveFileOutput{}, err + r := s.core.Action("dialog.saveFile").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskSaveFile{Options: dialog.SaveFileOptions{ + Title: input.Title, + Directory: input.Directory, + Filename: input.Filename, + Filters: input.Filters, + ShowHiddenFiles: input.ShowHiddenFiles, + }}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DialogSaveFileOutput{}, e + } + return nil, DialogSaveFileOutput{}, nil } - path, ok := result.(string) + path, ok := r.Value.(string) if !ok { return nil, DialogSaveFileOutput{}, coreerr.E("mcp.dialogSaveFile", "unexpected result type", nil) } @@ -87,15 +98,20 @@ type DialogOpenDirectoryOutput struct { } func (s *Subsystem) dialogOpenDirectory(_ context.Context, _ *mcp.CallToolRequest, input DialogOpenDirectoryInput) (*mcp.CallToolResult, DialogOpenDirectoryOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskOpenDirectory{Options: dialog.OpenDirectoryOptions{ - Title: input.Title, - Directory: input.Directory, - ShowHiddenFiles: input.ShowHiddenFiles, - }}) - if err != nil { - return nil, DialogOpenDirectoryOutput{}, err + r := s.core.Action("dialog.openDirectory").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskOpenDirectory{Options: dialog.OpenDirectoryOptions{ + Title: input.Title, + Directory: input.Directory, + ShowHiddenFiles: input.ShowHiddenFiles, + }}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DialogOpenDirectoryOutput{}, e + } + return nil, DialogOpenDirectoryOutput{}, nil } - path, ok := result.(string) + path, ok := r.Value.(string) if !ok { return nil, DialogOpenDirectoryOutput{}, coreerr.E("mcp.dialogOpenDirectory", "unexpected result type", nil) } @@ -114,15 +130,20 @@ type DialogConfirmOutput struct { } func (s *Subsystem) dialogConfirm(_ context.Context, _ *mcp.CallToolRequest, input DialogConfirmInput) (*mcp.CallToolResult, DialogConfirmOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskQuestion{ - Title: input.Title, - Message: input.Message, - Buttons: input.Buttons, - }) - if err != nil { - return nil, DialogConfirmOutput{}, err + r := s.core.Action("dialog.question").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskQuestion{ + Title: input.Title, + Message: input.Message, + Buttons: input.Buttons, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DialogConfirmOutput{}, e + } + return nil, DialogConfirmOutput{}, nil } - button, ok := result.(string) + button, ok := r.Value.(string) if !ok { return nil, DialogConfirmOutput{}, coreerr.E("mcp.dialogConfirm", "unexpected result type", nil) } @@ -140,15 +161,20 @@ type DialogPromptOutput struct { } func (s *Subsystem) dialogPrompt(_ context.Context, _ *mcp.CallToolRequest, input DialogPromptInput) (*mcp.CallToolResult, DialogPromptOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskInfo{ - Title: input.Title, - Message: input.Message, - Buttons: []string{"OK", "Cancel"}, - }) - if err != nil { - return nil, DialogPromptOutput{}, err + r := s.core.Action("dialog.info").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskInfo{ + Title: input.Title, + Message: input.Message, + Buttons: []string{"OK", "Cancel"}, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DialogPromptOutput{}, e + } + return nil, DialogPromptOutput{}, nil } - button, ok := result.(string) + button, ok := r.Value.(string) if !ok { return nil, DialogPromptOutput{}, coreerr.E("mcp.dialogPrompt", "unexpected result type", nil) } @@ -167,15 +193,20 @@ type DialogInfoOutput struct { } func (s *Subsystem) dialogInfo(_ context.Context, _ *mcp.CallToolRequest, input DialogInfoInput) (*mcp.CallToolResult, DialogInfoOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskInfo{ - Title: input.Title, - Message: input.Message, - Buttons: input.Buttons, - }) - if err != nil { - return nil, DialogInfoOutput{}, err + r := s.core.Action("dialog.info").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskInfo{ + Title: input.Title, + Message: input.Message, + Buttons: input.Buttons, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DialogInfoOutput{}, e + } + return nil, DialogInfoOutput{}, nil } - button, ok := result.(string) + button, ok := r.Value.(string) if !ok { return nil, DialogInfoOutput{}, coreerr.E("mcp.dialogInfo", "unexpected result type", nil) } @@ -194,15 +225,20 @@ type DialogWarningOutput struct { } func (s *Subsystem) dialogWarning(_ context.Context, _ *mcp.CallToolRequest, input DialogWarningInput) (*mcp.CallToolResult, DialogWarningOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskWarning{ - Title: input.Title, - Message: input.Message, - Buttons: input.Buttons, - }) - if err != nil { - return nil, DialogWarningOutput{}, err + r := s.core.Action("dialog.warning").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskWarning{ + Title: input.Title, + Message: input.Message, + Buttons: input.Buttons, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DialogWarningOutput{}, e + } + return nil, DialogWarningOutput{}, nil } - button, ok := result.(string) + button, ok := r.Value.(string) if !ok { return nil, DialogWarningOutput{}, coreerr.E("mcp.dialogWarning", "unexpected result type", nil) } @@ -221,15 +257,20 @@ type DialogErrorOutput struct { } func (s *Subsystem) dialogError(_ context.Context, _ *mcp.CallToolRequest, input DialogErrorInput) (*mcp.CallToolResult, DialogErrorOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskError{ - Title: input.Title, - Message: input.Message, - Buttons: input.Buttons, - }) - if err != nil { - return nil, DialogErrorOutput{}, err + r := s.core.Action("dialog.error").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskError{ + Title: input.Title, + Message: input.Message, + Buttons: input.Buttons, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DialogErrorOutput{}, e + } + return nil, DialogErrorOutput{}, nil } - button, ok := result.(string) + button, ok := r.Value.(string) if !ok { return nil, DialogErrorOutput{}, coreerr.E("mcp.dialogError", "unexpected result type", nil) } diff --git a/pkg/mcp/tools_dock.go b/pkg/mcp/tools_dock.go index bff74fc..dcc8863 100644 --- a/pkg/mcp/tools_dock.go +++ b/pkg/mcp/tools_dock.go @@ -4,6 +4,7 @@ package mcp import ( "context" + core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/dock" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -16,9 +17,12 @@ type DockShowOutput struct { } func (s *Subsystem) dockShow(_ context.Context, _ *mcp.CallToolRequest, _ DockShowInput) (*mcp.CallToolResult, DockShowOutput, error) { - _, _, err := s.core.PERFORM(dock.TaskShowIcon{}) - if err != nil { - return nil, DockShowOutput{}, err + r := s.core.Action("dock.showIcon").Run(context.Background(), core.NewOptions()) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DockShowOutput{}, e + } + return nil, DockShowOutput{}, nil } return nil, DockShowOutput{Success: true}, nil } @@ -31,9 +35,12 @@ type DockHideOutput struct { } func (s *Subsystem) dockHide(_ context.Context, _ *mcp.CallToolRequest, _ DockHideInput) (*mcp.CallToolResult, DockHideOutput, error) { - _, _, err := s.core.PERFORM(dock.TaskHideIcon{}) - if err != nil { - return nil, DockHideOutput{}, err + r := s.core.Action("dock.hideIcon").Run(context.Background(), core.NewOptions()) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DockHideOutput{}, e + } + return nil, DockHideOutput{}, nil } return nil, DockHideOutput{Success: true}, nil } @@ -48,9 +55,14 @@ type DockBadgeOutput struct { } func (s *Subsystem) dockBadge(_ context.Context, _ *mcp.CallToolRequest, input DockBadgeInput) (*mcp.CallToolResult, DockBadgeOutput, error) { - _, _, err := s.core.PERFORM(dock.TaskSetBadge{Label: input.Label}) - if err != nil { - return nil, DockBadgeOutput{}, err + r := s.core.Action("dock.setBadge").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dock.TaskSetBadge{Label: input.Label}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, DockBadgeOutput{}, e + } + return nil, DockBadgeOutput{}, nil } return nil, DockBadgeOutput{Success: true}, nil } diff --git a/pkg/mcp/tools_environment.go b/pkg/mcp/tools_environment.go index c8fc831..c7493e5 100644 --- a/pkg/mcp/tools_environment.go +++ b/pkg/mcp/tools_environment.go @@ -4,7 +4,7 @@ package mcp import ( "context" - coreerr "forge.lthn.ai/core/go-log" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/environment" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -17,11 +17,14 @@ type ThemeGetOutput struct { } func (s *Subsystem) themeGet(_ context.Context, _ *mcp.CallToolRequest, _ ThemeGetInput) (*mcp.CallToolResult, ThemeGetOutput, error) { - result, _, err := s.core.QUERY(environment.QueryTheme{}) - if err != nil { - return nil, ThemeGetOutput{}, err + r := s.core.QUERY(environment.QueryTheme{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ThemeGetOutput{}, e + } + return nil, ThemeGetOutput{}, nil } - theme, ok := result.(environment.ThemeInfo) + theme, ok := r.Value.(environment.ThemeInfo) if !ok { return nil, ThemeGetOutput{}, coreerr.E("mcp.themeGet", "unexpected result type", nil) } @@ -36,11 +39,14 @@ type ThemeSystemOutput struct { } func (s *Subsystem) themeSystem(_ context.Context, _ *mcp.CallToolRequest, _ ThemeSystemInput) (*mcp.CallToolResult, ThemeSystemOutput, error) { - result, _, err := s.core.QUERY(environment.QueryInfo{}) - if err != nil { - return nil, ThemeSystemOutput{}, err + r := s.core.QUERY(environment.QueryInfo{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ThemeSystemOutput{}, e + } + return nil, ThemeSystemOutput{}, nil } - info, ok := result.(environment.EnvironmentInfo) + info, ok := r.Value.(environment.EnvironmentInfo) if !ok { return nil, ThemeSystemOutput{}, coreerr.E("mcp.themeSystem", "unexpected result type", nil) } diff --git a/pkg/mcp/tools_events.go b/pkg/mcp/tools_events.go index e9ddca0..935f9d1 100644 --- a/pkg/mcp/tools_events.go +++ b/pkg/mcp/tools_events.go @@ -4,7 +4,8 @@ package mcp import ( "context" - coreerr "forge.lthn.ai/core/go-log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/events" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -20,12 +21,16 @@ type EventEmitOutput struct { } func (s *Subsystem) eventEmit(_ context.Context, _ *mcp.CallToolRequest, input EventEmitInput) (*mcp.CallToolResult, EventEmitOutput, error) { - // result := s.core.PERFORM(events.TaskEmit{Name: "user:login", Data: payload}) - result, _, err := s.core.PERFORM(events.TaskEmit{Name: input.Name, Data: input.Data}) - if err != nil { - return nil, EventEmitOutput{}, err + r := s.core.Action("events.emit").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: events.TaskEmit{Name: input.Name, Data: input.Data}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, EventEmitOutput{}, e + } + return nil, EventEmitOutput{}, nil } - cancelled, ok := result.(bool) + cancelled, ok := r.Value.(bool) if !ok { return nil, EventEmitOutput{}, coreerr.E("mcp.eventEmit", "unexpected result type", nil) } @@ -42,10 +47,14 @@ type EventOnOutput struct { } func (s *Subsystem) eventOn(_ context.Context, _ *mcp.CallToolRequest, input EventOnInput) (*mcp.CallToolResult, EventOnOutput, error) { - // s.core.PERFORM(events.TaskOn{Name: "theme:changed"}) - _, _, err := s.core.PERFORM(events.TaskOn{Name: input.Name}) - if err != nil { - return nil, EventOnOutput{}, err + r := s.core.Action("events.on").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: events.TaskOn{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, EventOnOutput{}, e + } + return nil, EventOnOutput{}, nil } return nil, EventOnOutput{Success: true}, nil } @@ -60,10 +69,14 @@ type EventOffOutput struct { } func (s *Subsystem) eventOff(_ context.Context, _ *mcp.CallToolRequest, input EventOffInput) (*mcp.CallToolResult, EventOffOutput, error) { - // s.core.PERFORM(events.TaskOff{Name: "theme:changed"}) - _, _, err := s.core.PERFORM(events.TaskOff{Name: input.Name}) - if err != nil { - return nil, EventOffOutput{}, err + r := s.core.Action("events.off").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: events.TaskOff{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, EventOffOutput{}, e + } + return nil, EventOffOutput{}, nil } return nil, EventOffOutput{Success: true}, nil } @@ -76,12 +89,14 @@ type EventListOutput struct { } func (s *Subsystem) eventList(_ context.Context, _ *mcp.CallToolRequest, _ EventListInput) (*mcp.CallToolResult, EventListOutput, error) { - // result, _, _ := s.core.QUERY(events.QueryListeners{}) - result, _, err := s.core.QUERY(events.QueryListeners{}) - if err != nil { - return nil, EventListOutput{}, err + r := s.core.QUERY(events.QueryListeners{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, EventListOutput{}, e + } + return nil, EventListOutput{}, nil } - listenerInfos, ok := result.([]events.ListenerInfo) + listenerInfos, ok := r.Value.([]events.ListenerInfo) if !ok { return nil, EventListOutput{}, coreerr.E("mcp.eventList", "unexpected result type", nil) } diff --git a/pkg/mcp/tools_keybinding.go b/pkg/mcp/tools_keybinding.go index f8a5bf6..46cb9af 100644 --- a/pkg/mcp/tools_keybinding.go +++ b/pkg/mcp/tools_keybinding.go @@ -4,6 +4,7 @@ package mcp import ( "context" + core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/keybinding" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -19,9 +20,14 @@ type KeybindingAddOutput struct { } func (s *Subsystem) keybindingAdd(_ context.Context, _ *mcp.CallToolRequest, input KeybindingAddInput) (*mcp.CallToolResult, KeybindingAddOutput, error) { - _, _, err := s.core.PERFORM(keybinding.TaskAdd{Accelerator: input.Accelerator, Description: input.Description}) - if err != nil { - return nil, KeybindingAddOutput{}, err + r := s.core.Action("keybinding.add").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: keybinding.TaskAdd{Accelerator: input.Accelerator, Description: input.Description}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, KeybindingAddOutput{}, e + } + return nil, KeybindingAddOutput{}, nil } return nil, KeybindingAddOutput{Success: true}, nil } @@ -36,9 +42,14 @@ type KeybindingRemoveOutput struct { } func (s *Subsystem) keybindingRemove(_ context.Context, _ *mcp.CallToolRequest, input KeybindingRemoveInput) (*mcp.CallToolResult, KeybindingRemoveOutput, error) { - _, _, err := s.core.PERFORM(keybinding.TaskRemove{Accelerator: input.Accelerator}) - if err != nil { - return nil, KeybindingRemoveOutput{}, err + r := s.core.Action("keybinding.remove").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: keybinding.TaskRemove{Accelerator: input.Accelerator}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, KeybindingRemoveOutput{}, e + } + return nil, KeybindingRemoveOutput{}, nil } return nil, KeybindingRemoveOutput{Success: true}, nil } diff --git a/pkg/mcp/tools_layout.go b/pkg/mcp/tools_layout.go index 1719ce5..2418eee 100644 --- a/pkg/mcp/tools_layout.go +++ b/pkg/mcp/tools_layout.go @@ -4,7 +4,8 @@ package mcp import ( "context" - coreerr "forge.lthn.ai/core/go-log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/window" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -19,9 +20,14 @@ type LayoutSaveOutput struct { } func (s *Subsystem) layoutSave(_ context.Context, _ *mcp.CallToolRequest, input LayoutSaveInput) (*mcp.CallToolResult, LayoutSaveOutput, error) { - _, _, err := s.core.PERFORM(window.TaskSaveLayout{Name: input.Name}) - if err != nil { - return nil, LayoutSaveOutput{}, err + r := s.core.Action("window.saveLayout").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSaveLayout{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, LayoutSaveOutput{}, e + } + return nil, LayoutSaveOutput{}, nil } return nil, LayoutSaveOutput{Success: true}, nil } @@ -36,9 +42,14 @@ type LayoutRestoreOutput struct { } func (s *Subsystem) layoutRestore(_ context.Context, _ *mcp.CallToolRequest, input LayoutRestoreInput) (*mcp.CallToolResult, LayoutRestoreOutput, error) { - _, _, err := s.core.PERFORM(window.TaskRestoreLayout{Name: input.Name}) - if err != nil { - return nil, LayoutRestoreOutput{}, err + r := s.core.Action("window.restoreLayout").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskRestoreLayout{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, LayoutRestoreOutput{}, e + } + return nil, LayoutRestoreOutput{}, nil } return nil, LayoutRestoreOutput{Success: true}, nil } @@ -51,11 +62,14 @@ type LayoutListOutput struct { } func (s *Subsystem) layoutList(_ context.Context, _ *mcp.CallToolRequest, _ LayoutListInput) (*mcp.CallToolResult, LayoutListOutput, error) { - result, _, err := s.core.QUERY(window.QueryLayoutList{}) - if err != nil { - return nil, LayoutListOutput{}, err + r := s.core.QUERY(window.QueryLayoutList{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, LayoutListOutput{}, e + } + return nil, LayoutListOutput{}, nil } - layouts, ok := result.([]window.LayoutInfo) + layouts, ok := r.Value.([]window.LayoutInfo) if !ok { return nil, LayoutListOutput{}, coreerr.E("mcp.layoutList", "unexpected result type", nil) } @@ -72,9 +86,14 @@ type LayoutDeleteOutput struct { } func (s *Subsystem) layoutDelete(_ context.Context, _ *mcp.CallToolRequest, input LayoutDeleteInput) (*mcp.CallToolResult, LayoutDeleteOutput, error) { - _, _, err := s.core.PERFORM(window.TaskDeleteLayout{Name: input.Name}) - if err != nil { - return nil, LayoutDeleteOutput{}, err + r := s.core.Action("window.deleteLayout").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskDeleteLayout{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, LayoutDeleteOutput{}, e + } + return nil, LayoutDeleteOutput{}, nil } return nil, LayoutDeleteOutput{Success: true}, nil } @@ -89,11 +108,14 @@ type LayoutGetOutput struct { } func (s *Subsystem) layoutGet(_ context.Context, _ *mcp.CallToolRequest, input LayoutGetInput) (*mcp.CallToolResult, LayoutGetOutput, error) { - result, _, err := s.core.QUERY(window.QueryLayoutGet{Name: input.Name}) - if err != nil { - return nil, LayoutGetOutput{}, err + r := s.core.QUERY(window.QueryLayoutGet{Name: input.Name}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, LayoutGetOutput{}, e + } + return nil, LayoutGetOutput{}, nil } - layout, ok := result.(*window.Layout) + layout, ok := r.Value.(*window.Layout) if !ok { return nil, LayoutGetOutput{}, coreerr.E("mcp.layoutGet", "unexpected result type", nil) } @@ -111,9 +133,14 @@ type LayoutTileOutput struct { } func (s *Subsystem) layoutTile(_ context.Context, _ *mcp.CallToolRequest, input LayoutTileInput) (*mcp.CallToolResult, LayoutTileOutput, error) { - _, _, err := s.core.PERFORM(window.TaskTileWindows{Mode: input.Mode, Windows: input.Windows}) - if err != nil { - return nil, LayoutTileOutput{}, err + r := s.core.Action("window.tileWindows").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskTileWindows{Mode: input.Mode, Windows: input.Windows}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, LayoutTileOutput{}, e + } + return nil, LayoutTileOutput{}, nil } return nil, LayoutTileOutput{Success: true}, nil } @@ -129,9 +156,14 @@ type LayoutSnapOutput struct { } func (s *Subsystem) layoutSnap(_ context.Context, _ *mcp.CallToolRequest, input LayoutSnapInput) (*mcp.CallToolResult, LayoutSnapOutput, error) { - _, _, err := s.core.PERFORM(window.TaskSnapWindow{Name: input.Name, Position: input.Position}) - if err != nil { - return nil, LayoutSnapOutput{}, err + r := s.core.Action("window.snapWindow").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSnapWindow{Name: input.Name, Position: input.Position}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, LayoutSnapOutput{}, e + } + return nil, LayoutSnapOutput{}, nil } return nil, LayoutSnapOutput{Success: true}, nil } @@ -148,9 +180,14 @@ type LayoutStackOutput struct { } func (s *Subsystem) layoutStack(_ context.Context, _ *mcp.CallToolRequest, input LayoutStackInput) (*mcp.CallToolResult, LayoutStackOutput, error) { - _, _, err := s.core.PERFORM(window.TaskStackWindows{Windows: input.Windows, OffsetX: input.OffsetX, OffsetY: input.OffsetY}) - if err != nil { - return nil, LayoutStackOutput{}, err + r := s.core.Action("window.stackWindows").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskStackWindows{Windows: input.Windows, OffsetX: input.OffsetX, OffsetY: input.OffsetY}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, LayoutStackOutput{}, e + } + return nil, LayoutStackOutput{}, nil } return nil, LayoutStackOutput{Success: true}, nil } @@ -166,9 +203,14 @@ type LayoutWorkflowOutput struct { } func (s *Subsystem) layoutWorkflow(_ context.Context, _ *mcp.CallToolRequest, input LayoutWorkflowInput) (*mcp.CallToolResult, LayoutWorkflowOutput, error) { - _, _, err := s.core.PERFORM(window.TaskApplyWorkflow{Workflow: input.Workflow, Windows: input.Windows}) - if err != nil { - return nil, LayoutWorkflowOutput{}, err + r := s.core.Action("window.applyWorkflow").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskApplyWorkflow{Workflow: input.Workflow, Windows: input.Windows}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, LayoutWorkflowOutput{}, e + } + return nil, LayoutWorkflowOutput{}, nil } return nil, LayoutWorkflowOutput{Success: true}, nil } diff --git a/pkg/mcp/tools_lifecycle.go b/pkg/mcp/tools_lifecycle.go index 26715bd..9ac77fd 100644 --- a/pkg/mcp/tools_lifecycle.go +++ b/pkg/mcp/tools_lifecycle.go @@ -17,10 +17,7 @@ type AppQuitOutput struct { func (s *Subsystem) appQuit(_ context.Context, _ *mcp.CallToolRequest, _ AppQuitInput) (*mcp.CallToolResult, AppQuitOutput, error) { // Broadcast the will-terminate action which triggers application shutdown - err := s.core.ACTION(lifecycle.ActionWillTerminate{}) - if err != nil { - return nil, AppQuitOutput{}, err - } + _ = s.core.ACTION(lifecycle.ActionWillTerminate{}) return nil, AppQuitOutput{Success: true}, nil } diff --git a/pkg/mcp/tools_notification.go b/pkg/mcp/tools_notification.go index 0b965d3..f63b53b 100644 --- a/pkg/mcp/tools_notification.go +++ b/pkg/mcp/tools_notification.go @@ -4,7 +4,8 @@ package mcp import ( "context" - coreerr "forge.lthn.ai/core/go-log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/notification" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -21,13 +22,18 @@ type NotificationShowOutput struct { } func (s *Subsystem) notificationShow(_ context.Context, _ *mcp.CallToolRequest, input NotificationShowInput) (*mcp.CallToolResult, NotificationShowOutput, error) { - _, _, err := s.core.PERFORM(notification.TaskSend{Options: notification.NotificationOptions{ - Title: input.Title, - Message: input.Message, - Subtitle: input.Subtitle, - }}) - if err != nil { - return nil, NotificationShowOutput{}, err + r := s.core.Action("notification.send").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: notification.TaskSend{Options: notification.NotificationOptions{ + Title: input.Title, + Message: input.Message, + Subtitle: input.Subtitle, + }}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, NotificationShowOutput{}, e + } + return nil, NotificationShowOutput{}, nil } return nil, NotificationShowOutput{Success: true}, nil } @@ -40,11 +46,14 @@ type NotificationPermissionRequestOutput struct { } func (s *Subsystem) notificationPermissionRequest(_ context.Context, _ *mcp.CallToolRequest, _ NotificationPermissionRequestInput) (*mcp.CallToolResult, NotificationPermissionRequestOutput, error) { - result, _, err := s.core.PERFORM(notification.TaskRequestPermission{}) - if err != nil { - return nil, NotificationPermissionRequestOutput{}, err + r := s.core.Action("notification.requestPermission").Run(context.Background(), core.NewOptions()) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, NotificationPermissionRequestOutput{}, e + } + return nil, NotificationPermissionRequestOutput{}, nil } - granted, ok := result.(bool) + granted, ok := r.Value.(bool) if !ok { return nil, NotificationPermissionRequestOutput{}, coreerr.E("mcp.notificationPermissionRequest", "unexpected result type", nil) } @@ -59,11 +68,14 @@ type NotificationPermissionCheckOutput struct { } func (s *Subsystem) notificationPermissionCheck(_ context.Context, _ *mcp.CallToolRequest, _ NotificationPermissionCheckInput) (*mcp.CallToolResult, NotificationPermissionCheckOutput, error) { - result, _, err := s.core.QUERY(notification.QueryPermission{}) - if err != nil { - return nil, NotificationPermissionCheckOutput{}, err + r := s.core.QUERY(notification.QueryPermission{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, NotificationPermissionCheckOutput{}, e + } + return nil, NotificationPermissionCheckOutput{}, nil } - status, ok := result.(notification.PermissionStatus) + status, ok := r.Value.(notification.PermissionStatus) if !ok { return nil, NotificationPermissionCheckOutput{}, coreerr.E("mcp.notificationPermissionCheck", "unexpected result type", nil) } diff --git a/pkg/mcp/tools_screen.go b/pkg/mcp/tools_screen.go index 7f86e7e..f49b325 100644 --- a/pkg/mcp/tools_screen.go +++ b/pkg/mcp/tools_screen.go @@ -4,7 +4,7 @@ package mcp import ( "context" - coreerr "forge.lthn.ai/core/go-log" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/screen" "forge.lthn.ai/core/gui/pkg/window" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -18,11 +18,14 @@ type ScreenListOutput struct { } func (s *Subsystem) screenList(_ context.Context, _ *mcp.CallToolRequest, _ ScreenListInput) (*mcp.CallToolResult, ScreenListOutput, error) { - result, _, err := s.core.QUERY(screen.QueryAll{}) - if err != nil { - return nil, ScreenListOutput{}, err + r := s.core.QUERY(screen.QueryAll{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ScreenListOutput{}, e + } + return nil, ScreenListOutput{}, nil } - screens, ok := result.([]screen.Screen) + screens, ok := r.Value.([]screen.Screen) if !ok { return nil, ScreenListOutput{}, coreerr.E("mcp.screenList", "unexpected result type", nil) } @@ -39,11 +42,14 @@ type ScreenGetOutput struct { } func (s *Subsystem) screenGet(_ context.Context, _ *mcp.CallToolRequest, input ScreenGetInput) (*mcp.CallToolResult, ScreenGetOutput, error) { - result, _, err := s.core.QUERY(screen.QueryByID{ID: input.ID}) - if err != nil { - return nil, ScreenGetOutput{}, err + r := s.core.QUERY(screen.QueryByID{ID: input.ID}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ScreenGetOutput{}, e + } + return nil, ScreenGetOutput{}, nil } - scr, ok := result.(*screen.Screen) + scr, ok := r.Value.(*screen.Screen) if !ok { return nil, ScreenGetOutput{}, coreerr.E("mcp.screenGet", "unexpected result type", nil) } @@ -58,11 +64,14 @@ type ScreenPrimaryOutput struct { } func (s *Subsystem) screenPrimary(_ context.Context, _ *mcp.CallToolRequest, _ ScreenPrimaryInput) (*mcp.CallToolResult, ScreenPrimaryOutput, error) { - result, _, err := s.core.QUERY(screen.QueryPrimary{}) - if err != nil { - return nil, ScreenPrimaryOutput{}, err + r := s.core.QUERY(screen.QueryPrimary{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ScreenPrimaryOutput{}, e + } + return nil, ScreenPrimaryOutput{}, nil } - scr, ok := result.(*screen.Screen) + scr, ok := r.Value.(*screen.Screen) if !ok { return nil, ScreenPrimaryOutput{}, coreerr.E("mcp.screenPrimary", "unexpected result type", nil) } @@ -80,11 +89,14 @@ type ScreenAtPointOutput struct { } func (s *Subsystem) screenAtPoint(_ context.Context, _ *mcp.CallToolRequest, input ScreenAtPointInput) (*mcp.CallToolResult, ScreenAtPointOutput, error) { - result, _, err := s.core.QUERY(screen.QueryAtPoint{X: input.X, Y: input.Y}) - if err != nil { - return nil, ScreenAtPointOutput{}, err + r := s.core.QUERY(screen.QueryAtPoint{X: input.X, Y: input.Y}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ScreenAtPointOutput{}, e + } + return nil, ScreenAtPointOutput{}, nil } - scr, ok := result.(*screen.Screen) + scr, ok := r.Value.(*screen.Screen) if !ok { return nil, ScreenAtPointOutput{}, coreerr.E("mcp.screenAtPoint", "unexpected result type", nil) } @@ -99,11 +111,14 @@ type ScreenWorkAreasOutput struct { } func (s *Subsystem) screenWorkAreas(_ context.Context, _ *mcp.CallToolRequest, _ ScreenWorkAreasInput) (*mcp.CallToolResult, ScreenWorkAreasOutput, error) { - result, _, err := s.core.QUERY(screen.QueryWorkAreas{}) - if err != nil { - return nil, ScreenWorkAreasOutput{}, err + r := s.core.QUERY(screen.QueryWorkAreas{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, ScreenWorkAreasOutput{}, e + } + return nil, ScreenWorkAreasOutput{}, nil } - areas, ok := result.([]screen.Rect) + areas, ok := r.Value.([]screen.Rect) if !ok { return nil, ScreenWorkAreasOutput{}, coreerr.E("mcp.screenWorkAreas", "unexpected result type", nil) } @@ -120,21 +135,21 @@ type ScreenForWindowOutput struct { } func (s *Subsystem) screenForWindow(_ context.Context, _ *mcp.CallToolRequest, input ScreenForWindowInput) (*mcp.CallToolResult, ScreenForWindowOutput, error) { - result, _, err := s.core.QUERY(window.QueryWindowByName{Name: input.Name}) - if err != nil { - return nil, ScreenForWindowOutput{}, err + r := s.core.QUERY(window.QueryWindowByName{Name: input.Name}) + if !r.OK { + return nil, ScreenForWindowOutput{}, nil } - info, _ := result.(*window.WindowInfo) + info, _ := r.Value.(*window.WindowInfo) if info == nil { return nil, ScreenForWindowOutput{}, nil } centerX := info.X + info.Width/2 centerY := info.Y + info.Height/2 - screenResult, _, err := s.core.QUERY(screen.QueryAtPoint{X: centerX, Y: centerY}) - if err != nil { - return nil, ScreenForWindowOutput{}, err + r2 := s.core.QUERY(screen.QueryAtPoint{X: centerX, Y: centerY}) + if !r2.OK { + return nil, ScreenForWindowOutput{}, nil } - scr, _ := screenResult.(*screen.Screen) + scr, _ := r2.Value.(*screen.Screen) return nil, ScreenForWindowOutput{Screen: scr}, nil } diff --git a/pkg/mcp/tools_tray.go b/pkg/mcp/tools_tray.go index d5efb45..8ad9f8d 100644 --- a/pkg/mcp/tools_tray.go +++ b/pkg/mcp/tools_tray.go @@ -4,7 +4,8 @@ package mcp import ( "context" - coreerr "forge.lthn.ai/core/go-log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/systray" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -19,9 +20,14 @@ type TraySetIconOutput struct { } func (s *Subsystem) traySetIcon(_ context.Context, _ *mcp.CallToolRequest, input TraySetIconInput) (*mcp.CallToolResult, TraySetIconOutput, error) { - _, _, err := s.core.PERFORM(systray.TaskSetTrayIcon{Data: input.Data}) - if err != nil { - return nil, TraySetIconOutput{}, err + r := s.core.Action("systray.setIcon").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: systray.TaskSetTrayIcon{Data: input.Data}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, TraySetIconOutput{}, e + } + return nil, TraySetIconOutput{}, nil } return nil, TraySetIconOutput{Success: true}, nil } @@ -64,11 +70,14 @@ type TrayInfoOutput struct { } func (s *Subsystem) trayInfo(_ context.Context, _ *mcp.CallToolRequest, _ TrayInfoInput) (*mcp.CallToolResult, TrayInfoOutput, error) { - result, _, err := s.core.QUERY(systray.QueryConfig{}) - if err != nil { - return nil, TrayInfoOutput{}, err + r := s.core.QUERY(systray.QueryConfig{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, TrayInfoOutput{}, e + } + return nil, TrayInfoOutput{}, nil } - config, ok := result.(map[string]any) + config, ok := r.Value.(map[string]any) if !ok { return nil, TrayInfoOutput{}, coreerr.E("mcp.trayInfo", "unexpected result type", nil) } diff --git a/pkg/mcp/tools_webview.go b/pkg/mcp/tools_webview.go index 923fe36..593d2a1 100644 --- a/pkg/mcp/tools_webview.go +++ b/pkg/mcp/tools_webview.go @@ -4,7 +4,8 @@ package mcp import ( "context" - coreerr "forge.lthn.ai/core/go-log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/webview" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -22,11 +23,16 @@ type WebviewEvalOutput struct { } func (s *Subsystem) webviewEval(_ context.Context, _ *mcp.CallToolRequest, input WebviewEvalInput) (*mcp.CallToolResult, WebviewEvalOutput, error) { - result, _, err := s.core.PERFORM(webview.TaskEvaluate{Window: input.Window, Script: input.Script}) - if err != nil { - return nil, WebviewEvalOutput{}, err + r := s.core.Action("webview.evaluate").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskEvaluate{Window: input.Window, Script: input.Script}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewEvalOutput{}, e + } + return nil, WebviewEvalOutput{}, nil } - return nil, WebviewEvalOutput{Result: result, Window: input.Window}, nil + return nil, WebviewEvalOutput{Result: r.Value, Window: input.Window}, nil } // --- webview_click --- @@ -41,9 +47,14 @@ type WebviewClickOutput struct { } func (s *Subsystem) webviewClick(_ context.Context, _ *mcp.CallToolRequest, input WebviewClickInput) (*mcp.CallToolResult, WebviewClickOutput, error) { - _, _, err := s.core.PERFORM(webview.TaskClick{Window: input.Window, Selector: input.Selector}) - if err != nil { - return nil, WebviewClickOutput{}, err + r := s.core.Action("webview.click").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskClick{Window: input.Window, Selector: input.Selector}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewClickOutput{}, e + } + return nil, WebviewClickOutput{}, nil } return nil, WebviewClickOutput{Success: true}, nil } @@ -61,9 +72,14 @@ type WebviewTypeOutput struct { } func (s *Subsystem) webviewType(_ context.Context, _ *mcp.CallToolRequest, input WebviewTypeInput) (*mcp.CallToolResult, WebviewTypeOutput, error) { - _, _, err := s.core.PERFORM(webview.TaskType{Window: input.Window, Selector: input.Selector, Text: input.Text}) - if err != nil { - return nil, WebviewTypeOutput{}, err + r := s.core.Action("webview.type").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskType{Window: input.Window, Selector: input.Selector, Text: input.Text}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewTypeOutput{}, e + } + return nil, WebviewTypeOutput{}, nil } return nil, WebviewTypeOutput{Success: true}, nil } @@ -80,9 +96,14 @@ type WebviewNavigateOutput struct { } func (s *Subsystem) webviewNavigate(_ context.Context, _ *mcp.CallToolRequest, input WebviewNavigateInput) (*mcp.CallToolResult, WebviewNavigateOutput, error) { - _, _, err := s.core.PERFORM(webview.TaskNavigate{Window: input.Window, URL: input.URL}) - if err != nil { - return nil, WebviewNavigateOutput{}, err + r := s.core.Action("webview.navigate").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskNavigate{Window: input.Window, URL: input.URL}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewNavigateOutput{}, e + } + return nil, WebviewNavigateOutput{}, nil } return nil, WebviewNavigateOutput{Success: true}, nil } @@ -99,11 +120,16 @@ type WebviewScreenshotOutput struct { } func (s *Subsystem) webviewScreenshot(_ context.Context, _ *mcp.CallToolRequest, input WebviewScreenshotInput) (*mcp.CallToolResult, WebviewScreenshotOutput, error) { - result, _, err := s.core.PERFORM(webview.TaskScreenshot{Window: input.Window}) - if err != nil { - return nil, WebviewScreenshotOutput{}, err + r := s.core.Action("webview.screenshot").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskScreenshot{Window: input.Window}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewScreenshotOutput{}, e + } + return nil, WebviewScreenshotOutput{}, nil } - sr, ok := result.(webview.ScreenshotResult) + sr, ok := r.Value.(webview.ScreenshotResult) if !ok { return nil, WebviewScreenshotOutput{}, coreerr.E("mcp.webviewScreenshot", "unexpected result type", nil) } @@ -123,9 +149,14 @@ type WebviewScrollOutput struct { } func (s *Subsystem) webviewScroll(_ context.Context, _ *mcp.CallToolRequest, input WebviewScrollInput) (*mcp.CallToolResult, WebviewScrollOutput, error) { - _, _, err := s.core.PERFORM(webview.TaskScroll{Window: input.Window, X: input.X, Y: input.Y}) - if err != nil { - return nil, WebviewScrollOutput{}, err + r := s.core.Action("webview.scroll").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskScroll{Window: input.Window, X: input.X, Y: input.Y}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewScrollOutput{}, e + } + return nil, WebviewScrollOutput{}, nil } return nil, WebviewScrollOutput{Success: true}, nil } @@ -142,9 +173,14 @@ type WebviewHoverOutput struct { } func (s *Subsystem) webviewHover(_ context.Context, _ *mcp.CallToolRequest, input WebviewHoverInput) (*mcp.CallToolResult, WebviewHoverOutput, error) { - _, _, err := s.core.PERFORM(webview.TaskHover{Window: input.Window, Selector: input.Selector}) - if err != nil { - return nil, WebviewHoverOutput{}, err + r := s.core.Action("webview.hover").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskHover{Window: input.Window, Selector: input.Selector}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewHoverOutput{}, e + } + return nil, WebviewHoverOutput{}, nil } return nil, WebviewHoverOutput{Success: true}, nil } @@ -162,9 +198,14 @@ type WebviewSelectOutput struct { } func (s *Subsystem) webviewSelect(_ context.Context, _ *mcp.CallToolRequest, input WebviewSelectInput) (*mcp.CallToolResult, WebviewSelectOutput, error) { - _, _, err := s.core.PERFORM(webview.TaskSelect{Window: input.Window, Selector: input.Selector, Value: input.Value}) - if err != nil { - return nil, WebviewSelectOutput{}, err + r := s.core.Action("webview.select").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskSelect{Window: input.Window, Selector: input.Selector, Value: input.Value}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewSelectOutput{}, e + } + return nil, WebviewSelectOutput{}, nil } return nil, WebviewSelectOutput{Success: true}, nil } @@ -182,9 +223,14 @@ type WebviewCheckOutput struct { } func (s *Subsystem) webviewCheck(_ context.Context, _ *mcp.CallToolRequest, input WebviewCheckInput) (*mcp.CallToolResult, WebviewCheckOutput, error) { - _, _, err := s.core.PERFORM(webview.TaskCheck{Window: input.Window, Selector: input.Selector, Checked: input.Checked}) - if err != nil { - return nil, WebviewCheckOutput{}, err + r := s.core.Action("webview.check").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskCheck{Window: input.Window, Selector: input.Selector, Checked: input.Checked}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewCheckOutput{}, e + } + return nil, WebviewCheckOutput{}, nil } return nil, WebviewCheckOutput{Success: true}, nil } @@ -202,9 +248,14 @@ type WebviewUploadOutput struct { } func (s *Subsystem) webviewUpload(_ context.Context, _ *mcp.CallToolRequest, input WebviewUploadInput) (*mcp.CallToolResult, WebviewUploadOutput, error) { - _, _, err := s.core.PERFORM(webview.TaskUploadFile{Window: input.Window, Selector: input.Selector, Paths: input.Paths}) - if err != nil { - return nil, WebviewUploadOutput{}, err + r := s.core.Action("webview.uploadFile").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskUploadFile{Window: input.Window, Selector: input.Selector, Paths: input.Paths}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewUploadOutput{}, e + } + return nil, WebviewUploadOutput{}, nil } return nil, WebviewUploadOutput{Success: true}, nil } @@ -222,9 +273,14 @@ type WebviewViewportOutput struct { } func (s *Subsystem) webviewViewport(_ context.Context, _ *mcp.CallToolRequest, input WebviewViewportInput) (*mcp.CallToolResult, WebviewViewportOutput, error) { - _, _, err := s.core.PERFORM(webview.TaskSetViewport{Window: input.Window, Width: input.Width, Height: input.Height}) - if err != nil { - return nil, WebviewViewportOutput{}, err + r := s.core.Action("webview.setViewport").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskSetViewport{Window: input.Window, Width: input.Width, Height: input.Height}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewViewportOutput{}, e + } + return nil, WebviewViewportOutput{}, nil } return nil, WebviewViewportOutput{Success: true}, nil } @@ -242,11 +298,14 @@ type WebviewConsoleOutput struct { } func (s *Subsystem) webviewConsole(_ context.Context, _ *mcp.CallToolRequest, input WebviewConsoleInput) (*mcp.CallToolResult, WebviewConsoleOutput, error) { - result, _, err := s.core.QUERY(webview.QueryConsole{Window: input.Window, Level: input.Level, Limit: input.Limit}) - if err != nil { - return nil, WebviewConsoleOutput{}, err + r := s.core.QUERY(webview.QueryConsole{Window: input.Window, Level: input.Level, Limit: input.Limit}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewConsoleOutput{}, e + } + return nil, WebviewConsoleOutput{}, nil } - msgs, ok := result.([]webview.ConsoleMessage) + msgs, ok := r.Value.([]webview.ConsoleMessage) if !ok { return nil, WebviewConsoleOutput{}, coreerr.E("mcp.webviewConsole", "unexpected result type", nil) } @@ -264,9 +323,14 @@ type WebviewConsoleClearOutput struct { } func (s *Subsystem) webviewConsoleClear(_ context.Context, _ *mcp.CallToolRequest, input WebviewConsoleClearInput) (*mcp.CallToolResult, WebviewConsoleClearOutput, error) { - _, _, err := s.core.PERFORM(webview.TaskClearConsole{Window: input.Window}) - if err != nil { - return nil, WebviewConsoleClearOutput{}, err + r := s.core.Action("webview.clearConsole").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: webview.TaskClearConsole{Window: input.Window}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewConsoleClearOutput{}, e + } + return nil, WebviewConsoleClearOutput{}, nil } return nil, WebviewConsoleClearOutput{Success: true}, nil } @@ -283,11 +347,14 @@ type WebviewQueryOutput struct { } func (s *Subsystem) webviewQuery(_ context.Context, _ *mcp.CallToolRequest, input WebviewQueryInput) (*mcp.CallToolResult, WebviewQueryOutput, error) { - result, _, err := s.core.QUERY(webview.QuerySelector{Window: input.Window, Selector: input.Selector}) - if err != nil { - return nil, WebviewQueryOutput{}, err + r := s.core.QUERY(webview.QuerySelector{Window: input.Window, Selector: input.Selector}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewQueryOutput{}, e + } + return nil, WebviewQueryOutput{}, nil } - el, ok := result.(*webview.ElementInfo) + el, ok := r.Value.(*webview.ElementInfo) if !ok { return nil, WebviewQueryOutput{}, coreerr.E("mcp.webviewQuery", "unexpected result type", nil) } @@ -306,11 +373,14 @@ type WebviewQueryAllOutput struct { } func (s *Subsystem) webviewQueryAll(_ context.Context, _ *mcp.CallToolRequest, input WebviewQueryAllInput) (*mcp.CallToolResult, WebviewQueryAllOutput, error) { - result, _, err := s.core.QUERY(webview.QuerySelectorAll{Window: input.Window, Selector: input.Selector}) - if err != nil { - return nil, WebviewQueryAllOutput{}, err + r := s.core.QUERY(webview.QuerySelectorAll{Window: input.Window, Selector: input.Selector}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewQueryAllOutput{}, e + } + return nil, WebviewQueryAllOutput{}, nil } - els, ok := result.([]*webview.ElementInfo) + els, ok := r.Value.([]*webview.ElementInfo) if !ok { return nil, WebviewQueryAllOutput{}, coreerr.E("mcp.webviewQueryAll", "unexpected result type", nil) } @@ -329,11 +399,14 @@ type WebviewDOMTreeOutput struct { } func (s *Subsystem) webviewDOMTree(_ context.Context, _ *mcp.CallToolRequest, input WebviewDOMTreeInput) (*mcp.CallToolResult, WebviewDOMTreeOutput, error) { - result, _, err := s.core.QUERY(webview.QueryDOMTree{Window: input.Window, Selector: input.Selector}) - if err != nil { - return nil, WebviewDOMTreeOutput{}, err + r := s.core.QUERY(webview.QueryDOMTree{Window: input.Window, Selector: input.Selector}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewDOMTreeOutput{}, e + } + return nil, WebviewDOMTreeOutput{}, nil } - html, ok := result.(string) + html, ok := r.Value.(string) if !ok { return nil, WebviewDOMTreeOutput{}, coreerr.E("mcp.webviewDOMTree", "unexpected result type", nil) } @@ -351,11 +424,14 @@ type WebviewURLOutput struct { } func (s *Subsystem) webviewURL(_ context.Context, _ *mcp.CallToolRequest, input WebviewURLInput) (*mcp.CallToolResult, WebviewURLOutput, error) { - result, _, err := s.core.QUERY(webview.QueryURL{Window: input.Window}) - if err != nil { - return nil, WebviewURLOutput{}, err + r := s.core.QUERY(webview.QueryURL{Window: input.Window}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewURLOutput{}, e + } + return nil, WebviewURLOutput{}, nil } - url, ok := result.(string) + url, ok := r.Value.(string) if !ok { return nil, WebviewURLOutput{}, coreerr.E("mcp.webviewURL", "unexpected result type", nil) } @@ -373,11 +449,14 @@ type WebviewTitleOutput struct { } func (s *Subsystem) webviewTitle(_ context.Context, _ *mcp.CallToolRequest, input WebviewTitleInput) (*mcp.CallToolResult, WebviewTitleOutput, error) { - result, _, err := s.core.QUERY(webview.QueryTitle{Window: input.Window}) - if err != nil { - return nil, WebviewTitleOutput{}, err + r := s.core.QUERY(webview.QueryTitle{Window: input.Window}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WebviewTitleOutput{}, e + } + return nil, WebviewTitleOutput{}, nil } - title, ok := result.(string) + title, ok := r.Value.(string) if !ok { return nil, WebviewTitleOutput{}, coreerr.E("mcp.webviewTitle", "unexpected result type", nil) } diff --git a/pkg/mcp/tools_window.go b/pkg/mcp/tools_window.go index 677c2ac..a8f7e08 100644 --- a/pkg/mcp/tools_window.go +++ b/pkg/mcp/tools_window.go @@ -4,7 +4,8 @@ package mcp import ( "context" - coreerr "forge.lthn.ai/core/go-log" + core "dappco.re/go/core" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/gui/pkg/window" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -17,11 +18,14 @@ type WindowListOutput struct { } func (s *Subsystem) windowList(_ context.Context, _ *mcp.CallToolRequest, _ WindowListInput) (*mcp.CallToolResult, WindowListOutput, error) { - result, _, err := s.core.QUERY(window.QueryWindowList{}) - if err != nil { - return nil, WindowListOutput{}, err + r := s.core.QUERY(window.QueryWindowList{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowListOutput{}, e + } + return nil, WindowListOutput{}, nil } - windows, ok := result.([]window.WindowInfo) + windows, ok := r.Value.([]window.WindowInfo) if !ok { return nil, WindowListOutput{}, coreerr.E("mcp.windowList", "unexpected result type", nil) } @@ -38,11 +42,14 @@ type WindowGetOutput struct { } func (s *Subsystem) windowGet(_ context.Context, _ *mcp.CallToolRequest, input WindowGetInput) (*mcp.CallToolResult, WindowGetOutput, error) { - result, _, err := s.core.QUERY(window.QueryWindowByName{Name: input.Name}) - if err != nil { - return nil, WindowGetOutput{}, err + r := s.core.QUERY(window.QueryWindowByName{Name: input.Name}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowGetOutput{}, e + } + return nil, WindowGetOutput{}, nil } - info, ok := result.(*window.WindowInfo) + info, ok := r.Value.(*window.WindowInfo) if !ok { return nil, WindowGetOutput{}, coreerr.E("mcp.windowGet", "unexpected result type", nil) } @@ -57,11 +64,14 @@ type WindowFocusedOutput struct { } func (s *Subsystem) windowFocused(_ context.Context, _ *mcp.CallToolRequest, _ WindowFocusedInput) (*mcp.CallToolResult, WindowFocusedOutput, error) { - result, _, err := s.core.QUERY(window.QueryWindowList{}) - if err != nil { - return nil, WindowFocusedOutput{}, err + r := s.core.QUERY(window.QueryWindowList{}) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowFocusedOutput{}, e + } + return nil, WindowFocusedOutput{}, nil } - windows, ok := result.([]window.WindowInfo) + windows, ok := r.Value.([]window.WindowInfo) if !ok { return nil, WindowFocusedOutput{}, coreerr.E("mcp.windowFocused", "unexpected result type", nil) } @@ -89,21 +99,26 @@ type WindowCreateOutput struct { } func (s *Subsystem) windowCreate(_ context.Context, _ *mcp.CallToolRequest, input WindowCreateInput) (*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, - }, - }) - if err != nil { - return nil, WindowCreateOutput{}, err + r := s.core.Action("window.open").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: 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, + }, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowCreateOutput{}, e + } + return nil, WindowCreateOutput{}, coreerr.E("mcp.windowCreate", "window.open failed", nil) } - info, ok := result.(window.WindowInfo) + info, ok := r.Value.(window.WindowInfo) if !ok { return nil, WindowCreateOutput{}, coreerr.E("mcp.windowCreate", "unexpected result type", nil) } @@ -120,9 +135,14 @@ type WindowCloseOutput struct { } func (s *Subsystem) windowClose(_ context.Context, _ *mcp.CallToolRequest, input WindowCloseInput) (*mcp.CallToolResult, WindowCloseOutput, error) { - _, _, err := s.core.PERFORM(window.TaskCloseWindow{Name: input.Name}) - if err != nil { - return nil, WindowCloseOutput{}, err + r := s.core.Action("window.close").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskCloseWindow{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowCloseOutput{}, e + } + return nil, WindowCloseOutput{}, nil } return nil, WindowCloseOutput{Success: true}, nil } @@ -139,9 +159,14 @@ type WindowPositionOutput struct { } func (s *Subsystem) windowPosition(_ context.Context, _ *mcp.CallToolRequest, input WindowPositionInput) (*mcp.CallToolResult, WindowPositionOutput, error) { - _, _, err := s.core.PERFORM(window.TaskSetPosition{Name: input.Name, X: input.X, Y: input.Y}) - if err != nil { - return nil, WindowPositionOutput{}, err + r := s.core.Action("window.setPosition").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetPosition{Name: input.Name, X: input.X, Y: input.Y}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowPositionOutput{}, e + } + return nil, WindowPositionOutput{}, nil } return nil, WindowPositionOutput{Success: true}, nil } @@ -158,9 +183,14 @@ type WindowSizeOutput struct { } func (s *Subsystem) windowSize(_ context.Context, _ *mcp.CallToolRequest, input WindowSizeInput) (*mcp.CallToolResult, WindowSizeOutput, error) { - _, _, err := s.core.PERFORM(window.TaskSetSize{Name: input.Name, Width: input.Width, Height: input.Height}) - if err != nil { - return nil, WindowSizeOutput{}, err + r := s.core.Action("window.setSize").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetSize{Name: input.Name, Width: input.Width, Height: input.Height}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowSizeOutput{}, e + } + return nil, WindowSizeOutput{}, nil } return nil, WindowSizeOutput{Success: true}, nil } @@ -179,13 +209,23 @@ type WindowBoundsOutput struct { } func (s *Subsystem) windowBounds(_ context.Context, _ *mcp.CallToolRequest, input WindowBoundsInput) (*mcp.CallToolResult, WindowBoundsOutput, error) { - _, _, err := s.core.PERFORM(window.TaskSetPosition{Name: input.Name, X: input.X, Y: input.Y}) - if err != nil { - return nil, WindowBoundsOutput{}, err + r := s.core.Action("window.setPosition").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetPosition{Name: input.Name, X: input.X, Y: input.Y}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowBoundsOutput{}, e + } + return nil, WindowBoundsOutput{}, nil } - _, _, err = s.core.PERFORM(window.TaskSetSize{Name: input.Name, Width: input.Width, Height: input.Height}) - if err != nil { - return nil, WindowBoundsOutput{}, err + r = s.core.Action("window.setSize").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetSize{Name: input.Name, Width: input.Width, Height: input.Height}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowBoundsOutput{}, e + } + return nil, WindowBoundsOutput{}, nil } return nil, WindowBoundsOutput{Success: true}, nil } @@ -200,9 +240,14 @@ type WindowMaximizeOutput struct { } func (s *Subsystem) windowMaximize(_ context.Context, _ *mcp.CallToolRequest, input WindowMaximizeInput) (*mcp.CallToolResult, WindowMaximizeOutput, error) { - _, _, err := s.core.PERFORM(window.TaskMaximise{Name: input.Name}) - if err != nil { - return nil, WindowMaximizeOutput{}, err + r := s.core.Action("window.maximise").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskMaximise{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowMaximizeOutput{}, e + } + return nil, WindowMaximizeOutput{}, nil } return nil, WindowMaximizeOutput{Success: true}, nil } @@ -217,9 +262,14 @@ type WindowMinimizeOutput struct { } func (s *Subsystem) windowMinimize(_ context.Context, _ *mcp.CallToolRequest, input WindowMinimizeInput) (*mcp.CallToolResult, WindowMinimizeOutput, error) { - _, _, err := s.core.PERFORM(window.TaskMinimise{Name: input.Name}) - if err != nil { - return nil, WindowMinimizeOutput{}, err + r := s.core.Action("window.minimise").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskMinimise{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowMinimizeOutput{}, e + } + return nil, WindowMinimizeOutput{}, nil } return nil, WindowMinimizeOutput{Success: true}, nil } @@ -234,9 +284,14 @@ type WindowRestoreOutput struct { } func (s *Subsystem) windowRestore(_ context.Context, _ *mcp.CallToolRequest, input WindowRestoreInput) (*mcp.CallToolResult, WindowRestoreOutput, error) { - _, _, err := s.core.PERFORM(window.TaskRestore{Name: input.Name}) - if err != nil { - return nil, WindowRestoreOutput{}, err + r := s.core.Action("window.restore").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskRestore{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowRestoreOutput{}, e + } + return nil, WindowRestoreOutput{}, nil } return nil, WindowRestoreOutput{Success: true}, nil } @@ -251,9 +306,14 @@ type WindowFocusOutput struct { } func (s *Subsystem) windowFocus(_ context.Context, _ *mcp.CallToolRequest, input WindowFocusInput) (*mcp.CallToolResult, WindowFocusOutput, error) { - _, _, err := s.core.PERFORM(window.TaskFocus{Name: input.Name}) - if err != nil { - return nil, WindowFocusOutput{}, err + r := s.core.Action("window.focus").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskFocus{Name: input.Name}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowFocusOutput{}, e + } + return nil, WindowFocusOutput{}, nil } return nil, WindowFocusOutput{Success: true}, nil } @@ -269,9 +329,14 @@ type WindowTitleOutput struct { } func (s *Subsystem) windowTitle(_ context.Context, _ *mcp.CallToolRequest, input WindowTitleInput) (*mcp.CallToolResult, WindowTitleOutput, error) { - _, _, err := s.core.PERFORM(window.TaskSetTitle{Name: input.Name, Title: input.Title}) - if err != nil { - return nil, WindowTitleOutput{}, err + r := s.core.Action("window.setTitle").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetTitle{Name: input.Name, Title: input.Title}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowTitleOutput{}, e + } + return nil, WindowTitleOutput{}, nil } return nil, WindowTitleOutput{Success: true}, nil } @@ -286,11 +351,11 @@ type WindowTitleGetOutput struct { } func (s *Subsystem) windowTitleGet(_ context.Context, _ *mcp.CallToolRequest, input WindowTitleGetInput) (*mcp.CallToolResult, WindowTitleGetOutput, error) { - result, _, err := s.core.QUERY(window.QueryWindowByName{Name: input.Name}) - if err != nil { - return nil, WindowTitleGetOutput{}, err + r := s.core.QUERY(window.QueryWindowByName{Name: input.Name}) + if !r.OK { + return nil, WindowTitleGetOutput{}, nil } - info, _ := result.(*window.WindowInfo) + info, _ := r.Value.(*window.WindowInfo) if info == nil { return nil, WindowTitleGetOutput{}, nil } @@ -308,9 +373,14 @@ type WindowVisibilityOutput struct { } func (s *Subsystem) windowVisibility(_ context.Context, _ *mcp.CallToolRequest, input WindowVisibilityInput) (*mcp.CallToolResult, WindowVisibilityOutput, error) { - _, _, err := s.core.PERFORM(window.TaskSetVisibility{Name: input.Name, Visible: input.Visible}) - if err != nil { - return nil, WindowVisibilityOutput{}, err + r := s.core.Action("window.setVisibility").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetVisibility{Name: input.Name, Visible: input.Visible}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowVisibilityOutput{}, e + } + return nil, WindowVisibilityOutput{}, nil } return nil, WindowVisibilityOutput{Success: true}, nil } @@ -326,9 +396,14 @@ type WindowAlwaysOnTopOutput struct { } func (s *Subsystem) windowAlwaysOnTop(_ context.Context, _ *mcp.CallToolRequest, input WindowAlwaysOnTopInput) (*mcp.CallToolResult, WindowAlwaysOnTopOutput, error) { - _, _, err := s.core.PERFORM(window.TaskSetAlwaysOnTop{Name: input.Name, AlwaysOnTop: input.AlwaysOnTop}) - if err != nil { - return nil, WindowAlwaysOnTopOutput{}, err + r := s.core.Action("window.setAlwaysOnTop").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetAlwaysOnTop{Name: input.Name, AlwaysOnTop: input.AlwaysOnTop}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowAlwaysOnTopOutput{}, e + } + return nil, WindowAlwaysOnTopOutput{}, nil } return nil, WindowAlwaysOnTopOutput{Success: true}, nil } @@ -347,11 +422,16 @@ type WindowBackgroundColourOutput struct { } func (s *Subsystem) windowBackgroundColour(_ context.Context, _ *mcp.CallToolRequest, input WindowBackgroundColourInput) (*mcp.CallToolResult, WindowBackgroundColourOutput, error) { - _, _, err := s.core.PERFORM(window.TaskSetBackgroundColour{ - Name: input.Name, Red: input.Red, Green: input.Green, Blue: input.Blue, Alpha: input.Alpha, - }) - if err != nil { - return nil, WindowBackgroundColourOutput{}, err + r := s.core.Action("window.setBackgroundColour").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskSetBackgroundColour{ + Name: input.Name, Red: input.Red, Green: input.Green, Blue: input.Blue, Alpha: input.Alpha, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowBackgroundColourOutput{}, e + } + return nil, WindowBackgroundColourOutput{}, nil } return nil, WindowBackgroundColourOutput{Success: true}, nil } @@ -367,9 +447,14 @@ type WindowFullscreenOutput struct { } func (s *Subsystem) windowFullscreen(_ context.Context, _ *mcp.CallToolRequest, input WindowFullscreenInput) (*mcp.CallToolResult, WindowFullscreenOutput, error) { - _, _, err := s.core.PERFORM(window.TaskFullscreen{Name: input.Name, Fullscreen: input.Fullscreen}) - if err != nil { - return nil, WindowFullscreenOutput{}, err + r := s.core.Action("window.fullscreen").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskFullscreen{Name: input.Name, Fullscreen: input.Fullscreen}}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return nil, WindowFullscreenOutput{}, e + } + return nil, WindowFullscreenOutput{}, nil } return nil, WindowFullscreenOutput{Success: true}, nil } diff --git a/pkg/menu/register.go b/pkg/menu/register.go index b97e3cf..c551894 100644 --- a/pkg/menu/register.go +++ b/pkg/menu/register.go @@ -1,15 +1,15 @@ package menu -import "forge.lthn.ai/core/go/pkg/core" +import core "dappco.re/go/core" // Register(p) binds the menu service to a Core instance. // core.WithService(menu.Register(wailsMenu)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, manager: NewManager(p), - }, nil + }, OK: true} } } diff --git a/pkg/menu/service.go b/pkg/menu/service.go index 1a3f838..33d5b32 100644 --- a/pkg/menu/service.go +++ b/pkg/menu/service.go @@ -3,7 +3,7 @@ package menu import ( "context" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" ) type Options struct{} @@ -16,16 +16,21 @@ type Service struct { showDevTools bool } -func (s *Service) OnStartup(ctx context.Context) error { - configValue, handled, _ := s.Core().QUERY(QueryConfig{}) - if handled { - if menuConfig, ok := configValue.(map[string]any); ok { +func (s *Service) OnStartup(_ context.Context) core.Result { + r := s.Core().QUERY(QueryConfig{}) + if r.OK { + if menuConfig, ok := r.Value.(map[string]any); ok { s.applyConfig(menuConfig) } } s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) - return nil + s.Core().Action("menu.setAppMenu", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskSetAppMenu) + s.menuItems = t.Items + s.manager.SetApplicationMenu(t.Items) + return core.Result{OK: true} + }) + return core.Result{OK: true} } func (s *Service) applyConfig(configData map[string]any) { @@ -40,27 +45,16 @@ func (s *Service) ShowDevTools() bool { return s.showDevTools } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } -func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q.(type) { case QueryGetAppMenu: - return s.menuItems, true, nil + return core.Result{Value: s.menuItems, OK: true} default: - return nil, false, nil - } -} - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskSetAppMenu: - s.menuItems = t.Items - s.manager.SetApplicationMenu(t.Items) - return nil, true, nil - default: - return nil, false, nil + return core.Result{} } } diff --git a/pkg/menu/service_test.go b/pkg/menu/service_test.go index a36d16f..0f5542d 100644 --- a/pkg/menu/service_test.go +++ b/pkg/menu/service_test.go @@ -4,23 +4,28 @@ import ( "context" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newTestMenuService(t *testing.T) (*Service, *core.Core) { t.Helper() - c, err := core.New( + c := core.New( core.WithService(Register(newMockPlatform())), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "menu") return svc, c } +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + func TestRegister_Good(t *testing.T) { svc, _ := newTestMenuService(t) assert.NotNil(t, svc) @@ -37,28 +42,25 @@ func TestTaskSetAppMenu_Good(t *testing.T) { {Label: "Quit"}, }}, } - _, handled, err := c.PERFORM(TaskSetAppMenu{Items: items}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "menu.setAppMenu", TaskSetAppMenu{Items: items}) + require.True(t, r.OK) } func TestQueryGetAppMenu_Good(t *testing.T) { _, c := newTestMenuService(t) items := []MenuItem{{Label: "File"}, {Label: "Edit"}} - _, _, _ = c.PERFORM(TaskSetAppMenu{Items: items}) + taskRun(c, "menu.setAppMenu", TaskSetAppMenu{Items: items}) - result, handled, err := c.QUERY(QueryGetAppMenu{}) - require.NoError(t, err) - assert.True(t, handled) - menuItems := result.([]MenuItem) + r := c.QUERY(QueryGetAppMenu{}) + require.True(t, r.OK) + menuItems := r.Value.([]MenuItem) assert.Len(t, menuItems, 2) assert.Equal(t, "File", menuItems[0].Label) } func TestTaskSetAppMenu_Bad(t *testing.T) { - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.PERFORM(TaskSetAppMenu{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("menu.setAppMenu").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } diff --git a/pkg/notification/service.go b/pkg/notification/service.go index ab8c086..5c59335 100644 --- a/pkg/notification/service.go +++ b/pkg/notification/service.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/dialog" ) @@ -18,50 +18,51 @@ type Service struct { categories map[string]NotificationCategory } -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, categories: make(map[string]NotificationCategory), - }, nil + }, OK: true} } } -func (s *Service) OnStartup(ctx context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) - return nil + s.Core().Action("notification.send", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskSend) + return core.Result{Value: nil, OK: true}.New(s.send(t.Options)) + }) + s.Core().Action("notification.requestPermission", func(_ context.Context, _ core.Options) core.Result { + granted, err := s.platform.RequestPermission() + return core.Result{}.New(granted, err) + }) + s.Core().Action("notification.revokePermission", func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: nil, OK: true}.New(s.platform.RevokePermission()) + }) + s.Core().Action("notification.registerCategory", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskRegisterCategory) + s.categories[t.Category.ID] = t.Category + return core.Result{OK: true} + }) + return core.Result{OK: true} } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } -func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q.(type) { case QueryPermission: granted, err := s.platform.CheckPermission() - return PermissionStatus{Granted: granted}, true, err + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: PermissionStatus{Granted: granted}, OK: true} default: - return nil, false, nil - } -} - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskSend: - return nil, true, s.send(t.Options) - case TaskRequestPermission: - granted, err := s.platform.RequestPermission() - return granted, true, err - case TaskRevokePermission: - return nil, true, s.platform.RevokePermission() - case TaskRegisterCategory: - s.categories[t.Category.ID] = t.Category - return nil, true, nil - default: - return nil, false, nil + return core.Result{} } } @@ -92,18 +93,25 @@ func (s *Service) fallbackDialog(options NotificationOptions) error { dt = dialog.DialogInfo } - msg := options.Message + message := options.Message if options.Subtitle != "" { - msg = options.Subtitle + "\n\n" + msg + message = options.Subtitle + "\n\n" + message } - _, _, err := s.Core().PERFORM(dialog.TaskMessageDialog{ - Options: dialog.MessageDialogOptions{ - Type: dt, - Title: options.Title, - Message: msg, - Buttons: []string{"OK"}, - }, - }) - return err + r := s.Core().Action("dialog.message").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: dialog.TaskMessageDialog{ + Options: dialog.MessageDialogOptions{ + Type: dt, + Title: options.Title, + Message: message, + Buttons: []string{"OK"}, + }, + }}, + )) + if !r.OK { + if err, ok := r.Value.(error); ok { + return err + } + } + return nil } diff --git a/pkg/notification/service_test.go b/pkg/notification/service_test.go index c6be05d..1ee6e23 100644 --- a/pkg/notification/service_test.go +++ b/pkg/notification/service_test.go @@ -6,20 +6,20 @@ import ( "errors" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/dialog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type mockPlatform struct { - sendErr error - permGranted bool - permErr error - revokeErr error - revokeCalled bool - lastOpts NotificationOptions - sendCalled bool + sendErr error + permGranted bool + permErr error + revokeErr error + revokeCalled bool + lastOpts NotificationOptions + sendCalled bool } func (m *mockPlatform) Send(opts NotificationOptions) error { @@ -54,15 +54,20 @@ func (m *mockDialogPlatform) MessageDialog(opts dialog.MessageDialogOptions) (st func newTestService(t *testing.T) (*mockPlatform, *core.Core) { t.Helper() mock := &mockPlatform{permGranted: true} - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) return mock, c } +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + func TestRegister_Good(t *testing.T) { _, c := newTestService(t) svc := core.MustServiceFor[*Service](c, "notification") @@ -71,11 +76,10 @@ func TestRegister_Good(t *testing.T) { func TestTaskSend_Good(t *testing.T) { mock, c := newTestService(t) - _, handled, err := c.PERFORM(TaskSend{ + r := taskRun(c, "notification.send", TaskSend{ Options: NotificationOptions{Title: "Test", Message: "Hello"}, }) - require.NoError(t, err) - assert.True(t, handled) + require.True(t, r.OK) assert.True(t, mock.sendCalled) assert.Equal(t, "Test", mock.lastOpts.Title) } @@ -84,70 +88,63 @@ func TestTaskSend_Fallback_Good(t *testing.T) { // Platform fails -> falls back to dialog via IPC mockNotify := &mockPlatform{sendErr: errors.New("no permission")} mockDlg := &mockDialogPlatform{} - c, err := core.New( + c := core.New( core.WithService(dialog.Register(mockDlg)), core.WithService(Register(mockNotify)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) - _, handled, err := c.PERFORM(TaskSend{ + r := taskRun(c, "notification.send", TaskSend{ Options: NotificationOptions{Title: "Warn", Message: "Oops", Severity: SeverityWarning}, }) - assert.True(t, handled) - assert.NoError(t, err) // fallback succeeds even though platform failed + assert.True(t, r.OK) // fallback succeeds even though platform failed assert.True(t, mockDlg.messageCalled) assert.Equal(t, dialog.DialogWarning, mockDlg.lastMsgOpts.Type) } func TestQueryPermission_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryPermission{}) - require.NoError(t, err) - assert.True(t, handled) - status := result.(PermissionStatus) + r := c.QUERY(QueryPermission{}) + require.True(t, r.OK) + status := r.Value.(PermissionStatus) assert.True(t, status.Granted) } func TestTaskRequestPermission_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.PERFORM(TaskRequestPermission{}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, true, result) + r := c.Action("notification.requestPermission").Run(context.Background(), core.NewOptions()) + require.True(t, r.OK) + assert.Equal(t, true, r.Value) } func TestTaskSend_Bad(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskSend{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("notification.send").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } // --- TaskRevokePermission --- func TestTaskRevokePermission_Good(t *testing.T) { mock, c := newTestService(t) - _, handled, err := c.PERFORM(TaskRevokePermission{}) - require.NoError(t, err) - assert.True(t, handled) + r := c.Action("notification.revokePermission").Run(context.Background(), core.NewOptions()) + require.True(t, r.OK) assert.True(t, mock.revokeCalled) } func TestTaskRevokePermission_Bad(t *testing.T) { mock, c := newTestService(t) mock.revokeErr = errors.New("cannot revoke") - _, handled, err := c.PERFORM(TaskRevokePermission{}) - assert.True(t, handled) - assert.Error(t, err) + r := c.Action("notification.revokePermission").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestTaskRevokePermission_Ugly(t *testing.T) { - // No service registered — PERFORM returns handled=false - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.PERFORM(TaskRevokePermission{}) - assert.False(t, handled) + // No service registered — action is not registered + c := core.New(core.WithServiceLock()) + r := c.Action("notification.revokePermission").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } // --- TaskRegisterCategory --- @@ -161,9 +158,8 @@ func TestTaskRegisterCategory_Good(t *testing.T) { {ID: "delete", Title: "Delete", Destructive: true}, }, } - _, handled, err := c.PERFORM(TaskRegisterCategory{Category: category}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "notification.registerCategory", TaskRegisterCategory{Category: category}) + require.True(t, r.OK) svc := core.MustServiceFor[*Service](c, "notification") stored, ok := svc.categories["message"] @@ -174,11 +170,10 @@ func TestTaskRegisterCategory_Good(t *testing.T) { } func TestTaskRegisterCategory_Bad(t *testing.T) { - // No service registered — PERFORM returns handled=false - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.PERFORM(TaskRegisterCategory{Category: NotificationCategory{ID: "x"}}) - assert.False(t, handled) + // No service registered — action is not registered + c := core.New(core.WithServiceLock()) + r := taskRun(c, "notification.registerCategory", TaskRegisterCategory{Category: NotificationCategory{ID: "x"}}) + assert.False(t, r.OK) } func TestTaskRegisterCategory_Ugly(t *testing.T) { @@ -187,10 +182,8 @@ func TestTaskRegisterCategory_Ugly(t *testing.T) { first := NotificationCategory{ID: "chat", Actions: []NotificationAction{{ID: "a", Title: "A"}}} second := NotificationCategory{ID: "chat", Actions: []NotificationAction{{ID: "b", Title: "B"}, {ID: "c", Title: "C"}}} - _, _, err := c.PERFORM(TaskRegisterCategory{Category: first}) - require.NoError(t, err) - _, _, err = c.PERFORM(TaskRegisterCategory{Category: second}) - require.NoError(t, err) + require.True(t, taskRun(c, "notification.registerCategory", TaskRegisterCategory{Category: first}).OK) + require.True(t, taskRun(c, "notification.registerCategory", TaskRegisterCategory{Category: second}).OK) svc := core.MustServiceFor[*Service](c, "notification") assert.Equal(t, 2, len(svc.categories["chat"].Actions)) @@ -210,9 +203,8 @@ func TestTaskSend_WithActions_Good(t *testing.T) { {ID: "dismiss", Title: "Dismiss"}, }, } - _, handled, err := c.PERFORM(TaskSend{Options: options}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "notification.send", TaskSend{Options: options}) + require.True(t, r.OK) assert.Equal(t, "message", mock.lastOpts.CategoryID) assert.Equal(t, 2, len(mock.lastOpts.Actions)) } @@ -223,11 +215,11 @@ func TestActionNotificationActionTriggered_Good(t *testing.T) { // ActionNotificationActionTriggered is broadcast by external code; confirm it can be received _, c := newTestService(t) var received *ActionNotificationActionTriggered - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionNotificationActionTriggered); ok { received = &a } - return nil + return core.Result{OK: true} }) _ = c.ACTION(ActionNotificationActionTriggered{NotificationID: "n1", ActionID: "reply"}) require.NotNil(t, received) @@ -238,11 +230,11 @@ func TestActionNotificationActionTriggered_Good(t *testing.T) { func TestActionNotificationDismissed_Good(t *testing.T) { _, c := newTestService(t) var received *ActionNotificationDismissed - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionNotificationDismissed); ok { received = &a } - return nil + return core.Result{OK: true} }) _ = c.ACTION(ActionNotificationDismissed{ID: "n2"}) require.NotNil(t, received) @@ -251,22 +243,19 @@ func TestActionNotificationDismissed_Good(t *testing.T) { func TestQueryPermission_Bad(t *testing.T) { // No service — QUERY returns handled=false - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.QUERY(QueryPermission{}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(QueryPermission{}) + assert.False(t, r.OK) } func TestQueryPermission_Ugly(t *testing.T) { - // Platform returns error — QUERY returns error with handled=true + // Platform returns error — QUERY returns OK=false (framework does not propagate Value for failed queries) mock := &mockPlatform{permErr: errors.New("platform error")} - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) - _, handled, queryErr := c.QUERY(QueryPermission{}) - assert.True(t, handled) - assert.Error(t, queryErr) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) + r := c.QUERY(QueryPermission{}) + assert.False(t, r.OK) } diff --git a/pkg/screen/service.go b/pkg/screen/service.go index 085ac62..21af392 100644 --- a/pkg/screen/service.go +++ b/pkg/screen/service.go @@ -4,7 +4,7 @@ package screen import ( "context" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" ) type Options struct{} @@ -16,40 +16,40 @@ type Service struct { // Register(p) binds the screen service to a Core instance. // core.WithService(screen.Register(wailsScreen)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, - }, nil + }, OK: true} } } -func (s *Service) OnStartup(ctx context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) - return nil + return core.Result{OK: true} } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } -func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q := q.(type) { case QueryAll: - return s.platform.GetAll(), true, nil + return core.Result{Value: s.platform.GetAll(), OK: true} case QueryPrimary: - return s.platform.GetPrimary(), true, nil + return core.Result{Value: s.platform.GetPrimary(), OK: true} case QueryByID: - return s.queryByID(q.ID), true, nil + return core.Result{Value: s.queryByID(q.ID), OK: true} case QueryAtPoint: - return s.queryAtPoint(q.X, q.Y), true, nil + return core.Result{Value: s.queryAtPoint(q.X, q.Y), OK: true} case QueryWorkAreas: - return s.queryWorkAreas(), true, nil + return core.Result{Value: s.queryWorkAreas(), OK: true} case QueryCurrent: - return s.platform.GetCurrent(), true, nil + return core.Result{Value: s.platform.GetCurrent(), OK: true} default: - return nil, false, nil + return core.Result{} } } diff --git a/pkg/screen/service_test.go b/pkg/screen/service_test.go index 736aec1..a8e8c16 100644 --- a/pkg/screen/service_test.go +++ b/pkg/screen/service_test.go @@ -5,7 +5,7 @@ import ( "context" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -49,12 +49,11 @@ func newTestService(t *testing.T) (*mockPlatform, *core.Core) { }, }, } - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) return mock, c } @@ -66,19 +65,17 @@ func TestRegister_Good(t *testing.T) { func TestQueryAll_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryAll{}) - require.NoError(t, err) - assert.True(t, handled) - screens := result.([]Screen) + r := c.QUERY(QueryAll{}) + require.True(t, r.OK) + screens := r.Value.([]Screen) assert.Len(t, screens, 2) } func TestQueryPrimary_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryPrimary{}) - require.NoError(t, err) - assert.True(t, handled) - scr := result.(*Screen) + r := c.QUERY(QueryPrimary{}) + require.True(t, r.OK) + scr := r.Value.(*Screen) require.NotNil(t, scr) assert.Equal(t, "Built-in", scr.Name) assert.True(t, scr.IsPrimary) @@ -86,54 +83,49 @@ func TestQueryPrimary_Good(t *testing.T) { func TestQueryByID_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryByID{ID: "2"}) - require.NoError(t, err) - assert.True(t, handled) - scr := result.(*Screen) + r := c.QUERY(QueryByID{ID: "2"}) + require.True(t, r.OK) + scr := r.Value.(*Screen) require.NotNil(t, scr) assert.Equal(t, "External", scr.Name) } func TestQueryByID_Bad(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryByID{ID: "99"}) - require.NoError(t, err) - assert.True(t, handled) - assert.Nil(t, result) + r := c.QUERY(QueryByID{ID: "99"}) + require.True(t, r.OK) + assert.Nil(t, r.Value) } func TestQueryAtPoint_Good(t *testing.T) { _, c := newTestService(t) // Point on primary screen - result, handled, err := c.QUERY(QueryAtPoint{X: 100, Y: 100}) - require.NoError(t, err) - assert.True(t, handled) - scr := result.(*Screen) + r := c.QUERY(QueryAtPoint{X: 100, Y: 100}) + require.True(t, r.OK) + scr := r.Value.(*Screen) require.NotNil(t, scr) assert.Equal(t, "Built-in", scr.Name) // Point on external screen - result, _, _ = c.QUERY(QueryAtPoint{X: 3000, Y: 500}) - scr = result.(*Screen) + r2 := c.QUERY(QueryAtPoint{X: 3000, Y: 500}) + scr = r2.Value.(*Screen) require.NotNil(t, scr) assert.Equal(t, "External", scr.Name) } func TestQueryAtPoint_Bad(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryAtPoint{X: -1000, Y: -1000}) - require.NoError(t, err) - assert.True(t, handled) - assert.Nil(t, result) + r := c.QUERY(QueryAtPoint{X: -1000, Y: -1000}) + require.True(t, r.OK) + assert.Nil(t, r.Value) } func TestQueryWorkAreas_Good(t *testing.T) { _, c := newTestService(t) - result, handled, err := c.QUERY(QueryWorkAreas{}) - require.NoError(t, err) - assert.True(t, handled) - areas := result.([]Rect) + r := c.QUERY(QueryWorkAreas{}) + require.True(t, r.OK) + areas := r.Value.([]Rect) assert.Len(t, areas, 2) assert.Equal(t, 38, areas[0].Y) // primary has menu bar offset } @@ -143,10 +135,9 @@ func TestQueryWorkAreas_Good(t *testing.T) { func TestQueryCurrent_Good(t *testing.T) { // current falls back to primary when not explicitly set _, c := newTestService(t) - result, handled, err := c.QUERY(QueryCurrent{}) - require.NoError(t, err) - assert.True(t, handled) - scr := result.(*Screen) + r := c.QUERY(QueryCurrent{}) + require.True(t, r.OK) + scr := r.Value.(*Screen) require.NotNil(t, scr) assert.True(t, scr.IsPrimary) assert.Equal(t, "Built-in", scr.Name) @@ -155,17 +146,15 @@ func TestQueryCurrent_Good(t *testing.T) { func TestQueryCurrent_Bad(t *testing.T) { // no screens at all → GetCurrent returns nil mock := &mockPlatform{screens: []Screen{}} - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) - result, handled, err := c.QUERY(QueryCurrent{}) - require.NoError(t, err) - assert.True(t, handled) - assert.Nil(t, result) + r := c.QUERY(QueryCurrent{}) + require.True(t, r.OK) + assert.Nil(t, r.Value) } func TestQueryCurrent_Ugly(t *testing.T) { @@ -179,15 +168,14 @@ func TestQueryCurrent_Ugly(t *testing.T) { }, } mock.current = &mock.screens[1] - c, err := core.New( + c := core.New( core.WithService(Register(mock)), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) - result, _, _ := c.QUERY(QueryCurrent{}) - scr := result.(*Screen) + r := c.QUERY(QueryCurrent{}) + scr := r.Value.(*Screen) require.NotNil(t, scr) assert.Equal(t, "External", scr.Name) } diff --git a/pkg/systray/menu.go b/pkg/systray/menu.go index ee77fd3..e5e57b1 100644 --- a/pkg/systray/menu.go +++ b/pkg/systray/menu.go @@ -1,7 +1,7 @@ // pkg/systray/menu.go package systray -import coreerr "forge.lthn.ai/core/go-log" +import coreerr "dappco.re/go/core/log" // SetMenu sets a dynamic menu on the tray from TrayMenuItem descriptors. func (m *Manager) SetMenu(items []TrayMenuItem) error { diff --git a/pkg/systray/register.go b/pkg/systray/register.go index bbae64c..a619397 100644 --- a/pkg/systray/register.go +++ b/pkg/systray/register.go @@ -1,15 +1,15 @@ package systray -import "forge.lthn.ai/core/go/pkg/core" +import core "dappco.re/go/core" // Register(p) binds the systray service to a Core instance. // core.WithService(systray.Register(wailsSystray)) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, manager: NewManager(p), - }, nil + }, OK: true} } } diff --git a/pkg/systray/service.go b/pkg/systray/service.go index f585e7e..0d883ed 100644 --- a/pkg/systray/service.go +++ b/pkg/systray/service.go @@ -3,7 +3,7 @@ package systray import ( "context" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" ) type Options struct{} @@ -15,15 +15,30 @@ type Service struct { iconPath string } -func (s *Service) OnStartup(ctx context.Context) error { - configValue, handled, _ := s.Core().QUERY(QueryConfig{}) - if handled { - if trayConfig, ok := configValue.(map[string]any); ok { +func (s *Service) OnStartup(_ context.Context) core.Result { + r := s.Core().QUERY(QueryConfig{}) + if r.OK { + if trayConfig, ok := r.Value.(map[string]any); ok { s.applyConfig(trayConfig) } } - s.Core().RegisterTask(s.handleTask) - return nil + s.Core().Action("systray.setIcon", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskSetTrayIcon) + return core.Result{Value: nil, OK: true}.New(s.manager.SetIcon(t.Data)) + }) + s.Core().Action("systray.setMenu", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskSetTrayMenu) + return core.Result{Value: nil, OK: true}.New(s.taskSetTrayMenu(t)) + }) + s.Core().Action("systray.showPanel", func(_ context.Context, _ core.Options) core.Result { + // Panel show — deferred (requires WindowHandle integration) + return core.Result{OK: true} + }) + s.Core().Action("systray.hidePanel", func(_ context.Context, _ core.Options) core.Result { + // Panel hide — deferred (requires WindowHandle integration) + return core.Result{OK: true} + }) + return core.Result{OK: true} } func (s *Service) applyConfig(configData map[string]any) { @@ -40,25 +55,8 @@ func (s *Service) applyConfig(configData map[string]any) { } } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil -} - -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskSetTrayIcon: - return nil, true, s.manager.SetIcon(t.Data) - case TaskSetTrayMenu: - return nil, true, s.taskSetTrayMenu(t) - case TaskShowPanel: - // Panel show — deferred (requires WindowHandle integration) - return nil, true, nil - case TaskHidePanel: - // Panel hide — deferred (requires WindowHandle integration) - return nil, true, nil - default: - return nil, false, nil - } +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } func (s *Service) taskSetTrayMenu(t TaskSetTrayMenu) error { diff --git a/pkg/systray/service_test.go b/pkg/systray/service_test.go index 4bcec30..f533999 100644 --- a/pkg/systray/service_test.go +++ b/pkg/systray/service_test.go @@ -4,23 +4,28 @@ import ( "context" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newTestSystrayService(t *testing.T) (*Service, *core.Core) { t.Helper() - c, err := core.New( + c := core.New( core.WithService(Register(newMockPlatform())), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "systray") return svc, c } +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + func TestRegister_Good(t *testing.T) { svc, _ := newTestSystrayService(t) assert.NotNil(t, svc) @@ -34,9 +39,8 @@ func TestTaskSetTrayIcon_Good(t *testing.T) { require.NoError(t, svc.manager.Setup("Test", "Test")) icon := []byte{0x89, 0x50, 0x4E, 0x47} // PNG header - _, handled, err := c.PERFORM(TaskSetTrayIcon{Data: icon}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "systray.setIcon", TaskSetTrayIcon{Data: icon}) + require.True(t, r.OK) } func TestTaskSetTrayMenu_Good(t *testing.T) { @@ -49,15 +53,13 @@ func TestTaskSetTrayMenu_Good(t *testing.T) { {Type: "separator"}, {Label: "Quit", ActionID: "quit"}, } - _, handled, err := c.PERFORM(TaskSetTrayMenu{Items: items}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "systray.setMenu", TaskSetTrayMenu{Items: items}) + require.True(t, r.OK) } func TestTaskSetTrayIcon_Bad(t *testing.T) { - // No systray service — PERFORM returns handled=false - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.PERFORM(TaskSetTrayIcon{Data: nil}) - assert.False(t, handled) + // No systray service — action is not registered + c := core.New(core.WithServiceLock()) + r := c.Action("systray.setIcon").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } diff --git a/pkg/systray/tray.go b/pkg/systray/tray.go index 8d2e108..231fb8f 100644 --- a/pkg/systray/tray.go +++ b/pkg/systray/tray.go @@ -5,7 +5,7 @@ import ( _ "embed" "sync" - coreerr "forge.lthn.ai/core/go-log" + coreerr "dappco.re/go/core/log" ) //go:embed assets/apptray.png diff --git a/pkg/webview/service.go b/pkg/webview/service.go index 7d0c46d..8d69a52 100644 --- a/pkg/webview/service.go +++ b/pkg/webview/service.go @@ -10,7 +10,7 @@ import ( "time" gowebview "forge.lthn.ai/core/go-webview" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/window" ) @@ -58,7 +58,7 @@ type Service struct { // Register binds the webview service to a Core instance. // core.WithService(webview.Register()) // core.WithService(webview.Register(func(o *Options) { o.DebugURL = "http://localhost:9223" })) -func Register(optionFns ...func(*Options)) func(*core.Core) (any, error) { +func Register(optionFns ...func(*Options)) func(*core.Core) core.Result { o := Options{ DebugURL: "http://localhost:9222", Timeout: 30 * time.Second, @@ -67,7 +67,7 @@ func Register(optionFns ...func(*Options)) func(*core.Core) (any, error) { for _, fn := range optionFns { fn(&o) } - return func(c *core.Core) (any, error) { + return func(c *core.Core) core.Result { svc := &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, o), options: o, @@ -75,7 +75,7 @@ func Register(optionFns ...func(*Options)) func(*core.Core) (any, error) { newConn: defaultNewConn(o), } svc.watcherSetup = svc.defaultWatcherSetup - return svc, nil + return core.Result{Value: svc, OK: true} } } @@ -157,25 +157,25 @@ func (s *Service) defaultWatcherSetup(conn connector, windowName string) { }) } -func (s *Service) OnStartup(_ context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) - s.Core().RegisterTask(s.handleTask) - return nil + s.registerTaskActions() + return core.Result{OK: true} } // OnShutdown closes all CDP connections. -func (s *Service) OnShutdown(_ context.Context) error { +func (s *Service) OnShutdown(_ context.Context) core.Result { s.mu.Lock() defer s.mu.Unlock() for name, conn := range s.connections { conn.Close() delete(s.connections, name) } - return nil + return core.Result{OK: true} } // HandleIPCEvents listens for window close events to clean up connections. -func (s *Service) HandleIPCEvents(_ *core.Core, msg core.Message) error { +func (s *Service) HandleIPCEvents(_ *core.Core, msg core.Message) core.Result { switch m := msg.(type) { case window.ActionWindowClosed: s.mu.Lock() @@ -185,7 +185,7 @@ func (s *Service) HandleIPCEvents(_ *core.Core, msg core.Message) error { } s.mu.Unlock() } - return nil + return core.Result{OK: true} } // getConn returns the connector for a window, creating it if needed. @@ -214,26 +214,26 @@ func (s *Service) getConn(windowName string) (connector, error) { return conn, nil } -func (s *Service) handleQuery(_ *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q := q.(type) { case QueryURL: conn, err := s.getConn(q.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } url, err := conn.GetURL() - return url, true, err + return core.Result{}.New(url, err) case QueryTitle: conn, err := s.getConn(q.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } title, err := conn.GetTitle() - return title, true, err + return core.Result{}.New(title, err) case QueryConsole: conn, err := s.getConn(q.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } msgs := conn.GetConsole() // Filter by level if specified @@ -250,159 +250,187 @@ func (s *Service) handleQuery(_ *core.Core, q core.Query) (any, bool, error) { if q.Limit > 0 && len(msgs) > q.Limit { msgs = msgs[len(msgs)-q.Limit:] } - return msgs, true, nil + return core.Result{Value: msgs, OK: true} case QuerySelector: conn, err := s.getConn(q.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } el, err := conn.QuerySelector(q.Selector) - return el, true, err + return core.Result{}.New(el, err) case QuerySelectorAll: conn, err := s.getConn(q.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } els, err := conn.QuerySelectorAll(q.Selector) - return els, true, err + return core.Result{}.New(els, err) case QueryDOMTree: conn, err := s.getConn(q.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } selector := q.Selector if selector == "" { selector = "html" } html, err := conn.GetHTML(selector) - return html, true, err + return core.Result{}.New(html, err) case QueryZoom: conn, err := s.getConn(q.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } zoom, err := conn.GetZoom() - return zoom, true, err + return core.Result{}.New(zoom, err) default: - return nil, false, nil + return core.Result{} } } -func (s *Service) handleTask(_ *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskEvaluate: +// registerTaskActions registers all webview task handlers as named Core actions. +func (s *Service) registerTaskActions() { + c := s.Core() + c.Action("webview.evaluate", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskEvaluate) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } result, err := conn.Evaluate(t.Script) - return result, true, err - case TaskClick: + return core.Result{}.New(result, err) + }) + c.Action("webview.click", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskClick) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, conn.Click(t.Selector) - case TaskType: + return core.Result{Value: nil, OK: true}.New(conn.Click(t.Selector)) + }) + c.Action("webview.type", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskType) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, conn.Type(t.Selector, t.Text) - case TaskNavigate: + return core.Result{Value: nil, OK: true}.New(conn.Type(t.Selector, t.Text)) + }) + c.Action("webview.navigate", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskNavigate) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, conn.Navigate(t.URL) - case TaskScreenshot: + return core.Result{Value: nil, OK: true}.New(conn.Navigate(t.URL)) + }) + c.Action("webview.screenshot", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskScreenshot) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } png, err := conn.Screenshot() if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return ScreenshotResult{ + return core.Result{Value: ScreenshotResult{ Base64: base64.StdEncoding.EncodeToString(png), MimeType: "image/png", - }, true, nil - case TaskScroll: + }, OK: true} + }) + c.Action("webview.scroll", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskScroll) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } _, err = conn.Evaluate("window.scrollTo(" + strconv.Itoa(t.X) + "," + strconv.Itoa(t.Y) + ")") - return nil, true, err - case TaskHover: + return core.Result{Value: nil, OK: true}.New(err) + }) + c.Action("webview.hover", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskHover) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, conn.Hover(t.Selector) - case TaskSelect: + return core.Result{Value: nil, OK: true}.New(conn.Hover(t.Selector)) + }) + c.Action("webview.select", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskSelect) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, conn.Select(t.Selector, t.Value) - case TaskCheck: + return core.Result{Value: nil, OK: true}.New(conn.Select(t.Selector, t.Value)) + }) + c.Action("webview.check", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskCheck) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, conn.Check(t.Selector, t.Checked) - case TaskUploadFile: + return core.Result{Value: nil, OK: true}.New(conn.Check(t.Selector, t.Checked)) + }) + c.Action("webview.uploadFile", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskUploadFile) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, conn.UploadFile(t.Selector, t.Paths) - case TaskSetViewport: + return core.Result{Value: nil, OK: true}.New(conn.UploadFile(t.Selector, t.Paths)) + }) + c.Action("webview.setViewport", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskSetViewport) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, conn.SetViewport(t.Width, t.Height) - case TaskClearConsole: + return core.Result{Value: nil, OK: true}.New(conn.SetViewport(t.Width, t.Height)) + }) + c.Action("webview.clearConsole", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskClearConsole) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } conn.ClearConsole() - return nil, true, nil - case TaskSetURL: + return core.Result{OK: true} + }) + c.Action("webview.setURL", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskSetURL) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, conn.Navigate(t.URL) - case TaskSetZoom: + return core.Result{Value: nil, OK: true}.New(conn.Navigate(t.URL)) + }) + c.Action("webview.setZoom", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskSetZoom) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } - return nil, true, conn.SetZoom(t.Zoom) - case TaskPrint: + return core.Result{Value: nil, OK: true}.New(conn.SetZoom(t.Zoom)) + }) + c.Action("webview.print", func(_ context.Context, opts core.Options) core.Result { + t, _ := opts.Get("task").Value.(TaskPrint) conn, err := s.getConn(t.Window) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } pdfBytes, err := conn.Print(t.ToPDF) if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } if !t.ToPDF { - return nil, true, nil + return core.Result{OK: true} } - return PrintResult{ + return core.Result{Value: PrintResult{ Base64: base64.StdEncoding.EncodeToString(pdfBytes), MimeType: "application/pdf", - }, true, nil - default: - return nil, false, nil - } + }, OK: true} + }) } // realConnector wraps *gowebview.Webview, converting types at the boundary. diff --git a/pkg/webview/service_test.go b/pkg/webview/service_test.go index 17d142c..56cc043 100644 --- a/pkg/webview/service_test.go +++ b/pkg/webview/service_test.go @@ -5,7 +5,7 @@ import ( "context" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/window" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -95,15 +95,20 @@ func (m *mockConnector) Print(toPDF bool) ([]byte, error) { func newTestService(t *testing.T, mock *mockConnector) (*Service, *core.Core) { t.Helper() factory := Register() - c, err := core.New(core.WithService(factory), core.WithServiceLock()) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + c := core.New(core.WithService(factory), core.WithServiceLock()) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "webview") // Inject mock connector svc.newConn = func(_, _ string) (connector, error) { return mock, nil } return svc, c } +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + func TestRegister_Good(t *testing.T) { svc, _ := newTestService(t, &mockConnector{}) assert.NotNil(t, svc) @@ -111,18 +116,16 @@ func TestRegister_Good(t *testing.T) { func TestQueryURL_Good(t *testing.T) { _, c := newTestService(t, &mockConnector{url: "https://example.com"}) - result, handled, err := c.QUERY(QueryURL{Window: "main"}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "https://example.com", result) + r := c.QUERY(QueryURL{Window: "main"}) + require.True(t, r.OK) + assert.Equal(t, "https://example.com", r.Value) } func TestQueryTitle_Good(t *testing.T) { _, c := newTestService(t, &mockConnector{title: "Test Page"}) - result, handled, err := c.QUERY(QueryTitle{Window: "main"}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "Test Page", result) + r := c.QUERY(QueryTitle{Window: "main"}) + require.True(t, r.OK) + assert.Equal(t, "Test Page", r.Value) } func TestQueryConsole_Good(t *testing.T) { @@ -132,10 +135,9 @@ func TestQueryConsole_Good(t *testing.T) { {Type: "log", Text: "world"}, }} _, c := newTestService(t, mock) - result, handled, err := c.QUERY(QueryConsole{Window: "main", Level: "error", Limit: 10}) - require.NoError(t, err) - assert.True(t, handled) - msgs, _ := result.([]ConsoleMessage) + r := c.QUERY(QueryConsole{Window: "main", Level: "error", Limit: 10}) + require.True(t, r.OK) + msgs, _ := r.Value.([]ConsoleMessage) assert.Len(t, msgs, 1) assert.Equal(t, "oops", msgs[0].Text) } @@ -147,45 +149,41 @@ func TestQueryConsole_Good_Limit(t *testing.T) { {Type: "log", Text: "c"}, }} _, c := newTestService(t, mock) - result, _, _ := c.QUERY(QueryConsole{Window: "main", Limit: 2}) - msgs, _ := result.([]ConsoleMessage) + r := c.QUERY(QueryConsole{Window: "main", Limit: 2}) + msgs, _ := r.Value.([]ConsoleMessage) assert.Len(t, msgs, 2) assert.Equal(t, "b", msgs[0].Text) // last 2 } func TestTaskEvaluate_Good(t *testing.T) { _, c := newTestService(t, &mockConnector{evalResult: 42}) - result, handled, err := c.PERFORM(TaskEvaluate{Window: "main", Script: "21*2"}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, 42, result) + r := taskRun(c, "webview.evaluate", TaskEvaluate{Window: "main", Script: "21*2"}) + require.True(t, r.OK) + assert.Equal(t, 42, r.Value) } func TestTaskClick_Good(t *testing.T) { mock := &mockConnector{} _, c := newTestService(t, mock) - _, handled, err := c.PERFORM(TaskClick{Window: "main", Selector: "#btn"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "webview.click", TaskClick{Window: "main", Selector: "#btn"}) + require.True(t, r.OK) assert.Equal(t, "#btn", mock.lastClickSel) } func TestTaskNavigate_Good(t *testing.T) { mock := &mockConnector{} _, c := newTestService(t, mock) - _, handled, err := c.PERFORM(TaskNavigate{Window: "main", URL: "https://example.com"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "webview.navigate", TaskNavigate{Window: "main", URL: "https://example.com"}) + require.True(t, r.OK) assert.Equal(t, "https://example.com", mock.lastNavURL) } func TestTaskScreenshot_Good(t *testing.T) { mock := &mockConnector{screenshot: []byte{0x89, 0x50, 0x4E, 0x47}} _, c := newTestService(t, mock) - result, handled, err := c.PERFORM(TaskScreenshot{Window: "main"}) - require.NoError(t, err) - assert.True(t, handled) - sr, ok := result.(ScreenshotResult) + r := taskRun(c, "webview.screenshot", TaskScreenshot{Window: "main"}) + require.True(t, r.OK) + sr, ok := r.Value.(ScreenshotResult) assert.True(t, ok) assert.Equal(t, "image/png", sr.MimeType) assert.NotEmpty(t, sr.Base64) @@ -194,9 +192,8 @@ func TestTaskScreenshot_Good(t *testing.T) { func TestTaskClearConsole_Good(t *testing.T) { mock := &mockConnector{} _, c := newTestService(t, mock) - _, handled, err := c.PERFORM(TaskClearConsole{Window: "main"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "webview.clearConsole", TaskClearConsole{Window: "main"}) + require.True(t, r.OK) assert.True(t, mock.consoleClearCalled) } @@ -204,7 +201,7 @@ func TestConnectionCleanup_Good(t *testing.T) { mock := &mockConnector{} _, c := newTestService(t, mock) // Access creates connection - _, _, _ = c.QUERY(QueryURL{Window: "main"}) + c.QUERY(QueryURL{Window: "main"}) assert.False(t, mock.closed) // Window close action triggers cleanup _ = c.ACTION(window.ActionWindowClosed{Name: "main"}) @@ -212,9 +209,9 @@ func TestConnectionCleanup_Good(t *testing.T) { } func TestQueryURL_Bad_NoService(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.QUERY(QueryURL{Window: "main"}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(QueryURL{Window: "main"}) + assert.False(t, r.OK) } // --- SetURL --- @@ -222,9 +219,8 @@ func TestQueryURL_Bad_NoService(t *testing.T) { func TestTaskSetURL_Good(t *testing.T) { mock := &mockConnector{} _, c := newTestService(t, mock) - _, handled, err := c.PERFORM(TaskSetURL{Window: "main", URL: "https://example.com/page"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "webview.setURL", TaskSetURL{Window: "main", URL: "https://example.com/page"}) + require.True(t, r.OK) assert.Equal(t, "https://example.com/page", mock.lastNavURL) } @@ -235,16 +231,15 @@ func TestTaskSetURL_Bad_UnknownWindow(t *testing.T) { svc.newConn = func(_, _ string) (connector, error) { return nil, core.E("test", "no connection", nil) } - _, _, err := c.PERFORM(TaskSetURL{Window: "bad", URL: "https://example.com"}) - assert.Error(t, err) + r := taskRun(c, "webview.setURL", TaskSetURL{Window: "bad", URL: "https://example.com"}) + assert.False(t, r.OK) } func TestTaskSetURL_Ugly_EmptyURL(t *testing.T) { mock := &mockConnector{} _, c := newTestService(t, mock) - _, handled, err := c.PERFORM(TaskSetURL{Window: "main", URL: ""}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "webview.setURL", TaskSetURL{Window: "main", URL: ""}) + require.True(t, r.OK) assert.Equal(t, "", mock.lastNavURL) } @@ -253,57 +248,53 @@ func TestTaskSetURL_Ugly_EmptyURL(t *testing.T) { func TestQueryZoom_Good(t *testing.T) { mock := &mockConnector{zoom: 1.5} _, c := newTestService(t, mock) - result, handled, err := c.QUERY(QueryZoom{Window: "main"}) - require.NoError(t, err) - assert.True(t, handled) - assert.InDelta(t, 1.5, result.(float64), 0.001) + r := c.QUERY(QueryZoom{Window: "main"}) + require.True(t, r.OK) + assert.InDelta(t, 1.5, r.Value.(float64), 0.001) } func TestQueryZoom_Good_DefaultsToOne(t *testing.T) { mock := &mockConnector{} // zoom not set → GetZoom returns 1.0 _, c := newTestService(t, mock) - result, handled, err := c.QUERY(QueryZoom{Window: "main"}) - require.NoError(t, err) - assert.True(t, handled) - assert.InDelta(t, 1.0, result.(float64), 0.001) + r := c.QUERY(QueryZoom{Window: "main"}) + require.True(t, r.OK) + assert.InDelta(t, 1.0, r.Value.(float64), 0.001) } func TestQueryZoom_Bad_NoService(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.QUERY(QueryZoom{Window: "main"}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.QUERY(QueryZoom{Window: "main"}) + assert.False(t, r.OK) } func TestTaskSetZoom_Good(t *testing.T) { mock := &mockConnector{} _, c := newTestService(t, mock) - _, handled, err := c.PERFORM(TaskSetZoom{Window: "main", Zoom: 2.0}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "webview.setZoom", TaskSetZoom{Window: "main", Zoom: 2.0}) + require.True(t, r.OK) assert.InDelta(t, 2.0, mock.lastZoomSet, 0.001) } func TestTaskSetZoom_Good_Reset(t *testing.T) { mock := &mockConnector{zoom: 1.5} _, c := newTestService(t, mock) - _, _, err := c.PERFORM(TaskSetZoom{Window: "main", Zoom: 1.0}) - require.NoError(t, err) + r := taskRun(c, "webview.setZoom", TaskSetZoom{Window: "main", Zoom: 1.0}) + require.True(t, r.OK) assert.InDelta(t, 1.0, mock.zoom, 0.001) } func TestTaskSetZoom_Bad_NoService(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskSetZoom{Window: "main", Zoom: 1.5}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("webview.setZoom").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestTaskSetZoom_Ugly_ZeroZoom(t *testing.T) { mock := &mockConnector{} _, c := newTestService(t, mock) // Zero zoom is technically valid input; the connector accepts it. - _, handled, err := c.PERFORM(TaskSetZoom{Window: "main", Zoom: 0}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "webview.setZoom", TaskSetZoom{Window: "main", Zoom: 0}) + require.True(t, r.OK) assert.InDelta(t, 0.0, mock.lastZoomSet, 0.001) } @@ -312,10 +303,9 @@ func TestTaskSetZoom_Ugly_ZeroZoom(t *testing.T) { func TestTaskPrint_Good_Dialog(t *testing.T) { mock := &mockConnector{} _, c := newTestService(t, mock) - result, handled, err := c.PERFORM(TaskPrint{Window: "main", ToPDF: false}) - require.NoError(t, err) - assert.True(t, handled) - assert.Nil(t, result) + r := taskRun(c, "webview.print", TaskPrint{Window: "main", ToPDF: false}) + require.True(t, r.OK) + assert.Nil(t, r.Value) assert.True(t, mock.printCalled) assert.False(t, mock.printToPDF) } @@ -324,10 +314,9 @@ func TestTaskPrint_Good_PDF(t *testing.T) { pdfHeader := []byte{0x25, 0x50, 0x44, 0x46} // %PDF mock := &mockConnector{printPDFBytes: pdfHeader} _, c := newTestService(t, mock) - result, handled, err := c.PERFORM(TaskPrint{Window: "main", ToPDF: true}) - require.NoError(t, err) - assert.True(t, handled) - pr, ok := result.(PrintResult) + r := taskRun(c, "webview.print", TaskPrint{Window: "main", ToPDF: true}) + require.True(t, r.OK) + pr, ok := r.Value.(PrintResult) require.True(t, ok) assert.Equal(t, "application/pdf", pr.MimeType) assert.NotEmpty(t, pr.Base64) @@ -335,26 +324,25 @@ func TestTaskPrint_Good_PDF(t *testing.T) { } func TestTaskPrint_Bad_NoService(t *testing.T) { - c, _ := core.New(core.WithServiceLock()) - _, handled, _ := c.PERFORM(TaskPrint{Window: "main"}) - assert.False(t, handled) + c := core.New(core.WithServiceLock()) + r := c.Action("webview.print").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestTaskPrint_Bad_Error(t *testing.T) { mock := &mockConnector{printErr: core.E("test", "print failed", nil)} _, c := newTestService(t, mock) - _, _, err := c.PERFORM(TaskPrint{Window: "main", ToPDF: true}) - assert.Error(t, err) + r := taskRun(c, "webview.print", TaskPrint{Window: "main", ToPDF: true}) + assert.False(t, r.OK) } func TestTaskPrint_Ugly_EmptyPDF(t *testing.T) { // toPDF=true but connector returns zero bytes — should still wrap as PrintResult mock := &mockConnector{printPDFBytes: []byte{}} _, c := newTestService(t, mock) - result, handled, err := c.PERFORM(TaskPrint{Window: "main", ToPDF: true}) - require.NoError(t, err) - assert.True(t, handled) - pr, ok := result.(PrintResult) + r := taskRun(c, "webview.print", TaskPrint{Window: "main", ToPDF: true}) + require.True(t, r.OK) + pr, ok := r.Value.(PrintResult) require.True(t, ok) assert.Equal(t, "application/pdf", pr.MimeType) assert.Equal(t, "", pr.Base64) // empty PDF encodes to empty base64 diff --git a/pkg/window/layout.go b/pkg/window/layout.go index 7021a72..9bdfc2b 100644 --- a/pkg/window/layout.go +++ b/pkg/window/layout.go @@ -8,8 +8,8 @@ import ( "sync" "time" - coreio "forge.lthn.ai/core/go-io" - coreerr "forge.lthn.ai/core/go-log" + coreio "dappco.re/go/core/io" + coreerr "dappco.re/go/core/log" ) // Layout is a named window arrangement. diff --git a/pkg/window/register.go b/pkg/window/register.go index 8adb088..8b8c991 100644 --- a/pkg/window/register.go +++ b/pkg/window/register.go @@ -1,15 +1,15 @@ package window -import "forge.lthn.ai/core/go/pkg/core" +import core "dappco.re/go/core" // Register(p) binds the window service to a Core instance. // core.WithService(window.Register(window.NewWailsPlatform(app))) -func Register(p Platform) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - return &Service{ +func Register(p Platform) func(*core.Core) core.Result { + return func(c *core.Core) core.Result { + return core.Result{Value: &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), platform: p, manager: NewManager(p), - }, nil + }, OK: true} } } diff --git a/pkg/window/service.go b/pkg/window/service.go index af847a4..e46cc90 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -3,8 +3,8 @@ package window import ( "context" - coreerr "forge.lthn.ai/core/go-log" - "forge.lthn.ai/core/go/pkg/core" + coreerr "dappco.re/go/core/log" + core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/screen" ) @@ -16,19 +16,19 @@ type Service struct { platform Platform } -func (s *Service) OnStartup(ctx context.Context) error { +func (s *Service) OnStartup(_ context.Context) core.Result { // Query config — display registers its handler before us (registration order guarantee). - // If display is not registered, handled=false and we skip config. - configValue, handled, _ := s.Core().QUERY(QueryConfig{}) - if handled { - if windowConfig, ok := configValue.(map[string]any); ok { + // 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.Core().RegisterTask(s.handleTask) - return nil + s.registerTaskActions() + return core.Result{OK: true} } func (s *Service) applyConfig(configData map[string]any) { @@ -49,30 +49,30 @@ func (s *Service) applyConfig(configData map[string]any) { } } -func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - return nil +func (s *Service) HandleIPCEvents(_ *core.Core, _ core.Message) core.Result { + return core.Result{OK: true} } -func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { +func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { switch q := q.(type) { case QueryWindowList: - return s.queryWindowList(), true, nil + return core.Result{Value: s.queryWindowList(), OK: true} case QueryWindowByName: - return s.queryWindowByName(q.Name), true, nil + return core.Result{Value: s.queryWindowByName(q.Name), OK: true} case QueryLayoutList: - return s.manager.Layout().ListLayouts(), true, nil + return core.Result{Value: s.manager.Layout().ListLayouts(), OK: true} case QueryLayoutGet: l, ok := s.manager.Layout().GetLayout(q.Name) if !ok { - return (*Layout)(nil), true, nil + return core.Result{Value: (*Layout)(nil), OK: true} } - return &l, true, nil + return core.Result{Value: &l, OK: true} case QueryWindowZoom: return s.queryWindowZoom(q.Name) case QueryWindowBounds: return s.queryWindowBounds(q.Name) default: - return nil, false, nil + return core.Result{} } } @@ -107,80 +107,144 @@ func (s *Service) queryWindowByName(name string) *WindowInfo { } } -// --- Task Handlers --- +// --- Action Registration --- -func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { - switch t := t.(type) { - case TaskOpenWindow: +// 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) - 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.Width, t.Height) - 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) - case TaskRestore: - return nil, true, s.taskRestore(t.Name) - case TaskSetTitle: - return nil, true, s.taskSetTitle(t.Name, t.Title) - case TaskSetAlwaysOnTop: - return nil, true, s.taskSetAlwaysOnTop(t.Name, t.AlwaysOnTop) - case TaskSetBackgroundColour: - return nil, true, s.taskSetBackgroundColour(t.Name, t.Red, t.Green, t.Blue, t.Alpha) - case TaskSetVisibility: - return nil, true, s.taskSetVisibility(t.Name, t.Visible) - case TaskFullscreen: - return nil, true, s.taskFullscreen(t.Name, t.Fullscreen) - case TaskSaveLayout: - return nil, true, s.taskSaveLayout(t.Name) - case TaskRestoreLayout: - return nil, true, s.taskRestoreLayout(t.Name) - case TaskDeleteLayout: + }) + 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 nil, true, nil - case TaskTileWindows: - return nil, true, s.taskTileWindows(t.Mode, t.Windows) - case TaskStackWindows: - return nil, true, s.taskStackWindows(t.Windows, t.OffsetX, t.OffsetY) - case TaskSnapWindow: - return nil, true, s.taskSnapWindow(t.Name, t.Position) - case TaskApplyWorkflow: - return nil, true, s.taskApplyWorkflow(t.Workflow, t.Windows) - case TaskSetZoom: - return nil, true, s.taskSetZoom(t.Name, t.Magnification) - case TaskZoomIn: - return nil, true, s.taskZoomIn(t.Name) - case TaskZoomOut: - return nil, true, s.taskZoomOut(t.Name) - case TaskZoomReset: - return nil, true, s.taskZoomReset(t.Name) - case TaskSetURL: - return nil, true, s.taskSetURL(t.Name, t.URL) - case TaskSetHTML: - return nil, true, s.taskSetHTML(t.Name, t.HTML) - case TaskExecJS: - return nil, true, s.taskExecJS(t.Name, t.JS) - case TaskToggleFullscreen: - return nil, true, s.taskToggleFullscreen(t.Name) - case TaskToggleMaximise: - return nil, true, s.taskToggleMaximise(t.Name) - case TaskSetBounds: - return nil, true, s.taskSetBounds(t.Name, t.X, t.Y, t.Width, t.Height) - case TaskSetContentProtection: - return nil, true, s.taskSetContentProtection(t.Name, t.Protection) - case TaskFlash: - return nil, true, s.taskFlash(t.Name, t.Enabled) - case TaskPrint: - return nil, true, s.taskPrint(t.Name) - default: - return nil, false, nil - } + 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) { @@ -189,12 +253,12 @@ func (s *Service) primaryScreenArea() (int, int, int, int) { const fallbackWidth = 1920 const fallbackHeight = 1080 - result, handled, err := s.Core().QUERY(screen.QueryPrimary{}) - if err != nil || !handled { + r := s.Core().QUERY(screen.QueryPrimary{}) + if !r.OK { return fallbackX, fallbackY, fallbackWidth, fallbackHeight } - primary, ok := result.(*screen.Screen) + primary, ok := r.Value.(*screen.Screen) if !ok || primary == nil { return fallbackX, fallbackY, fallbackWidth, fallbackHeight } @@ -216,7 +280,7 @@ func (s *Service) primaryScreenArea() (int, int, int, int) { return x, y, width, height } -func (s *Service) taskOpenWindow(t TaskOpenWindow) (any, bool, error) { +func (s *Service) taskOpenWindow(t TaskOpenWindow) core.Result { var ( pw PlatformWindow err error @@ -227,7 +291,7 @@ func (s *Service) taskOpenWindow(t TaskOpenWindow) (any, bool, error) { pw, err = s.manager.Open(t.Options...) } if err != nil { - return nil, true, err + return core.Result{Value: err, OK: false} } x, y := pw.Position() w, h := pw.Size() @@ -238,7 +302,7 @@ func (s *Service) taskOpenWindow(t TaskOpenWindow) (any, bool, error) { // Broadcast to all listeners _ = s.Core().ACTION(ActionWindowOpened{Name: pw.Name()}) - return info, true, nil + return core.Result{Value: info, OK: true} } // trackWindow attaches platform event listeners that emit IPC actions. @@ -494,12 +558,12 @@ func (s *Service) taskApplyWorkflow(workflow string, names []string) error { // --- Zoom --- -func (s *Service) queryWindowZoom(name string) (any, bool, error) { +func (s *Service) queryWindowZoom(name string) core.Result { pw, ok := s.manager.Get(name) if !ok { - return nil, true, coreerr.E("window.queryWindowZoom", "window not found: "+name, nil) + return core.Result{Value: coreerr.E("window.queryWindowZoom", "window not found: "+name, nil), OK: false} } - return pw.GetZoom(), true, nil + return core.Result{Value: pw.GetZoom(), OK: true} } func (s *Service) taskSetZoom(name string, magnification float64) error { @@ -595,13 +659,13 @@ func (s *Service) taskToggleMaximise(name string) error { // --- Bounds --- -func (s *Service) queryWindowBounds(name string) (any, bool, error) { +func (s *Service) queryWindowBounds(name string) core.Result { pw, ok := s.manager.Get(name) if !ok { - return nil, true, coreerr.E("window.queryWindowBounds", "window not found: "+name, nil) + return core.Result{Value: coreerr.E("window.queryWindowBounds", "window not found: "+name, nil), OK: false} } x, y, width, height := pw.GetBounds() - return WindowBounds{X: x, Y: y, Width: width, Height: height}, true, nil + 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 { diff --git a/pkg/window/service_screen_test.go b/pkg/window/service_screen_test.go index 720536b..583735d 100644 --- a/pkg/window/service_screen_test.go +++ b/pkg/window/service_screen_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "forge.lthn.ai/core/gui/pkg/screen" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -32,18 +32,18 @@ func (m *mockScreenPlatform) GetCurrent() *screen.Screen { func newTestWindowServiceWithScreen(t *testing.T, screens []screen.Screen) (*Service, *core.Core) { t.Helper() - c, err := core.New( + c := core.New( core.WithService(screen.Register(&mockScreenPlatform{screens: screens})), core.WithService(Register(newMockPlatform())), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "window") return svc, c } + func TestTaskTileWindows_Good_UsesPrimaryScreenSize(t *testing.T) { _, c := newTestWindowServiceWithScreen(t, []screen.Screen{ { @@ -53,25 +53,22 @@ func TestTaskTileWindows_Good_UsesPrimaryScreenSize(t *testing.T) { }, }) - _, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("left"), WithSize(400, 400)}}) - require.NoError(t, err) - _, _, err = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("right"), WithSize(400, 400)}}) - require.NoError(t, err) + require.True(t, taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("left"), WithSize(400, 400)}}).OK) + require.True(t, taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("right"), WithSize(400, 400)}}).OK) - _, handled, err := c.PERFORM(TaskTileWindows{Mode: "left-right", Windows: []string{"left", "right"}}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.tileWindows", TaskTileWindows{Mode: "left-right", Windows: []string{"left", "right"}}) + require.True(t, r.OK) - result, _, err := c.QUERY(QueryWindowByName{Name: "left"}) - require.NoError(t, err) - left := result.(*WindowInfo) + r2 := c.QUERY(QueryWindowByName{Name: "left"}) + require.True(t, r2.OK) + left := r2.Value.(*WindowInfo) assert.Equal(t, 0, left.X) assert.Equal(t, 1000, left.Width) assert.Equal(t, 1000, left.Height) - result, _, err = c.QUERY(QueryWindowByName{Name: "right"}) - require.NoError(t, err) - right := result.(*WindowInfo) + r3 := c.QUERY(QueryWindowByName{Name: "right"}) + require.True(t, r3.OK) + right := r3.Value.(*WindowInfo) assert.Equal(t, 1000, right.X) assert.Equal(t, 1000, right.Width) assert.Equal(t, 1000, right.Height) @@ -86,16 +83,14 @@ func TestTaskSnapWindow_Good_UsesPrimaryScreenSize(t *testing.T) { }, }) - _, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("snap"), WithSize(400, 300)}}) - require.NoError(t, err) + require.True(t, taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("snap"), WithSize(400, 300)}}).OK) - _, handled, err := c.PERFORM(TaskSnapWindow{Name: "snap", Position: "left"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.snapWindow", TaskSnapWindow{Name: "snap", Position: "left"}) + require.True(t, r.OK) - result, _, err := c.QUERY(QueryWindowByName{Name: "snap"}) - require.NoError(t, err) - info := result.(*WindowInfo) + r2 := c.QUERY(QueryWindowByName{Name: "snap"}) + require.True(t, r2.OK) + info := r2.Value.(*WindowInfo) assert.Equal(t, 0, info.X) assert.Equal(t, 0, info.Y) assert.Equal(t, 1000, info.Width) @@ -111,26 +106,23 @@ func TestTaskTileWindows_Good_UsesPrimaryWorkAreaOrigin(t *testing.T) { }, }) - _, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("left"), WithSize(400, 400)}}) - require.NoError(t, err) - _, _, err = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("right"), WithSize(400, 400)}}) - require.NoError(t, err) + require.True(t, taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("left"), WithSize(400, 400)}}).OK) + require.True(t, taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("right"), WithSize(400, 400)}}).OK) - _, handled, err := c.PERFORM(TaskTileWindows{Mode: "left-right", Windows: []string{"left", "right"}}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.tileWindows", TaskTileWindows{Mode: "left-right", Windows: []string{"left", "right"}}) + require.True(t, r.OK) - result, _, err := c.QUERY(QueryWindowByName{Name: "left"}) - require.NoError(t, err) - left := result.(*WindowInfo) + r2 := c.QUERY(QueryWindowByName{Name: "left"}) + require.True(t, r2.OK) + left := r2.Value.(*WindowInfo) assert.Equal(t, 100, left.X) assert.Equal(t, 50, left.Y) assert.Equal(t, 1000, left.Width) assert.Equal(t, 1000, left.Height) - result, _, err = c.QUERY(QueryWindowByName{Name: "right"}) - require.NoError(t, err) - right := result.(*WindowInfo) + r3 := c.QUERY(QueryWindowByName{Name: "right"}) + require.True(t, r3.OK) + right := r3.Value.(*WindowInfo) assert.Equal(t, 1100, right.X) assert.Equal(t, 50, right.Y) assert.Equal(t, 1000, right.Width) diff --git a/pkg/window/service_test.go b/pkg/window/service_test.go index 5b8f4ba..a73b08d 100644 --- a/pkg/window/service_test.go +++ b/pkg/window/service_test.go @@ -5,23 +5,28 @@ import ( "sync" "testing" - "forge.lthn.ai/core/go/pkg/core" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newTestWindowService(t *testing.T) (*Service, *core.Core) { t.Helper() - c, err := core.New( + c := core.New( core.WithService(Register(newMockPlatform())), core.WithServiceLock(), ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) svc := core.MustServiceFor[*Service](c, "window") return svc, c } +func taskRun(c *core.Core, name string, task any) core.Result { + return c.Action(name).Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: task}, + )) +} + func TestRegister_Good(t *testing.T) { svc, _ := newTestWindowService(t) assert.NotNil(t, svc) @@ -30,123 +35,112 @@ func TestRegister_Good(t *testing.T) { func TestTaskOpenWindow_Good(t *testing.T) { _, c := newTestWindowService(t) - result, handled, err := c.PERFORM(TaskOpenWindow{ + r := taskRun(c, "window.open", TaskOpenWindow{ Window: &Window{Name: "test", URL: "/"}, }) - require.NoError(t, err) - assert.True(t, handled) - info := result.(WindowInfo) + require.True(t, r.OK) + info := r.Value.(WindowInfo) assert.Equal(t, "test", info.Name) } func TestTaskOpenWindow_OptionsFallback_Good(t *testing.T) { _, c := newTestWindowService(t) - result, handled, err := c.PERFORM(TaskOpenWindow{ + r := taskRun(c, "window.open", TaskOpenWindow{ Options: []WindowOption{WithName("test-fallback"), WithURL("/")}, }) - require.NoError(t, err) - assert.True(t, handled) - info := result.(WindowInfo) + require.True(t, r.OK) + info := r.Value.(WindowInfo) assert.Equal(t, "test-fallback", info.Name) } func TestTaskOpenWindow_Bad(t *testing.T) { - // No window service registered — PERFORM returns handled=false - c, err := core.New(core.WithServiceLock()) - require.NoError(t, err) - _, handled, _ := c.PERFORM(TaskOpenWindow{}) - assert.False(t, handled) + // No window service registered — action is not registered + c := core.New(core.WithServiceLock()) + r := c.Action("window.open").Run(context.Background(), core.NewOptions()) + assert.False(t, r.OK) } func TestQueryWindowList_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("a")}}) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("b")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("a")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("b")}}) - result, handled, err := c.QUERY(QueryWindowList{}) - require.NoError(t, err) - assert.True(t, handled) - list := result.([]WindowInfo) + r := c.QUERY(QueryWindowList{}) + require.True(t, r.OK) + list := r.Value.([]WindowInfo) assert.Len(t, list, 2) } func TestQueryWindowByName_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - result, handled, err := c.QUERY(QueryWindowByName{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) - info := result.(*WindowInfo) + r := c.QUERY(QueryWindowByName{Name: "test"}) + require.True(t, r.OK) + info := r.Value.(*WindowInfo) assert.Equal(t, "test", info.Name) } func TestQueryWindowByName_Bad(t *testing.T) { _, c := newTestWindowService(t) - result, handled, err := c.QUERY(QueryWindowByName{Name: "nonexistent"}) - require.NoError(t, err) - assert.True(t, handled) // handled=true, result is nil (not found) - assert.Nil(t, result) + r := c.QUERY(QueryWindowByName{Name: "nonexistent"}) + require.True(t, r.OK) // handled=true, result is nil (not found) + assert.Nil(t, r.Value) } func TestTaskCloseWindow_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskCloseWindow{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.close", TaskCloseWindow{Name: "test"}) + require.True(t, r.OK) // Verify window is removed - result, _, _ := c.QUERY(QueryWindowByName{Name: "test"}) - assert.Nil(t, result) + r2 := c.QUERY(QueryWindowByName{Name: "test"}) + assert.Nil(t, r2.Value) } func TestTaskCloseWindow_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskCloseWindow{Name: "nonexistent"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.close", TaskCloseWindow{Name: "nonexistent"}) + assert.False(t, r.OK) } func TestTaskSetPosition_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskSetPosition{Name: "test", X: 100, Y: 200}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.setPosition", TaskSetPosition{Name: "test", X: 100, Y: 200}) + require.True(t, r.OK) - result, _, _ := c.QUERY(QueryWindowByName{Name: "test"}) - info := result.(*WindowInfo) + r2 := c.QUERY(QueryWindowByName{Name: "test"}) + info := r2.Value.(*WindowInfo) assert.Equal(t, 100, info.X) assert.Equal(t, 200, info.Y) } func TestTaskSetSize_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskSetSize{Name: "test", Width: 800, Height: 600}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.setSize", TaskSetSize{Name: "test", Width: 800, Height: 600}) + require.True(t, r.OK) - result, _, _ := c.QUERY(QueryWindowByName{Name: "test"}) - info := result.(*WindowInfo) + r2 := c.QUERY(QueryWindowByName{Name: "test"}) + info := r2.Value.(*WindowInfo) assert.Equal(t, 800, info.Width) assert.Equal(t, 600, info.Height) } func TestTaskMaximise_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskMaximise{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.maximise", TaskMaximise{Name: "test"}) + require.True(t, r.OK) - result, _, _ := c.QUERY(QueryWindowByName{Name: "test"}) - info := result.(*WindowInfo) + r2 := c.QUERY(QueryWindowByName{Name: "test"}) + info := r2.Value.(*WindowInfo) assert.True(t, info.Maximized) } @@ -154,22 +148,22 @@ func TestFileDrop_Good(t *testing.T) { _, c := newTestWindowService(t) // Open a window - result, _, _ := c.PERFORM(TaskOpenWindow{ + r := taskRun(c, "window.open", TaskOpenWindow{ Options: []WindowOption{WithName("drop-test")}, }) - info := result.(WindowInfo) + info := r.Value.(WindowInfo) assert.Equal(t, "drop-test", info.Name) // Capture broadcast actions var dropped ActionFilesDropped var mu sync.Mutex - c.RegisterAction(func(_ *core.Core, msg core.Message) error { + c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result { if a, ok := msg.(ActionFilesDropped); ok { mu.Lock() dropped = a mu.Unlock() } - return nil + return core.Result{OK: true} }) // Get the mock window and simulate file drop @@ -190,11 +184,10 @@ func TestFileDrop_Good(t *testing.T) { func TestTaskMinimise_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskMinimise{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.minimise", TaskMinimise{Name: "test"}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -204,20 +197,18 @@ func TestTaskMinimise_Good(t *testing.T) { func TestTaskMinimise_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskMinimise{Name: "nonexistent"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.minimise", TaskMinimise{Name: "nonexistent"}) + assert.False(t, r.OK) } // --- TaskFocus --- func TestTaskFocus_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskFocus{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.focus", TaskFocus{Name: "test"}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -227,23 +218,21 @@ func TestTaskFocus_Good(t *testing.T) { func TestTaskFocus_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskFocus{Name: "nonexistent"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.focus", TaskFocus{Name: "nonexistent"}) + assert.False(t, r.OK) } // --- TaskRestore --- func TestTaskRestore_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) // First maximise, then restore - _, _, _ = c.PERFORM(TaskMaximise{Name: "test"}) + taskRun(c, "window.maximise", TaskMaximise{Name: "test"}) - _, handled, err := c.PERFORM(TaskRestore{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.restore", TaskRestore{Name: "test"}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -258,20 +247,18 @@ func TestTaskRestore_Good(t *testing.T) { func TestTaskRestore_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskRestore{Name: "nonexistent"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.restore", TaskRestore{Name: "nonexistent"}) + assert.False(t, r.OK) } // --- TaskSetTitle --- func TestTaskSetTitle_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskSetTitle{Name: "test", Title: "New Title"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.setTitle", TaskSetTitle{Name: "test", Title: "New Title"}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -280,20 +267,18 @@ func TestTaskSetTitle_Good(t *testing.T) { func TestTaskSetTitle_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskSetTitle{Name: "nonexistent", Title: "Nope"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.setTitle", TaskSetTitle{Name: "nonexistent", Title: "Nope"}) + assert.False(t, r.OK) } // --- TaskSetAlwaysOnTop --- func TestTaskSetAlwaysOnTop_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskSetAlwaysOnTop{Name: "test", AlwaysOnTop: true}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.setAlwaysOnTop", TaskSetAlwaysOnTop{Name: "test", AlwaysOnTop: true}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -303,22 +288,20 @@ func TestTaskSetAlwaysOnTop_Good(t *testing.T) { func TestTaskSetAlwaysOnTop_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskSetAlwaysOnTop{Name: "nonexistent", AlwaysOnTop: true}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.setAlwaysOnTop", TaskSetAlwaysOnTop{Name: "nonexistent", AlwaysOnTop: true}) + assert.False(t, r.OK) } // --- TaskSetBackgroundColour --- func TestTaskSetBackgroundColour_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskSetBackgroundColour{ + r := taskRun(c, "window.setBackgroundColour", TaskSetBackgroundColour{ Name: "test", Red: 10, Green: 20, Blue: 30, Alpha: 40, }) - require.NoError(t, err) - assert.True(t, handled) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -328,20 +311,18 @@ func TestTaskSetBackgroundColour_Good(t *testing.T) { func TestTaskSetBackgroundColour_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskSetBackgroundColour{Name: "nonexistent", Red: 1, Green: 2, Blue: 3, Alpha: 4}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.setBackgroundColour", TaskSetBackgroundColour{Name: "nonexistent", Red: 1, Green: 2, Blue: 3, Alpha: 4}) + assert.False(t, r.OK) } // --- TaskSetVisibility --- func TestTaskSetVisibility_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskSetVisibility{Name: "test", Visible: true}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.setVisibility", TaskSetVisibility{Name: "test", Visible: true}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -349,29 +330,26 @@ func TestTaskSetVisibility_Good(t *testing.T) { assert.True(t, mw.visible) // Now hide it - _, handled, err = c.PERFORM(TaskSetVisibility{Name: "test", Visible: false}) - require.NoError(t, err) - assert.True(t, handled) + r2 := taskRun(c, "window.setVisibility", TaskSetVisibility{Name: "test", Visible: false}) + require.True(t, r2.OK) assert.False(t, mw.visible) } func TestTaskSetVisibility_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskSetVisibility{Name: "nonexistent", Visible: true}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.setVisibility", TaskSetVisibility{Name: "nonexistent", Visible: true}) + assert.False(t, r.OK) } // --- TaskFullscreen --- func TestTaskFullscreen_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) // Enter fullscreen - _, handled, err := c.PERFORM(TaskFullscreen{Name: "test", Fullscreen: true}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.fullscreen", TaskFullscreen{Name: "test", Fullscreen: true}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -379,29 +357,26 @@ func TestTaskFullscreen_Good(t *testing.T) { assert.True(t, mw.fullscreened) // Exit fullscreen - _, handled, err = c.PERFORM(TaskFullscreen{Name: "test", Fullscreen: false}) - require.NoError(t, err) - assert.True(t, handled) + r2 := taskRun(c, "window.fullscreen", TaskFullscreen{Name: "test", Fullscreen: false}) + require.True(t, r2.OK) assert.False(t, mw.fullscreened) } func TestTaskFullscreen_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskFullscreen{Name: "nonexistent", Fullscreen: true}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.fullscreen", TaskFullscreen{Name: "nonexistent", Fullscreen: true}) + assert.False(t, r.OK) } // --- TaskSaveLayout --- 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)}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("editor"), WithSize(960, 1080), WithPosition(0, 0)}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("terminal"), WithSize(960, 1080), WithPosition(960, 0)}}) - _, handled, err := c.PERFORM(TaskSaveLayout{Name: "coding"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.saveLayout", TaskSaveLayout{Name: "coding"}) + require.True(t, r.OK) // Verify layout was saved with correct window states layout, ok := svc.Manager().Layout().GetLayout("coding") @@ -423,9 +398,8 @@ func TestTaskSaveLayout_Good(t *testing.T) { func TestTaskSaveLayout_Bad(t *testing.T) { _, c := newTestWindowService(t) // Saving an empty layout with empty name returns an error from LayoutManager - _, handled, err := c.PERFORM(TaskSaveLayout{Name: ""}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.saveLayout", TaskSaveLayout{Name: ""}) + assert.False(t, r.OK) } // --- TaskRestoreLayout --- @@ -433,20 +407,19 @@ 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)}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("editor"), WithSize(800, 600), WithPosition(0, 0)}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("terminal"), WithSize(800, 600), WithPosition(0, 0)}}) // Save a layout with specific positions - _, _, _ = c.PERFORM(TaskSaveLayout{Name: "coding"}) + taskRun(c, "window.saveLayout", TaskSaveLayout{Name: "coding"}) // Move the windows to different positions - _, _, _ = c.PERFORM(TaskSetPosition{Name: "editor", X: 500, Y: 500}) - _, _, _ = c.PERFORM(TaskSetPosition{Name: "terminal", X: 600, Y: 600}) + taskRun(c, "window.setPosition", TaskSetPosition{Name: "editor", X: 500, Y: 500}) + taskRun(c, "window.setPosition", TaskSetPosition{Name: "terminal", X: 600, Y: 600}) // Restore the layout - _, handled, err := c.PERFORM(TaskRestoreLayout{Name: "coding"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.restoreLayout", TaskRestoreLayout{Name: "coding"}) + require.True(t, r.OK) // Verify windows were moved back to saved positions pw, ok := svc.Manager().Get("editor") @@ -474,21 +447,19 @@ func TestTaskRestoreLayout_Good(t *testing.T) { func TestTaskRestoreLayout_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskRestoreLayout{Name: "nonexistent"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.restoreLayout", TaskRestoreLayout{Name: "nonexistent"}) + assert.False(t, r.OK) } // --- TaskStackWindows --- 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)}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("s1"), WithSize(800, 600)}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("s2"), WithSize(800, 600)}}) - _, handled, err := c.PERFORM(TaskStackWindows{Windows: []string{"s1", "s2"}, OffsetX: 25, OffsetY: 35}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.stackWindows", TaskStackWindows{Windows: []string{"s1", "s2"}, OffsetX: 25, OffsetY: 35}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("s2") require.True(t, ok) @@ -501,12 +472,11 @@ 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)}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("editor"), WithSize(800, 600)}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("terminal"), WithSize(800, 600)}}) - _, handled, err := c.PERFORM(TaskApplyWorkflow{Workflow: "side-by-side"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.applyWorkflow", TaskApplyWorkflow{Workflow: "side-by-side"}) + require.True(t, r.OK) editor, ok := svc.Manager().Get("editor") require.True(t, ok) @@ -525,28 +495,25 @@ func TestTaskApplyWorkflow_Good(t *testing.T) { func TestQueryWindowZoom_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - result, handled, err := c.QUERY(QueryWindowZoom{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, 1.0, result.(float64)) + r := c.QUERY(QueryWindowZoom{Name: "test"}) + require.True(t, r.OK) + assert.Equal(t, 1.0, r.Value.(float64)) } func TestQueryWindowZoom_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.QUERY(QueryWindowZoom{Name: "nonexistent"}) - assert.True(t, handled) - assert.Error(t, err) + r := c.QUERY(QueryWindowZoom{Name: "nonexistent"}) + assert.False(t, r.OK) } func TestTaskSetZoom_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskSetZoom{Name: "test", Magnification: 1.5}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.setZoom", TaskSetZoom{Name: "test", Magnification: 1.5}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -556,18 +523,16 @@ func TestTaskSetZoom_Good(t *testing.T) { func TestTaskSetZoom_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskSetZoom{Name: "nonexistent", Magnification: 1.5}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.setZoom", TaskSetZoom{Name: "nonexistent", Magnification: 1.5}) + assert.False(t, r.OK) } func TestTaskZoomIn_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskZoomIn{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.zoomIn", TaskZoomIn{Name: "test"}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -577,20 +542,18 @@ func TestTaskZoomIn_Good(t *testing.T) { func TestTaskZoomIn_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskZoomIn{Name: "nonexistent"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.zoomIn", TaskZoomIn{Name: "nonexistent"}) + assert.False(t, r.OK) } func TestTaskZoomOut_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) // Set zoom to 1.5 first so we can decrease it - _, _, _ = c.PERFORM(TaskSetZoom{Name: "test", Magnification: 1.5}) + taskRun(c, "window.setZoom", TaskSetZoom{Name: "test", Magnification: 1.5}) - _, handled, err := c.PERFORM(TaskZoomOut{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.zoomOut", TaskZoomOut{Name: "test"}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -600,19 +563,17 @@ func TestTaskZoomOut_Good(t *testing.T) { func TestTaskZoomOut_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskZoomOut{Name: "nonexistent"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.zoomOut", TaskZoomOut{Name: "nonexistent"}) + assert.False(t, r.OK) } func TestTaskZoomReset_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, _, _ = c.PERFORM(TaskSetZoom{Name: "test", Magnification: 2.0}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.setZoom", TaskSetZoom{Name: "test", Magnification: 2.0}) - _, handled, err := c.PERFORM(TaskZoomReset{Name: "test"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.zoomReset", TaskZoomReset{Name: "test"}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -622,20 +583,18 @@ func TestTaskZoomReset_Good(t *testing.T) { func TestTaskZoomReset_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskZoomReset{Name: "nonexistent"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.zoomReset", TaskZoomReset{Name: "nonexistent"}) + assert.False(t, r.OK) } // --- Content --- func TestTaskSetURL_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskSetURL{Name: "test", URL: "https://example.com"}) - require.NoError(t, err) - assert.True(t, handled) + r := taskRun(c, "window.setURL", TaskSetURL{Name: "test", URL: "https://example.com"}) + require.True(t, r.OK) pw, ok := svc.Manager().Get("test") require.True(t, ok) @@ -645,18 +604,16 @@ func TestTaskSetURL_Good(t *testing.T) { func TestTaskSetURL_Bad(t *testing.T) { _, c := newTestWindowService(t) - _, handled, err := c.PERFORM(TaskSetURL{Name: "nonexistent", URL: "https://example.com"}) - assert.True(t, handled) - assert.Error(t, err) + r := taskRun(c, "window.setURL", TaskSetURL{Name: "nonexistent", URL: "https://example.com"}) + assert.False(t, r.OK) } func TestTaskSetHTML_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) + taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskSetHTML{Name: "test", HTML: "