diff --git a/pkg/clipboard/messages.go b/pkg/clipboard/messages.go new file mode 100644 index 0000000..29f29de --- /dev/null +++ b/pkg/clipboard/messages.go @@ -0,0 +1,11 @@ +// pkg/clipboard/messages.go +package clipboard + +// QueryText reads the clipboard. Result: ClipboardContent +type QueryText struct{} + +// TaskSetText writes text to the clipboard. Result: bool (success) +type TaskSetText struct{ Text string } + +// TaskClear clears the clipboard. Result: bool (success) +type TaskClear struct{} diff --git a/pkg/clipboard/platform.go b/pkg/clipboard/platform.go new file mode 100644 index 0000000..1857cfd --- /dev/null +++ b/pkg/clipboard/platform.go @@ -0,0 +1,14 @@ +// pkg/clipboard/platform.go +package clipboard + +// Platform abstracts the system clipboard backend. +type Platform interface { + Text() (string, bool) + SetText(text string) bool +} + +// ClipboardContent is the result of QueryText. +type ClipboardContent struct { + Text string `json:"text"` + HasContent bool `json:"hasContent"` +} diff --git a/pkg/clipboard/service.go b/pkg/clipboard/service.go new file mode 100644 index 0000000..799c24f --- /dev/null +++ b/pkg/clipboard/service.go @@ -0,0 +1,60 @@ +// pkg/clipboard/service.go +package clipboard + +import ( + "context" + + "forge.lthn.ai/core/go/pkg/core" +) + +// Options holds configuration for the clipboard service. +type Options struct{} + +// Service is a core.Service managing clipboard operations via IPC. +type Service struct { + *core.ServiceRuntime[Options] + platform Platform +} + +// Register creates a factory closure that captures the Platform adapter. +func Register(p Platform) func(*core.Core) (any, error) { + return func(c *core.Core) (any, error) { + return &Service{ + ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), + platform: p, + }, nil + } +} + +// OnStartup registers IPC handlers. +func (s *Service) OnStartup(ctx context.Context) error { + s.Core().RegisterQuery(s.handleQuery) + s.Core().RegisterTask(s.handleTask) + return nil +} + +// HandleIPCEvents is auto-discovered by core.WithService. +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 QueryText: + text, ok := s.platform.Text() + return ClipboardContent{Text: text, HasContent: ok && text != ""}, 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 TaskSetText: + return s.platform.SetText(t.Text), true, nil + case TaskClear: + return s.platform.SetText(""), true, nil + default: + return nil, false, nil + } +} diff --git a/pkg/clipboard/service_test.go b/pkg/clipboard/service_test.go new file mode 100644 index 0000000..63677df --- /dev/null +++ b/pkg/clipboard/service_test.go @@ -0,0 +1,81 @@ +// pkg/clipboard/service_test.go +package clipboard + +import ( + "context" + "testing" + + "forge.lthn.ai/core/go/pkg/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockPlatform struct { + text string + ok bool +} + +func (m *mockPlatform) Text() (string, bool) { return m.text, m.ok } +func (m *mockPlatform) SetText(text string) bool { + m.text = text + m.ok = text != "" + return true +} + +func newTestService(t *testing.T) (*Service, *core.Core) { + t.Helper() + c, err := core.New( + core.WithService(Register(&mockPlatform{text: "hello", ok: true})), + core.WithServiceLock(), + ) + require.NoError(t, err) + require.NoError(t, c.ServiceStartup(context.Background(), nil)) + svc := core.MustServiceFor[*Service](c, "clipboard") + return svc, c +} + +func TestRegister_Good(t *testing.T) { + svc, _ := newTestService(t) + assert.NotNil(t, svc) +} + +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) + 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) +} + +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) + + // Verify via query + r, _, _ := c.QUERY(QueryText{}) + assert.Equal(t, "world", r.(ClipboardContent).Text) +} + +func TestTaskClear_Good(t *testing.T) { + _, c := newTestService(t) + _, handled, err := c.PERFORM(TaskClear{}) + require.NoError(t, err) + assert.True(t, handled) + + // Verify empty + r, _, _ := c.QUERY(QueryText{}) + assert.Equal(t, "", r.(ClipboardContent).Text) + assert.False(t, r.(ClipboardContent).HasContent) +}