feat(dock): add dock/badge core.Service with Platform interface and IPC
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
22315d07bb
commit
073794aed0
5 changed files with 324 additions and 0 deletions
29
pkg/dock/messages.go
Normal file
29
pkg/dock/messages.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// pkg/dock/messages.go
|
||||
package dock
|
||||
|
||||
// --- Queries (read-only) ---
|
||||
|
||||
// QueryVisible returns whether the dock icon is visible. Result: bool
|
||||
type QueryVisible struct{}
|
||||
|
||||
// --- Tasks (side-effects) ---
|
||||
|
||||
// TaskShowIcon shows the dock/taskbar icon. Result: nil
|
||||
type TaskShowIcon struct{}
|
||||
|
||||
// TaskHideIcon hides the dock/taskbar icon. Result: nil
|
||||
type TaskHideIcon struct{}
|
||||
|
||||
// TaskSetBadge sets the dock/taskbar badge label.
|
||||
// Empty string "" shows the default system badge indicator.
|
||||
// Numeric "3", "99" shows unread count. Text "New", "Paused" shows brief status.
|
||||
// Result: nil
|
||||
type TaskSetBadge struct{ Label string }
|
||||
|
||||
// TaskRemoveBadge removes the dock/taskbar badge. Result: nil
|
||||
type TaskRemoveBadge struct{}
|
||||
|
||||
// --- Actions (broadcasts) ---
|
||||
|
||||
// ActionVisibilityChanged is broadcast after a successful TaskShowIcon or TaskHideIcon.
|
||||
type ActionVisibilityChanged struct{ Visible bool }
|
||||
14
pkg/dock/platform.go
Normal file
14
pkg/dock/platform.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// pkg/dock/platform.go
|
||||
package dock
|
||||
|
||||
// Platform abstracts the dock/taskbar backend (Wails v3).
|
||||
// macOS: dock icon show/hide + badge.
|
||||
// Windows: taskbar badge only (show/hide not supported).
|
||||
// Linux: not supported — adapter returns nil for all operations.
|
||||
type Platform interface {
|
||||
ShowIcon() error
|
||||
HideIcon() error
|
||||
SetBadge(label string) error
|
||||
RemoveBadge() error
|
||||
IsVisible() bool
|
||||
}
|
||||
15
pkg/dock/register.go
Normal file
15
pkg/dock/register.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// pkg/dock/register.go
|
||||
package dock
|
||||
|
||||
import "forge.lthn.ai/core/go/pkg/core"
|
||||
|
||||
// Register creates a factory closure that captures the Platform adapter.
|
||||
// The returned function has the signature WithService requires: func(*Core) (any, error).
|
||||
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
|
||||
}
|
||||
}
|
||||
72
pkg/dock/service.go
Normal file
72
pkg/dock/service.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// pkg/dock/service.go
|
||||
package dock
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
)
|
||||
|
||||
// Options holds configuration for the dock service.
|
||||
type Options struct{}
|
||||
|
||||
// Service is a core.Service managing dock/taskbar operations via IPC.
|
||||
// It embeds ServiceRuntime for Core access and delegates to Platform.
|
||||
type Service struct {
|
||||
*core.ServiceRuntime[Options]
|
||||
platform Platform
|
||||
}
|
||||
|
||||
// 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 and registered by core.WithService.
|
||||
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Query Handlers ---
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// --- Task Handlers ---
|
||||
|
||||
func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
||||
switch t := t.(type) {
|
||||
case TaskShowIcon:
|
||||
if err := s.platform.ShowIcon(); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
_ = s.Core().ACTION(ActionVisibilityChanged{Visible: true})
|
||||
return nil, true, nil
|
||||
case TaskHideIcon:
|
||||
if err := s.platform.HideIcon(); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
_ = 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 nil, true, nil
|
||||
case TaskRemoveBadge:
|
||||
if err := s.platform.RemoveBadge(); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
return nil, true, nil
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
194
pkg/dock/service_test.go
Normal file
194
pkg/dock/service_test.go
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
// pkg/dock/service_test.go
|
||||
package dock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// --- Mock Platform ---
|
||||
|
||||
type mockPlatform struct {
|
||||
visible bool
|
||||
badge string
|
||||
hasBadge bool
|
||||
showErr error
|
||||
hideErr error
|
||||
badgeErr error
|
||||
removeErr error
|
||||
}
|
||||
|
||||
func (m *mockPlatform) ShowIcon() error {
|
||||
if m.showErr != nil {
|
||||
return m.showErr
|
||||
}
|
||||
m.visible = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPlatform) HideIcon() error {
|
||||
if m.hideErr != nil {
|
||||
return m.hideErr
|
||||
}
|
||||
m.visible = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPlatform) SetBadge(label string) error {
|
||||
if m.badgeErr != nil {
|
||||
return m.badgeErr
|
||||
}
|
||||
m.badge = label
|
||||
m.hasBadge = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPlatform) RemoveBadge() error {
|
||||
if m.removeErr != nil {
|
||||
return m.removeErr
|
||||
}
|
||||
m.badge = ""
|
||||
m.hasBadge = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPlatform) IsVisible() bool { return m.visible }
|
||||
|
||||
// --- Test helpers ---
|
||||
|
||||
func newTestDockService(t *testing.T) (*Service, *core.Core, *mockPlatform) {
|
||||
t.Helper()
|
||||
mock := &mockPlatform{visible: true}
|
||||
c, err := core.New(
|
||||
core.WithService(Register(mock)),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
||||
svc := core.MustServiceFor[*Service](c, "dock")
|
||||
return svc, c, mock
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
func TestRegister_Good(t *testing.T) {
|
||||
svc, _, _ := newTestDockService(t)
|
||||
assert.NotNil(t, svc)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func TestTaskShowIcon_Good(t *testing.T) {
|
||||
_, c, mock := newTestDockService(t)
|
||||
mock.visible = false // Start hidden
|
||||
|
||||
var received *ActionVisibilityChanged
|
||||
c.RegisterAction(func(_ *core.Core, msg core.Message) error {
|
||||
if a, ok := msg.(ActionVisibilityChanged); ok {
|
||||
received = &a
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskShowIcon{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.True(t, mock.visible)
|
||||
require.NotNil(t, received)
|
||||
assert.True(t, received.Visible)
|
||||
}
|
||||
|
||||
func TestTaskHideIcon_Good(t *testing.T) {
|
||||
_, c, mock := newTestDockService(t)
|
||||
mock.visible = true // Start visible
|
||||
|
||||
var received *ActionVisibilityChanged
|
||||
c.RegisterAction(func(_ *core.Core, msg core.Message) error {
|
||||
if a, ok := msg.(ActionVisibilityChanged); ok {
|
||||
received = &a
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskHideIcon{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.False(t, mock.visible)
|
||||
require.NotNil(t, received)
|
||||
assert.False(t, received.Visible)
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
assert.Equal(t, "", mock.badge)
|
||||
assert.True(t, mock.hasBadge) // Empty string = default system badge indicator
|
||||
}
|
||||
|
||||
func TestTaskRemoveBadge_Good(t *testing.T) {
|
||||
_, c, mock := newTestDockService(t)
|
||||
// Set a badge first
|
||||
_, _, _ = c.PERFORM(TaskSetBadge{Label: "5"})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskRemoveBadge{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "", mock.badge)
|
||||
assert.False(t, mock.hasBadge)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue