refactor: migrate entire gui to Core v0.8.0 API
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:
Claude 2026-03-31 16:14:19 +01:00
parent 84ec201a05
commit 18a455b460
No known key found for this signature in database
GPG key ID: AF404715446AEB41
67 changed files with 2954 additions and 2498 deletions

14
go.mod
View file

@ -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
View file

@ -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=

View file

@ -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}
}
}

View file

@ -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}
}

View file

@ -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)
}

View file

@ -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{}
}
}

View file

@ -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)
}

View file

@ -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}
}
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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}
}

View file

@ -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

View file

@ -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)

View file

@ -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}
}
}

View file

@ -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{}
}
}

View file

@ -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)
}

View file

@ -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{}
}
}

View file

@ -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)
}

View file

@ -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}
}
}

View file

@ -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{}
}
}

View file

@ -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)

View file

@ -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}
}
}

View file

@ -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)

View file

@ -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))
}

View file

@ -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}
}
}

View file

@ -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}
}

View file

@ -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

View file

@ -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)
}

View file

@ -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"
)

View file

@ -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
}

View file

@ -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 ---

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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}
}
}

View file

@ -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{}
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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{}
}
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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}
}
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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}
}
}

View file

@ -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 {

View file

@ -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)

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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.