feat(dialog): add dialog core.Service with Platform interface and IPC
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7b8ed9df6f
commit
df37dfa041
4 changed files with 253 additions and 0 deletions
14
pkg/dialog/messages.go
Normal file
14
pkg/dialog/messages.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// pkg/dialog/messages.go
|
||||
package dialog
|
||||
|
||||
// TaskOpenFile shows an open file dialog. Result: []string (paths)
|
||||
type TaskOpenFile struct{ Opts OpenFileOptions }
|
||||
|
||||
// TaskSaveFile shows a save file dialog. Result: string (path)
|
||||
type TaskSaveFile struct{ Opts SaveFileOptions }
|
||||
|
||||
// TaskOpenDirectory shows a directory picker. Result: string (path)
|
||||
type TaskOpenDirectory struct{ Opts OpenDirectoryOptions }
|
||||
|
||||
// TaskMessageDialog shows a message dialog. Result: string (button clicked)
|
||||
type TaskMessageDialog struct{ Opts MessageDialogOptions }
|
||||
59
pkg/dialog/platform.go
Normal file
59
pkg/dialog/platform.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// pkg/dialog/platform.go
|
||||
package dialog
|
||||
|
||||
// Platform abstracts the native dialog backend.
|
||||
type Platform interface {
|
||||
OpenFile(opts OpenFileOptions) ([]string, error)
|
||||
SaveFile(opts SaveFileOptions) (string, error)
|
||||
OpenDirectory(opts OpenDirectoryOptions) (string, error)
|
||||
MessageDialog(opts MessageDialogOptions) (string, error)
|
||||
}
|
||||
|
||||
// DialogType represents the type of message dialog.
|
||||
type DialogType int
|
||||
|
||||
const (
|
||||
DialogInfo DialogType = iota
|
||||
DialogWarning
|
||||
DialogError
|
||||
DialogQuestion
|
||||
)
|
||||
|
||||
// OpenFileOptions contains options for the open file dialog.
|
||||
type OpenFileOptions struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Directory string `json:"directory,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Filters []FileFilter `json:"filters,omitempty"`
|
||||
AllowMultiple bool `json:"allowMultiple,omitempty"`
|
||||
}
|
||||
|
||||
// SaveFileOptions contains options for the save file dialog.
|
||||
type SaveFileOptions struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Directory string `json:"directory,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Filters []FileFilter `json:"filters,omitempty"`
|
||||
}
|
||||
|
||||
// OpenDirectoryOptions contains options for the directory picker.
|
||||
type OpenDirectoryOptions struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Directory string `json:"directory,omitempty"`
|
||||
AllowMultiple bool `json:"allowMultiple,omitempty"`
|
||||
}
|
||||
|
||||
// MessageDialogOptions contains options for a message dialog.
|
||||
type MessageDialogOptions struct {
|
||||
Type DialogType `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Buttons []string `json:"buttons,omitempty"`
|
||||
}
|
||||
|
||||
// FileFilter represents a file type filter for dialogs.
|
||||
type FileFilter struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
Pattern string `json:"pattern"`
|
||||
Extensions []string `json:"extensions,omitempty"`
|
||||
}
|
||||
57
pkg/dialog/service.go
Normal file
57
pkg/dialog/service.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// pkg/dialog/service.go
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
)
|
||||
|
||||
// Options holds configuration for the dialog service.
|
||||
type Options struct{}
|
||||
|
||||
// Service is a core.Service managing native dialogs 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().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) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
||||
switch t := t.(type) {
|
||||
case TaskOpenFile:
|
||||
paths, err := s.platform.OpenFile(t.Opts)
|
||||
return paths, true, err
|
||||
case TaskSaveFile:
|
||||
path, err := s.platform.SaveFile(t.Opts)
|
||||
return path, true, err
|
||||
case TaskOpenDirectory:
|
||||
path, err := s.platform.OpenDirectory(t.Opts)
|
||||
return path, true, err
|
||||
case TaskMessageDialog:
|
||||
button, err := s.platform.MessageDialog(t.Opts)
|
||||
return button, true, err
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
123
pkg/dialog/service_test.go
Normal file
123
pkg/dialog/service_test.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// pkg/dialog/service_test.go
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockPlatform struct {
|
||||
openFilePaths []string
|
||||
saveFilePath string
|
||||
openDirPath string
|
||||
messageButton string
|
||||
openFileErr error
|
||||
saveFileErr error
|
||||
openDirErr error
|
||||
messageErr error
|
||||
lastOpenOpts OpenFileOptions
|
||||
lastSaveOpts SaveFileOptions
|
||||
lastDirOpts OpenDirectoryOptions
|
||||
lastMsgOpts MessageDialogOptions
|
||||
}
|
||||
|
||||
func (m *mockPlatform) OpenFile(opts OpenFileOptions) ([]string, error) {
|
||||
m.lastOpenOpts = opts
|
||||
return m.openFilePaths, m.openFileErr
|
||||
}
|
||||
func (m *mockPlatform) SaveFile(opts SaveFileOptions) (string, error) {
|
||||
m.lastSaveOpts = opts
|
||||
return m.saveFilePath, m.saveFileErr
|
||||
}
|
||||
func (m *mockPlatform) OpenDirectory(opts OpenDirectoryOptions) (string, error) {
|
||||
m.lastDirOpts = opts
|
||||
return m.openDirPath, m.openDirErr
|
||||
}
|
||||
func (m *mockPlatform) MessageDialog(opts MessageDialogOptions) (string, error) {
|
||||
m.lastMsgOpts = opts
|
||||
return m.messageButton, m.messageErr
|
||||
}
|
||||
|
||||
func newTestService(t *testing.T) (*mockPlatform, *core.Core) {
|
||||
t.Helper()
|
||||
mock := &mockPlatform{
|
||||
openFilePaths: []string{"/tmp/file.txt"},
|
||||
saveFilePath: "/tmp/save.txt",
|
||||
openDirPath: "/tmp/dir",
|
||||
messageButton: "OK",
|
||||
}
|
||||
c, err := core.New(
|
||||
core.WithService(Register(mock)),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
||||
return mock, c
|
||||
}
|
||||
|
||||
func TestRegister_Good(t *testing.T) {
|
||||
_, c := newTestService(t)
|
||||
svc := core.MustServiceFor[*Service](c, "dialog")
|
||||
assert.NotNil(t, svc)
|
||||
}
|
||||
|
||||
func TestTaskOpenFile_Good(t *testing.T) {
|
||||
mock, c := newTestService(t)
|
||||
mock.openFilePaths = []string{"/a.txt", "/b.txt"}
|
||||
|
||||
result, handled, err := c.PERFORM(TaskOpenFile{
|
||||
Opts: OpenFileOptions{Title: "Pick", AllowMultiple: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
paths := result.([]string)
|
||||
assert.Equal(t, []string{"/a.txt", "/b.txt"}, paths)
|
||||
assert.Equal(t, "Pick", mock.lastOpenOpts.Title)
|
||||
assert.True(t, mock.lastOpenOpts.AllowMultiple)
|
||||
}
|
||||
|
||||
func TestTaskSaveFile_Good(t *testing.T) {
|
||||
_, c := newTestService(t)
|
||||
result, handled, err := c.PERFORM(TaskSaveFile{
|
||||
Opts: SaveFileOptions{Filename: "out.txt"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "/tmp/save.txt", result)
|
||||
}
|
||||
|
||||
func TestTaskOpenDirectory_Good(t *testing.T) {
|
||||
_, c := newTestService(t)
|
||||
result, handled, err := c.PERFORM(TaskOpenDirectory{
|
||||
Opts: OpenDirectoryOptions{Title: "Pick Dir"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "/tmp/dir", result)
|
||||
}
|
||||
|
||||
func TestTaskMessageDialog_Good(t *testing.T) {
|
||||
mock, c := newTestService(t)
|
||||
mock.messageButton = "Yes"
|
||||
|
||||
result, handled, err := c.PERFORM(TaskMessageDialog{
|
||||
Opts: MessageDialogOptions{
|
||||
Type: DialogQuestion, Title: "Confirm",
|
||||
Message: "Sure?", Buttons: []string{"Yes", "No"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "Yes", result)
|
||||
assert.Equal(t, DialogQuestion, mock.lastMsgOpts.Type)
|
||||
}
|
||||
|
||||
func TestTaskOpenFile_Bad(t *testing.T) {
|
||||
c, _ := core.New(core.WithServiceLock())
|
||||
_, handled, _ := c.PERFORM(TaskOpenFile{})
|
||||
assert.False(t, handled)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue