refactor: migrate entire gui to Core v0.8.0 API
Some checks failed
Security Scan / security (push) Failing after 25s
Some checks failed
Security Scan / security (push) Failing after 25s
- Import paths: forge.lthn.ai/core/go → dappco.re/go/core
- Import paths: forge.lthn.ai/core/go-log → dappco.re/go/core/log
- Import paths: forge.lthn.ai/core/go-io → dappco.re/go/core/io
- RegisterTask → c.Action("name", handler) across all 15 services
- QueryHandler signature: (any, bool, error) → core.Result
- PERFORM(task) → Action.Run(ctx, opts)
- QUERY returns single core.Result (not 3 values)
- All 17 packages build and test clean on v0.8.0-alpha.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
84ec201a05
commit
18a455b460
67 changed files with 2954 additions and 2498 deletions
14
go.mod
14
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
|
||||
)
|
||||
|
|
|
|||
7
go.sum
7
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=
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: "<h1>Hello</h1>"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
r := taskRun(c, "window.setHTML", TaskSetHTML{Name: "test", HTML: "<h1>Hello</h1>"})
|
||||
require.True(t, r.OK)
|
||||
|
||||
pw, ok := svc.Manager().Get("test")
|
||||
require.True(t, ok)
|
||||
|
|
@ -666,18 +623,16 @@ func TestTaskSetHTML_Good(t *testing.T) {
|
|||
|
||||
func TestTaskSetHTML_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskSetHTML{Name: "nonexistent", HTML: "<h1>Hello</h1>"})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
r := taskRun(c, "window.setHTML", TaskSetHTML{Name: "nonexistent", HTML: "<h1>Hello</h1>"})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestTaskExecJS_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(TaskExecJS{Name: "test", JS: "document.title = 'Ready'"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
r := taskRun(c, "window.execJS", TaskExecJS{Name: "test", JS: "document.title = 'Ready'"})
|
||||
require.True(t, r.OK)
|
||||
|
||||
pw, ok := svc.Manager().Get("test")
|
||||
require.True(t, ok)
|
||||
|
|
@ -687,21 +642,19 @@ func TestTaskExecJS_Good(t *testing.T) {
|
|||
|
||||
func TestTaskExecJS_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskExecJS{Name: "nonexistent", JS: "alert(1)"})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
r := taskRun(c, "window.execJS", TaskExecJS{Name: "nonexistent", JS: "alert(1)"})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
// --- State toggles ---
|
||||
|
||||
func TestTaskToggleFullscreen_Good(t *testing.T) {
|
||||
svc, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
|
||||
taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}})
|
||||
|
||||
// Toggle on
|
||||
_, handled, err := c.PERFORM(TaskToggleFullscreen{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
r := taskRun(c, "window.toggleFullscreen", TaskToggleFullscreen{Name: "test"})
|
||||
require.True(t, r.OK)
|
||||
|
||||
pw, ok := svc.Manager().Get("test")
|
||||
require.True(t, ok)
|
||||
|
|
@ -709,25 +662,23 @@ func TestTaskToggleFullscreen_Good(t *testing.T) {
|
|||
assert.True(t, mw.fullscreened)
|
||||
|
||||
// Toggle off
|
||||
_, _, _ = c.PERFORM(TaskToggleFullscreen{Name: "test"})
|
||||
taskRun(c, "window.toggleFullscreen", TaskToggleFullscreen{Name: "test"})
|
||||
assert.False(t, mw.fullscreened)
|
||||
}
|
||||
|
||||
func TestTaskToggleFullscreen_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskToggleFullscreen{Name: "nonexistent"})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
r := taskRun(c, "window.toggleFullscreen", TaskToggleFullscreen{Name: "nonexistent"})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestTaskToggleMaximise_Good(t *testing.T) {
|
||||
svc, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
|
||||
taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test")}})
|
||||
|
||||
// Toggle on
|
||||
_, handled, err := c.PERFORM(TaskToggleMaximise{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
r := taskRun(c, "window.toggleMaximise", TaskToggleMaximise{Name: "test"})
|
||||
require.True(t, r.OK)
|
||||
|
||||
pw, ok := svc.Manager().Get("test")
|
||||
require.True(t, ok)
|
||||
|
|
@ -735,30 +686,28 @@ func TestTaskToggleMaximise_Good(t *testing.T) {
|
|||
assert.True(t, mw.maximised)
|
||||
|
||||
// Toggle off
|
||||
_, _, _ = c.PERFORM(TaskToggleMaximise{Name: "test"})
|
||||
taskRun(c, "window.toggleMaximise", TaskToggleMaximise{Name: "test"})
|
||||
assert.False(t, mw.maximised)
|
||||
}
|
||||
|
||||
func TestTaskToggleMaximise_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskToggleMaximise{Name: "nonexistent"})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
r := taskRun(c, "window.toggleMaximise", TaskToggleMaximise{Name: "nonexistent"})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
// --- Bounds ---
|
||||
|
||||
func TestQueryWindowBounds_Good(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{
|
||||
taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{
|
||||
WithName("test"), WithSize(800, 600), WithPosition(100, 200),
|
||||
}})
|
||||
|
||||
result, handled, err := c.QUERY(QueryWindowBounds{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
r := c.QUERY(QueryWindowBounds{Name: "test"})
|
||||
require.True(t, r.OK)
|
||||
|
||||
bounds := result.(WindowBounds)
|
||||
bounds := r.Value.(WindowBounds)
|
||||
assert.Equal(t, 100, bounds.X)
|
||||
assert.Equal(t, 200, bounds.Y)
|
||||
assert.Equal(t, 800, bounds.Width)
|
||||
|
|
@ -767,18 +716,16 @@ func TestQueryWindowBounds_Good(t *testing.T) {
|
|||
|
||||
func TestQueryWindowBounds_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.QUERY(QueryWindowBounds{Name: "nonexistent"})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
r := c.QUERY(QueryWindowBounds{Name: "nonexistent"})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestTaskSetBounds_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(TaskSetBounds{Name: "test", X: 50, Y: 75, Width: 1024, Height: 768})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
r := taskRun(c, "window.setBounds", TaskSetBounds{Name: "test", X: 50, Y: 75, Width: 1024, Height: 768})
|
||||
require.True(t, r.OK)
|
||||
|
||||
pw, ok := svc.Manager().Get("test")
|
||||
require.True(t, ok)
|
||||
|
|
@ -791,20 +738,18 @@ func TestTaskSetBounds_Good(t *testing.T) {
|
|||
|
||||
func TestTaskSetBounds_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskSetBounds{Name: "nonexistent", X: 0, Y: 0, Width: 800, Height: 600})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
r := taskRun(c, "window.setBounds", TaskSetBounds{Name: "nonexistent", X: 0, Y: 0, Width: 800, Height: 600})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
// --- Content protection ---
|
||||
|
||||
func TestTaskSetContentProtection_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(TaskSetContentProtection{Name: "test", Protection: true})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
r := taskRun(c, "window.setContentProtection", TaskSetContentProtection{Name: "test", Protection: true})
|
||||
require.True(t, r.OK)
|
||||
|
||||
pw, ok := svc.Manager().Get("test")
|
||||
require.True(t, ok)
|
||||
|
|
@ -814,20 +759,18 @@ func TestTaskSetContentProtection_Good(t *testing.T) {
|
|||
|
||||
func TestTaskSetContentProtection_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskSetContentProtection{Name: "nonexistent", Protection: true})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
r := taskRun(c, "window.setContentProtection", TaskSetContentProtection{Name: "nonexistent", Protection: true})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
// --- Flash ---
|
||||
|
||||
func TestTaskFlash_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(TaskFlash{Name: "test", Enabled: true})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
r := taskRun(c, "window.flash", TaskFlash{Name: "test", Enabled: true})
|
||||
require.True(t, r.OK)
|
||||
|
||||
pw, ok := svc.Manager().Get("test")
|
||||
require.True(t, ok)
|
||||
|
|
@ -837,27 +780,24 @@ func TestTaskFlash_Good(t *testing.T) {
|
|||
|
||||
func TestTaskFlash_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskFlash{Name: "nonexistent", Enabled: true})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
r := taskRun(c, "window.flash", TaskFlash{Name: "nonexistent", Enabled: true})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
// --- Print ---
|
||||
|
||||
func TestTaskPrint_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(TaskPrint{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
r := taskRun(c, "window.print", TaskPrint{Name: "test"})
|
||||
require.True(t, r.OK)
|
||||
}
|
||||
|
||||
func TestTaskPrint_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskPrint{Name: "nonexistent"})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
r := taskRun(c, "window.print", TaskPrint{Name: "nonexistent"})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
// --- State queries (IsVisible, IsFullscreen, IsMinimised) ---
|
||||
|
|
@ -865,12 +805,12 @@ func TestTaskPrint_Bad(t *testing.T) {
|
|||
func TestQueryWindowBounds_Ugly(t *testing.T) {
|
||||
// Verify bounds reflect position and size changes
|
||||
_, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test"), WithSize(1280, 800)}})
|
||||
_, _, _ = c.PERFORM(TaskSetBounds{Name: "test", X: 10, Y: 20, Width: 640, Height: 480})
|
||||
taskRun(c, "window.open", TaskOpenWindow{Options: []WindowOption{WithName("test"), WithSize(1280, 800)}})
|
||||
taskRun(c, "window.setBounds", TaskSetBounds{Name: "test", X: 10, Y: 20, Width: 640, Height: 480})
|
||||
|
||||
result, _, err := c.QUERY(QueryWindowBounds{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
bounds := result.(WindowBounds)
|
||||
r := c.QUERY(QueryWindowBounds{Name: "test"})
|
||||
require.True(t, r.OK)
|
||||
bounds := r.Value.(WindowBounds)
|
||||
assert.Equal(t, 10, bounds.X)
|
||||
assert.Equal(t, 20, bounds.Y)
|
||||
assert.Equal(t, 640, bounds.Width)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
coreio "dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
// WindowState holds the persisted position/size of a window.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// pkg/window/tiling.go
|
||||
package window
|
||||
|
||||
import coreerr "forge.lthn.ai/core/go-log"
|
||||
import coreerr "dappco.re/go/core/log"
|
||||
|
||||
// TileMode defines how windows are arranged.
|
||||
type TileMode int
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ package window
|
|||
import (
|
||||
"sync"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// Window is CoreGUI's own window descriptor — NOT a Wails type alias.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue