diff --git a/pkg/browser/register.go b/pkg/browser/register.go index ff081e7..204686a 100644 --- a/pkg/browser/register.go +++ b/pkg/browser/register.go @@ -1,10 +1,7 @@ -// pkg/browser/register.go package browser 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{ diff --git a/pkg/browser/service.go b/pkg/browser/service.go index a3b8915..13000b9 100644 --- a/pkg/browser/service.go +++ b/pkg/browser/service.go @@ -1,4 +1,3 @@ -// pkg/browser/service.go package browser import ( @@ -7,23 +6,18 @@ import ( "forge.lthn.ai/core/go/pkg/core" ) -// Options holds configuration for the browser service. type Options struct{} -// Service is a core.Service that delegates browser/file-open operations -// to the platform. It is stateless — no queries, no actions. type Service struct { *core.ServiceRuntime[Options] platform Platform } -// OnStartup registers IPC handlers. func (s *Service) OnStartup(ctx context.Context) error { 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 } diff --git a/pkg/contextmenu/messages.go b/pkg/contextmenu/messages.go index cb62e17..c5f131f 100644 --- a/pkg/contextmenu/messages.go +++ b/pkg/contextmenu/messages.go @@ -1,42 +1,28 @@ -// pkg/contextmenu/messages.go package contextmenu import "errors" -// ErrMenuNotFound is returned when attempting to remove or get a menu -// that does not exist in the registry. -var ErrMenuNotFound = errors.New("contextmenu: menu not found") +var ErrorMenuNotFound = errors.New("contextmenu: menu not found") // --- Queries --- -// QueryGet returns a single context menu by name. Result: *ContextMenuDef (nil if not found) type QueryGet struct { Name string `json:"name"` } -// QueryList returns all registered context menus. Result: map[string]ContextMenuDef type QueryList struct{} // --- Tasks --- -// TaskAdd registers a context menu. Result: nil -// If a menu with the same name already exists it is replaced (remove + re-add). type TaskAdd struct { Name string `json:"name"` Menu ContextMenuDef `json:"menu"` } -// TaskRemove unregisters a context menu. Result: nil -// Returns ErrMenuNotFound if the menu does not exist. type TaskRemove struct { Name string `json:"name"` } -// --- Actions --- - -// ActionItemClicked is broadcast when a context menu item is clicked. -// The Data field is populated from the CSS --custom-contextmenu-data property -// on the element that triggered the context menu. type ActionItemClicked struct { MenuName string `json:"menuName"` ActionID string `json:"actionId"` diff --git a/pkg/contextmenu/register.go b/pkg/contextmenu/register.go index afb0604..f0c3100 100644 --- a/pkg/contextmenu/register.go +++ b/pkg/contextmenu/register.go @@ -1,16 +1,13 @@ -// pkg/contextmenu/register.go package contextmenu 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, - menus: make(map[string]ContextMenuDef), + ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), + platform: p, + registeredMenus: make(map[string]ContextMenuDef), }, nil } } diff --git a/pkg/contextmenu/service.go b/pkg/contextmenu/service.go index 44ef16b..f6d97e4 100644 --- a/pkg/contextmenu/service.go +++ b/pkg/contextmenu/service.go @@ -8,26 +8,20 @@ import ( "forge.lthn.ai/core/go/pkg/core" ) -// Options holds configuration for the context menu service. type Options struct{} -// Service is a core.Service managing context menus via IPC. -// It maintains an in-memory registry of menus (map[string]ContextMenuDef) -// and delegates platform-level registration to the Platform interface. type Service struct { *core.ServiceRuntime[Options] - platform Platform - menus map[string]ContextMenuDef + platform Platform + registeredMenus map[string]ContextMenuDef } -// 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 } @@ -45,19 +39,17 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { } } -// queryGet returns a single menu definition by name, or nil if not found. func (s *Service) queryGet(q QueryGet) *ContextMenuDef { - menu, ok := s.menus[q.Name] + menu, ok := s.registeredMenus[q.Name] if !ok { return nil } return &menu } -// queryList returns a copy of all registered menus. func (s *Service) queryList() map[string]ContextMenuDef { - result := make(map[string]ContextMenuDef, len(s.menus)) - for k, v := range s.menus { + result := make(map[string]ContextMenuDef, len(s.registeredMenus)) + for k, v := range s.registeredMenus { result[k] = v } return result @@ -78,9 +70,9 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { func (s *Service) taskAdd(t TaskAdd) error { // If menu already exists, remove it first (replace semantics) - if _, exists := s.menus[t.Name]; exists { + if _, exists := s.registeredMenus[t.Name]; exists { _ = s.platform.Remove(t.Name) - delete(s.menus, t.Name) + delete(s.registeredMenus, t.Name) } // Register on platform with a callback that broadcasts ActionItemClicked @@ -95,13 +87,13 @@ func (s *Service) taskAdd(t TaskAdd) error { return coreerr.E("contextmenu.taskAdd", "platform add failed", err) } - s.menus[t.Name] = t.Menu + s.registeredMenus[t.Name] = t.Menu return nil } func (s *Service) taskRemove(t TaskRemove) error { - if _, exists := s.menus[t.Name]; !exists { - return ErrMenuNotFound + if _, exists := s.registeredMenus[t.Name]; !exists { + return ErrorMenuNotFound } err := s.platform.Remove(t.Name) @@ -109,6 +101,6 @@ func (s *Service) taskRemove(t TaskRemove) error { return coreerr.E("contextmenu.taskRemove", "platform remove failed", err) } - delete(s.menus, t.Name) + delete(s.registeredMenus, t.Name) return nil } diff --git a/pkg/contextmenu/service_test.go b/pkg/contextmenu/service_test.go index 93dd8d3..edab171 100644 --- a/pkg/contextmenu/service_test.go +++ b/pkg/contextmenu/service_test.go @@ -171,7 +171,7 @@ func TestTaskRemove_Bad_NotFound(t *testing.T) { _, handled, err := c.PERFORM(TaskRemove{Name: "nonexistent"}) assert.True(t, handled) - assert.ErrorIs(t, err, ErrMenuNotFound) + assert.ErrorIs(t, err, ErrorMenuNotFound) } func TestQueryGet_Good(t *testing.T) { diff --git a/pkg/dialog/messages.go b/pkg/dialog/messages.go index 131592e..c274f2c 100644 --- a/pkg/dialog/messages.go +++ b/pkg/dialog/messages.go @@ -1,14 +1,9 @@ -// pkg/dialog/messages.go package dialog -// TaskOpenFile shows an open file dialog. Result: []string (paths) -type TaskOpenFile struct{ Opts OpenFileOptions } +type TaskOpenFile struct{ Options OpenFileOptions } -// TaskSaveFile shows a save file dialog. Result: string (path) -type TaskSaveFile struct{ Opts SaveFileOptions } +type TaskSaveFile struct{ Options SaveFileOptions } -// TaskOpenDirectory shows a directory picker. Result: string (path) -type TaskOpenDirectory struct{ Opts OpenDirectoryOptions } +type TaskOpenDirectory struct{ Options OpenDirectoryOptions } -// TaskMessageDialog shows a message dialog. Result: string (button clicked) -type TaskMessageDialog struct{ Opts MessageDialogOptions } +type TaskMessageDialog struct{ Options MessageDialogOptions } diff --git a/pkg/dialog/platform.go b/pkg/dialog/platform.go index 80b74d7..10585a4 100644 --- a/pkg/dialog/platform.go +++ b/pkg/dialog/platform.go @@ -3,10 +3,10 @@ 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) + OpenFile(options OpenFileOptions) ([]string, error) + SaveFile(options SaveFileOptions) (string, error) + OpenDirectory(options OpenDirectoryOptions) (string, error) + MessageDialog(options MessageDialogOptions) (string, error) } // DialogType represents the type of message dialog. diff --git a/pkg/dialog/service.go b/pkg/dialog/service.go index 231f3be..b9b23b5 100644 --- a/pkg/dialog/service.go +++ b/pkg/dialog/service.go @@ -7,16 +7,13 @@ import ( "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{ @@ -26,13 +23,11 @@ func Register(p Platform) func(*core.Core) (any, error) { } } -// 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 } @@ -40,16 +35,16 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { 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) + paths, err := s.platform.OpenFile(t.Options) return paths, true, err case TaskSaveFile: - path, err := s.platform.SaveFile(t.Opts) + path, err := s.platform.SaveFile(t.Options) return path, true, err case TaskOpenDirectory: - path, err := s.platform.OpenDirectory(t.Opts) + path, err := s.platform.OpenDirectory(t.Options) return path, true, err case TaskMessageDialog: - button, err := s.platform.MessageDialog(t.Opts) + button, err := s.platform.MessageDialog(t.Options) return button, true, err default: return nil, false, nil diff --git a/pkg/dialog/service_test.go b/pkg/dialog/service_test.go index 66fe760..de476da 100644 --- a/pkg/dialog/service_test.go +++ b/pkg/dialog/service_test.go @@ -11,18 +11,18 @@ import ( ) 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 + 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) { @@ -70,7 +70,7 @@ func TestTaskOpenFile_Good(t *testing.T) { mock.openFilePaths = []string{"/a.txt", "/b.txt"} result, handled, err := c.PERFORM(TaskOpenFile{ - Opts: OpenFileOptions{Title: "Pick", AllowMultiple: true}, + Options: OpenFileOptions{Title: "Pick", AllowMultiple: true}, }) require.NoError(t, err) assert.True(t, handled) @@ -83,7 +83,7 @@ func TestTaskOpenFile_Good(t *testing.T) { func TestTaskSaveFile_Good(t *testing.T) { _, c := newTestService(t) result, handled, err := c.PERFORM(TaskSaveFile{ - Opts: SaveFileOptions{Filename: "out.txt"}, + Options: SaveFileOptions{Filename: "out.txt"}, }) require.NoError(t, err) assert.True(t, handled) @@ -93,7 +93,7 @@ func TestTaskSaveFile_Good(t *testing.T) { func TestTaskOpenDirectory_Good(t *testing.T) { _, c := newTestService(t) result, handled, err := c.PERFORM(TaskOpenDirectory{ - Opts: OpenDirectoryOptions{Title: "Pick Dir"}, + Options: OpenDirectoryOptions{Title: "Pick Dir"}, }) require.NoError(t, err) assert.True(t, handled) @@ -105,7 +105,7 @@ func TestTaskMessageDialog_Good(t *testing.T) { mock.messageButton = "Yes" result, handled, err := c.PERFORM(TaskMessageDialog{ - Opts: MessageDialogOptions{ + Options: MessageDialogOptions{ Type: DialogQuestion, Title: "Confirm", Message: "Sure?", Buttons: []string{"Yes", "No"}, }, diff --git a/pkg/display/display.go b/pkg/display/display.go index 96e031d..3a51295 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -7,10 +7,10 @@ import ( "path/filepath" "runtime" - "forge.lthn.ai/core/config" - "forge.lthn.ai/core/go/pkg/core" - coreerr "forge.lthn.ai/core/go-log" "encoding/json" + "forge.lthn.ai/core/config" + coreerr "forge.lthn.ai/core/go-log" + "forge.lthn.ai/core/go/pkg/core" "forge.lthn.ai/core/gui/pkg/browser" "forge.lthn.ai/core/gui/pkg/contextmenu" @@ -41,10 +41,9 @@ type Service struct { *core.ServiceRuntime[Options] wailsApp *application.App app App - config Options configData map[string]map[string]any - cfg *config.Config // config instance for file persistence - events *WSEventManager + configFile *config.Config // config instance for file persistence + events *WSEventManager } // New is the constructor for the display service. @@ -117,7 +116,7 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { case window.ActionWindowResized: if s.events != nil { s.events.Emit(Event{Type: EventWindowResize, Window: m.Name, - Data: map[string]any{"w": m.W, "h": m.H}}) + Data: map[string]any{"w": m.Width, "h": m.Height}}) } case window.ActionWindowFocused: if s.events != nil { @@ -491,7 +490,7 @@ func (s *Service) handleTrayAction(actionID string) { details := fmt.Sprintf("OS: %s\nArch: %s\nPlatform: %s %s", info.OS, info.Arch, info.Platform.Name, info.Platform.Version) _, _, _ = s.Core().PERFORM(dialog.TaskMessageDialog{ - Opts: dialog.MessageDialogOptions{ + Options: dialog.MessageDialogOptions{ Type: dialog.DialogInfo, Title: "Environment", Message: details, Buttons: []string{"OK"}, }, @@ -513,23 +512,23 @@ func guiConfigPath() string { } func (s *Service) loadConfig() { - if s.cfg != nil { + if s.configFile != nil { return // Already loaded (e.g., via loadConfigFrom in tests) } s.loadConfigFrom(guiConfigPath()) } func (s *Service) loadConfigFrom(path string) { - cfg, err := config.New(config.WithPath(path)) + configFile, err := config.New(config.WithPath(path)) if err != nil { // Non-critical — continue with empty configData return } - s.cfg = cfg + s.configFile = configFile for _, section := range []string{"window", "systray", "menu"} { var data map[string]any - if err := cfg.Get(section, &data); err == nil && data != nil { + if err := configFile.Get(section, &data); err == nil && data != nil { s.configData[section] = data } } @@ -551,16 +550,16 @@ func (s *Service) handleConfigQuery(c *core.Core, q core.Query) (any, bool, erro func (s *Service) handleConfigTask(c *core.Core, t core.Task) (any, bool, error) { switch t := t.(type) { case window.TaskSaveConfig: - s.configData["window"] = t.Value - s.persistSection("window", t.Value) + s.configData["window"] = t.Config + s.persistSection("window", t.Config) return nil, true, nil case systray.TaskSaveConfig: - s.configData["systray"] = t.Value - s.persistSection("systray", t.Value) + s.configData["systray"] = t.Config + s.persistSection("systray", t.Config) return nil, true, nil case menu.TaskSaveConfig: - s.configData["menu"] = t.Value - s.persistSection("menu", t.Value) + s.configData["menu"] = t.Config + s.persistSection("menu", t.Config) return nil, true, nil default: return nil, false, nil @@ -568,11 +567,11 @@ func (s *Service) handleConfigTask(c *core.Core, t core.Task) (any, bool, error) } func (s *Service) persistSection(key string, value map[string]any) { - if s.cfg == nil { + if s.configFile == nil { return } - _ = s.cfg.Set(key, value) - _ = s.cfg.Commit() + _ = s.configFile.Set(key, value) + _ = s.configFile.Commit() } // --- Service accessors --- @@ -589,8 +588,8 @@ func (s *Service) windowService() *window.Service { // --- Window Management (delegates via IPC) --- // OpenWindow creates a new window via IPC. -func (s *Service) OpenWindow(opts ...window.WindowOption) error { - _, _, err := s.Core().PERFORM(window.TaskOpenWindow{Opts: opts}) +func (s *Service) OpenWindow(options ...window.WindowOption) error { + _, _, err := s.Core().PERFORM(window.TaskOpenWindow{Options: options}) return err } @@ -625,7 +624,7 @@ func (s *Service) SetWindowPosition(name string, x, y int) error { // SetWindowSize resizes a window via IPC. func (s *Service) SetWindowSize(name string, width, height int) error { - _, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, W: width, H: height}) + _, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, Width: width, Height: height}) return err } @@ -634,7 +633,7 @@ func (s *Service) SetWindowBounds(name string, x, y, width, height int) error { if _, _, err := s.Core().PERFORM(window.TaskSetPosition{Name: name, X: x, Y: y}); err != nil { return err } - _, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, W: width, H: height}) + _, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, Width: width, Height: height}) return err } @@ -815,17 +814,17 @@ type CreateWindowOptions struct { } // CreateWindow creates a new window with the specified options. -func (s *Service) CreateWindow(opts CreateWindowOptions) (*window.WindowInfo, error) { - if opts.Name == "" { +func (s *Service) CreateWindow(options CreateWindowOptions) (*window.WindowInfo, error) { + if options.Name == "" { return nil, coreerr.E("display.CreateWindow", "window name is required", nil) } result, _, err := s.Core().PERFORM(window.TaskOpenWindow{ - Opts: []window.WindowOption{ - window.WithName(opts.Name), - window.WithTitle(opts.Title), - window.WithURL(opts.URL), - window.WithSize(opts.Width, opts.Height), - window.WithPosition(opts.X, opts.Y), + Options: []window.WindowOption{ + window.WithName(options.Name), + window.WithTitle(options.Title), + window.WithURL(options.URL), + window.WithSize(options.Width, options.Height), + window.WithPosition(options.X, options.Y), }, }) if err != nil { @@ -994,7 +993,7 @@ func ptr[T any](v T) *T { return &v } func (s *Service) handleNewWorkspace() { _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ - Opts: []window.WindowOption{ + Options: []window.WindowOption{ window.WithName("workspace-new"), window.WithTitle("New Workspace"), window.WithURL("/workspace/new"), @@ -1017,7 +1016,7 @@ func (s *Service) handleListWorkspaces() { func (s *Service) handleNewFile() { _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ - Opts: []window.WindowOption{ + Options: []window.WindowOption{ window.WithName("editor"), window.WithTitle("New File - Editor"), window.WithURL("/#/developer/editor?new=true"), @@ -1028,7 +1027,7 @@ func (s *Service) handleNewFile() { func (s *Service) handleOpenFile() { result, handled, err := s.Core().PERFORM(dialog.TaskOpenFile{ - Opts: dialog.OpenFileOptions{ + Options: dialog.OpenFileOptions{ Title: "Open File", AllowMultiple: false, }, @@ -1041,7 +1040,7 @@ func (s *Service) handleOpenFile() { return } _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ - Opts: []window.WindowOption{ + Options: []window.WindowOption{ window.WithName("editor"), window.WithTitle(paths[0] + " - Editor"), window.WithURL("/#/developer/editor?file=" + paths[0]), @@ -1053,7 +1052,7 @@ func (s *Service) handleOpenFile() { func (s *Service) handleSaveFile() { _ = s.Core().ACTION(ActionIDECommand{Command: "save"}) } func (s *Service) handleOpenEditor() { _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ - Opts: []window.WindowOption{ + Options: []window.WindowOption{ window.WithName("editor"), window.WithTitle("Editor"), window.WithURL("/#/developer/editor"), @@ -1063,7 +1062,7 @@ func (s *Service) handleOpenEditor() { } func (s *Service) handleOpenTerminal() { _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ - Opts: []window.WindowOption{ + Options: []window.WindowOption{ window.WithName("terminal"), window.WithTitle("Terminal"), window.WithURL("/#/developer/terminal"), diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index 03eb1d2..0c49729 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -104,7 +104,7 @@ func TestConfigTask_Good(t *testing.T) { _, c := newTestDisplayService(t) newCfg := map[string]any{"default_width": 800} - _, handled, err := c.PERFORM(window.TaskSaveConfig{Value: newCfg}) + _, handled, err := c.PERFORM(window.TaskSaveConfig{Config: newCfg}) require.NoError(t, err) assert.True(t, handled) @@ -121,7 +121,7 @@ func TestServiceConclave_Good(t *testing.T) { // Open a window via IPC result, handled, err := c.PERFORM(window.TaskOpenWindow{ - Opts: []window.WindowOption{window.WithName("main")}, + Options: []window.WindowOption{window.WithName("main")}, }) require.NoError(t, err) assert.True(t, handled) @@ -413,7 +413,7 @@ 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{ - Opts: []window.WindowOption{window.WithName("test")}, + Options: []window.WindowOption{window.WithName("test")}, }) require.NoError(t, err) assert.True(t, handled) @@ -493,7 +493,7 @@ func TestHandleConfigTask_Persists_Good(t *testing.T) { c.ServiceStartup(context.Background(), nil) _, handled, err := c.PERFORM(window.TaskSaveConfig{ - Value: map[string]any{"default_width": 1920}, + Config: map[string]any{"default_width": 1920}, }) require.NoError(t, err) assert.True(t, handled) diff --git a/pkg/dock/register.go b/pkg/dock/register.go index 1123927..96ec94d 100644 --- a/pkg/dock/register.go +++ b/pkg/dock/register.go @@ -1,10 +1,7 @@ -// 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{ diff --git a/pkg/dock/service.go b/pkg/dock/service.go index 260ff0a..346ef95 100644 --- a/pkg/dock/service.go +++ b/pkg/dock/service.go @@ -1,4 +1,3 @@ -// pkg/dock/service.go package dock import ( @@ -7,30 +6,23 @@ import ( "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: @@ -40,8 +32,6 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { } } -// --- Task Handlers --- - func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { switch t := t.(type) { case TaskShowIcon: diff --git a/pkg/keybinding/messages.go b/pkg/keybinding/messages.go index 7f037f3..a168806 100644 --- a/pkg/keybinding/messages.go +++ b/pkg/keybinding/messages.go @@ -1,40 +1,25 @@ -// pkg/keybinding/messages.go package keybinding import "errors" -// ErrAlreadyRegistered is returned when attempting to add a binding -// that already exists. Callers must TaskRemove first to rebind. -var ErrAlreadyRegistered = errors.New("keybinding: accelerator already registered") +var ErrorAlreadyRegistered = errors.New("keybinding: accelerator already registered") -// BindingInfo describes a registered keyboard shortcut. type BindingInfo struct { Accelerator string `json:"accelerator"` Description string `json:"description"` } -// --- Queries --- - -// QueryList returns all registered bindings. Result: []BindingInfo type QueryList struct{} -// --- Tasks --- - -// TaskAdd registers a new keyboard shortcut. Result: nil -// Returns ErrAlreadyRegistered if the accelerator is already bound. type TaskAdd struct { Accelerator string `json:"accelerator"` Description string `json:"description"` } -// TaskRemove unregisters a keyboard shortcut. Result: nil type TaskRemove struct { Accelerator string `json:"accelerator"` } -// --- Actions --- - -// ActionTriggered is broadcast when a registered shortcut is activated. type ActionTriggered struct { Accelerator string `json:"accelerator"` } diff --git a/pkg/keybinding/register.go b/pkg/keybinding/register.go index 417819e..091cbfa 100644 --- a/pkg/keybinding/register.go +++ b/pkg/keybinding/register.go @@ -1,16 +1,13 @@ -// pkg/keybinding/register.go package keybinding 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, - bindings: make(map[string]BindingInfo), + ServiceRuntime: core.NewServiceRuntime[Options](c, Options{}), + platform: p, + registeredBindings: make(map[string]BindingInfo), }, nil } } diff --git a/pkg/keybinding/service.go b/pkg/keybinding/service.go index 8461292..3afd23b 100644 --- a/pkg/keybinding/service.go +++ b/pkg/keybinding/service.go @@ -8,26 +8,20 @@ import ( "forge.lthn.ai/core/go/pkg/core" ) -// Options holds configuration for the keybinding service. type Options struct{} -// Service is a core.Service managing keyboard shortcuts via IPC. -// It maintains an in-memory registry of bindings and delegates -// platform-level registration to the Platform interface. type Service struct { *core.ServiceRuntime[Options] - platform Platform - bindings map[string]BindingInfo + platform Platform + registeredBindings map[string]BindingInfo } -// 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 } @@ -43,10 +37,9 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { } } -// queryList reads from the in-memory registry (not platform.GetAll()). func (s *Service) queryList() []BindingInfo { - result := make([]BindingInfo, 0, len(s.bindings)) - for _, info := range s.bindings { + result := make([]BindingInfo, 0, len(s.registeredBindings)) + for _, info := range s.registeredBindings { result = append(result, info) } return result @@ -66,8 +59,8 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { } func (s *Service) taskAdd(t TaskAdd) error { - if _, exists := s.bindings[t.Accelerator]; exists { - return ErrAlreadyRegistered + if _, exists := s.registeredBindings[t.Accelerator]; exists { + return ErrorAlreadyRegistered } // Register on platform with a callback that broadcasts ActionTriggered @@ -78,7 +71,7 @@ func (s *Service) taskAdd(t TaskAdd) error { return coreerr.E("keybinding.taskAdd", "platform add failed", err) } - s.bindings[t.Accelerator] = BindingInfo{ + s.registeredBindings[t.Accelerator] = BindingInfo{ Accelerator: t.Accelerator, Description: t.Description, } @@ -86,7 +79,7 @@ func (s *Service) taskAdd(t TaskAdd) error { } func (s *Service) taskRemove(t TaskRemove) error { - if _, exists := s.bindings[t.Accelerator]; !exists { + if _, exists := s.registeredBindings[t.Accelerator]; !exists { return coreerr.E("keybinding.taskRemove", "not registered: "+t.Accelerator, nil) } @@ -95,6 +88,6 @@ func (s *Service) taskRemove(t TaskRemove) error { return coreerr.E("keybinding.taskRemove", "platform remove failed", err) } - delete(s.bindings, t.Accelerator) + delete(s.registeredBindings, t.Accelerator) return nil } diff --git a/pkg/keybinding/service_test.go b/pkg/keybinding/service_test.go index 14749f2..b586e07 100644 --- a/pkg/keybinding/service_test.go +++ b/pkg/keybinding/service_test.go @@ -99,7 +99,7 @@ func TestTaskAdd_Bad_Duplicate(t *testing.T) { // Second add with same accelerator should fail _, handled, err := c.PERFORM(TaskAdd{Accelerator: "Ctrl+S", Description: "Save Again"}) assert.True(t, handled) - assert.ErrorIs(t, err, ErrAlreadyRegistered) + assert.ErrorIs(t, err, ErrorAlreadyRegistered) } func TestTaskRemove_Good(t *testing.T) { diff --git a/pkg/lifecycle/register.go b/pkg/lifecycle/register.go index 90e5d40..fcf43ea 100644 --- a/pkg/lifecycle/register.go +++ b/pkg/lifecycle/register.go @@ -1,10 +1,7 @@ -// pkg/lifecycle/register.go package lifecycle 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{ diff --git a/pkg/lifecycle/service.go b/pkg/lifecycle/service.go index 41e7ca8..3ba4255 100644 --- a/pkg/lifecycle/service.go +++ b/pkg/lifecycle/service.go @@ -1,4 +1,3 @@ -// pkg/lifecycle/service.go package lifecycle import ( @@ -7,22 +6,15 @@ import ( "forge.lthn.ai/core/go/pkg/core" ) -// Options holds configuration for the lifecycle service. type Options struct{} -// Service is a core.Service that registers platform lifecycle callbacks -// and broadcasts corresponding IPC Actions. It implements both Startable -// and Stoppable: OnStartup registers all callbacks, OnShutdown cancels them. type Service struct { *core.ServiceRuntime[Options] platform Platform cancels []func() } -// OnStartup registers a platform callback for each EventType and for file-open. -// Each callback broadcasts the corresponding Action via s.Core().ACTION(). func (s *Service) OnStartup(ctx context.Context) error { - // Register fire-and-forget event callbacks eventActions := map[EventType]func(){ EventApplicationStarted: func() { _ = s.Core().ACTION(ActionApplicationStarted{}) }, EventWillTerminate: func() { _ = s.Core().ACTION(ActionWillTerminate{}) }, @@ -38,7 +30,6 @@ func (s *Service) OnStartup(ctx context.Context) error { s.cancels = append(s.cancels, cancel) } - // Register file-open callback (carries data) cancel := s.platform.OnOpenedWithFile(func(path string) { _ = s.Core().ACTION(ActionOpenedWithFile{Path: path}) }) @@ -47,7 +38,6 @@ func (s *Service) OnStartup(ctx context.Context) error { return nil } -// OnShutdown cancels all registered platform callbacks. func (s *Service) OnShutdown(ctx context.Context) error { for _, cancel := range s.cancels { cancel() @@ -56,8 +46,6 @@ func (s *Service) OnShutdown(ctx context.Context) error { return nil } -// HandleIPCEvents is auto-discovered and registered by core.WithService. -// Lifecycle events are all outbound (platform -> IPC) so there is nothing to handle here. func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { return nil } diff --git a/pkg/mcp/tools_dialog.go b/pkg/mcp/tools_dialog.go index bfc6015..aee701d 100644 --- a/pkg/mcp/tools_dialog.go +++ b/pkg/mcp/tools_dialog.go @@ -4,8 +4,8 @@ package mcp import ( "context" - "forge.lthn.ai/core/gui/pkg/dialog" coreerr "forge.lthn.ai/core/go-log" + "forge.lthn.ai/core/gui/pkg/dialog" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -22,7 +22,7 @@ type DialogOpenFileOutput struct { } func (s *Subsystem) dialogOpenFile(_ context.Context, _ *mcp.CallToolRequest, input DialogOpenFileInput) (*mcp.CallToolResult, DialogOpenFileOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskOpenFile{Opts: dialog.OpenFileOptions{ + result, _, err := s.core.PERFORM(dialog.TaskOpenFile{Options: dialog.OpenFileOptions{ Title: input.Title, Directory: input.Directory, Filters: input.Filters, @@ -51,7 +51,7 @@ type DialogSaveFileOutput struct { } func (s *Subsystem) dialogSaveFile(_ context.Context, _ *mcp.CallToolRequest, input DialogSaveFileInput) (*mcp.CallToolResult, DialogSaveFileOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskSaveFile{Opts: dialog.SaveFileOptions{ + result, _, err := s.core.PERFORM(dialog.TaskSaveFile{Options: dialog.SaveFileOptions{ Title: input.Title, Directory: input.Directory, Filename: input.Filename, @@ -78,7 +78,7 @@ type DialogOpenDirectoryOutput struct { } func (s *Subsystem) dialogOpenDirectory(_ context.Context, _ *mcp.CallToolRequest, input DialogOpenDirectoryInput) (*mcp.CallToolResult, DialogOpenDirectoryOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskOpenDirectory{Opts: dialog.OpenDirectoryOptions{ + result, _, err := s.core.PERFORM(dialog.TaskOpenDirectory{Options: dialog.OpenDirectoryOptions{ Title: input.Title, Directory: input.Directory, }}) @@ -104,7 +104,7 @@ type DialogConfirmOutput struct { } func (s *Subsystem) dialogConfirm(_ context.Context, _ *mcp.CallToolRequest, input DialogConfirmInput) (*mcp.CallToolResult, DialogConfirmOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskMessageDialog{Opts: dialog.MessageDialogOptions{ + result, _, err := s.core.PERFORM(dialog.TaskMessageDialog{Options: dialog.MessageDialogOptions{ Type: dialog.DialogQuestion, Title: input.Title, Message: input.Message, @@ -131,7 +131,7 @@ type DialogPromptOutput struct { } func (s *Subsystem) dialogPrompt(_ context.Context, _ *mcp.CallToolRequest, input DialogPromptInput) (*mcp.CallToolResult, DialogPromptOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskMessageDialog{Opts: dialog.MessageDialogOptions{ + result, _, err := s.core.PERFORM(dialog.TaskMessageDialog{Options: dialog.MessageDialogOptions{ Type: dialog.DialogInfo, Title: input.Title, Message: input.Message, diff --git a/pkg/mcp/tools_notification.go b/pkg/mcp/tools_notification.go index 0629054..0b965d3 100644 --- a/pkg/mcp/tools_notification.go +++ b/pkg/mcp/tools_notification.go @@ -21,7 +21,7 @@ type NotificationShowOutput struct { } func (s *Subsystem) notificationShow(_ context.Context, _ *mcp.CallToolRequest, input NotificationShowInput) (*mcp.CallToolResult, NotificationShowOutput, error) { - _, _, err := s.core.PERFORM(notification.TaskSend{Opts: notification.NotificationOptions{ + _, _, err := s.core.PERFORM(notification.TaskSend{Options: notification.NotificationOptions{ Title: input.Title, Message: input.Message, Subtitle: input.Subtitle, diff --git a/pkg/mcp/tools_window.go b/pkg/mcp/tools_window.go index 16484d9..b5eeb7b 100644 --- a/pkg/mcp/tools_window.go +++ b/pkg/mcp/tools_window.go @@ -89,22 +89,22 @@ type WindowCreateOutput struct { } func (s *Subsystem) windowCreate(_ context.Context, _ *mcp.CallToolRequest, input WindowCreateInput) (*mcp.CallToolResult, WindowCreateOutput, error) { - opts := []window.WindowOption{ + options := []window.WindowOption{ window.WithName(input.Name), } if input.Title != "" { - opts = append(opts, window.WithTitle(input.Title)) + options = append(options, window.WithTitle(input.Title)) } if input.URL != "" { - opts = append(opts, window.WithURL(input.URL)) + options = append(options, window.WithURL(input.URL)) } if input.Width > 0 || input.Height > 0 { - opts = append(opts, window.WithSize(input.Width, input.Height)) + options = append(options, window.WithSize(input.Width, input.Height)) } if input.X != 0 || input.Y != 0 { - opts = append(opts, window.WithPosition(input.X, input.Y)) + options = append(options, window.WithPosition(input.X, input.Y)) } - result, _, err := s.core.PERFORM(window.TaskOpenWindow{Opts: opts}) + result, _, err := s.core.PERFORM(window.TaskOpenWindow{Options: options}) if err != nil { return nil, WindowCreateOutput{}, err } @@ -163,7 +163,7 @@ 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, W: input.Width, H: input.Height}) + _, _, err := s.core.PERFORM(window.TaskSetSize{Name: input.Name, Width: input.Width, Height: input.Height}) if err != nil { return nil, WindowSizeOutput{}, err } @@ -188,7 +188,7 @@ func (s *Subsystem) windowBounds(_ context.Context, _ *mcp.CallToolRequest, inpu if err != nil { return nil, WindowBoundsOutput{}, err } - _, _, err = s.core.PERFORM(window.TaskSetSize{Name: input.Name, W: input.Width, H: input.Height}) + _, _, err = s.core.PERFORM(window.TaskSetSize{Name: input.Name, Width: input.Width, Height: input.Height}) if err != nil { return nil, WindowBoundsOutput{}, err } diff --git a/pkg/menu/messages.go b/pkg/menu/messages.go index 61c8de5..55aed57 100644 --- a/pkg/menu/messages.go +++ b/pkg/menu/messages.go @@ -1,16 +1,9 @@ package menu -// QueryConfig requests this service's config section from the display orchestrator. -// Result: map[string]any type QueryConfig struct{} -// QueryGetAppMenu returns the current app menu item descriptors. -// Result: []MenuItem type QueryGetAppMenu struct{} -// TaskSetAppMenu sets the application menu. OnClick closures work because -// core/go IPC is in-process (no serialisation boundary). type TaskSetAppMenu struct{ Items []MenuItem } -// TaskSaveConfig persists this service's config section via the display orchestrator. -type TaskSaveConfig struct{ Value map[string]any } +type TaskSaveConfig struct{ Config map[string]any } diff --git a/pkg/menu/register.go b/pkg/menu/register.go index acb4b88..59dbae8 100644 --- a/pkg/menu/register.go +++ b/pkg/menu/register.go @@ -2,7 +2,6 @@ package menu import "forge.lthn.ai/core/go/pkg/core" -// 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{ diff --git a/pkg/menu/service.go b/pkg/menu/service.go index 2e8ac26..1a3f838 100644 --- a/pkg/menu/service.go +++ b/pkg/menu/service.go @@ -6,24 +6,21 @@ import ( "forge.lthn.ai/core/go/pkg/core" ) -// Options holds configuration for the menu service. type Options struct{} -// Service is a core.Service managing application menus via IPC. type Service struct { *core.ServiceRuntime[Options] manager *Manager platform Platform - items []MenuItem // last-set menu items for QueryGetAppMenu + menuItems []MenuItem showDevTools bool } -// OnStartup queries config and registers IPC handlers. func (s *Service) OnStartup(ctx context.Context) error { - cfg, handled, _ := s.Core().QUERY(QueryConfig{}) + configValue, handled, _ := s.Core().QUERY(QueryConfig{}) if handled { - if mCfg, ok := cfg.(map[string]any); ok { - s.applyConfig(mCfg) + if menuConfig, ok := configValue.(map[string]any); ok { + s.applyConfig(menuConfig) } } s.Core().RegisterQuery(s.handleQuery) @@ -31,20 +28,18 @@ func (s *Service) OnStartup(ctx context.Context) error { return nil } -func (s *Service) applyConfig(cfg map[string]any) { - if v, ok := cfg["show_dev_tools"]; ok { +func (s *Service) applyConfig(configData map[string]any) { + if v, ok := configData["show_dev_tools"]; ok { if show, ok := v.(bool); ok { s.showDevTools = show } } } -// ShowDevTools returns whether developer tools menu items should be shown. func (s *Service) ShowDevTools() bool { return s.showDevTools } -// HandleIPCEvents is auto-discovered and registered by core.WithService. func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { return nil } @@ -52,7 +47,7 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { switch q.(type) { case QueryGetAppMenu: - return s.items, true, nil + return s.menuItems, true, nil default: return nil, false, nil } @@ -61,7 +56,7 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { switch t := t.(type) { case TaskSetAppMenu: - s.items = t.Items + s.menuItems = t.Items s.manager.SetApplicationMenu(t.Items) return nil, true, nil default: @@ -69,7 +64,6 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { } } -// Manager returns the underlying menu Manager. func (s *Service) Manager() *Manager { return s.manager } diff --git a/pkg/notification/messages.go b/pkg/notification/messages.go index e0df1ea..1cc10f9 100644 --- a/pkg/notification/messages.go +++ b/pkg/notification/messages.go @@ -1,14 +1,9 @@ -// pkg/notification/messages.go package notification -// QueryPermission checks notification authorisation. Result: PermissionStatus type QueryPermission struct{} -// TaskSend sends a notification. Falls back to dialog if platform fails. -type TaskSend struct{ Opts NotificationOptions } +type TaskSend struct{ Options NotificationOptions } -// TaskRequestPermission requests notification authorisation. Result: bool (granted) type TaskRequestPermission struct{} -// ActionNotificationClicked is broadcast when a notification is clicked (future). type ActionNotificationClicked struct{ ID string } diff --git a/pkg/notification/platform.go b/pkg/notification/platform.go index f0d9963..954a5af 100644 --- a/pkg/notification/platform.go +++ b/pkg/notification/platform.go @@ -3,7 +3,7 @@ package notification // Platform abstracts the native notification backend. type Platform interface { - Send(opts NotificationOptions) error + Send(options NotificationOptions) error RequestPermission() (bool, error) CheckPermission() (bool, error) } diff --git a/pkg/notification/service.go b/pkg/notification/service.go index df43b6d..a98774a 100644 --- a/pkg/notification/service.go +++ b/pkg/notification/service.go @@ -10,16 +10,13 @@ import ( "forge.lthn.ai/core/gui/pkg/dialog" ) -// Options holds configuration for the notification service. type Options struct{} -// Service is a core.Service managing notifications 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{ @@ -29,14 +26,12 @@ func Register(p Platform) func(*core.Core) (any, error) { } } -// 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 } @@ -54,7 +49,7 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { 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.Opts) + return nil, true, s.send(t.Options) case TaskRequestPermission: granted, err := s.platform.RequestPermission() return granted, true, err @@ -64,24 +59,24 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { } // send attempts native notification, falls back to dialog via IPC. -func (s *Service) send(opts NotificationOptions) error { +func (s *Service) send(options NotificationOptions) error { // Generate ID if not provided - if opts.ID == "" { - opts.ID = fmt.Sprintf("core-%d", time.Now().UnixNano()) + if options.ID == "" { + options.ID = fmt.Sprintf("core-%d", time.Now().UnixNano()) } - if err := s.platform.Send(opts); err != nil { + if err := s.platform.Send(options); err != nil { // Fallback: show as dialog via IPC - return s.fallbackDialog(opts) + return s.fallbackDialog(options) } return nil } // fallbackDialog shows a dialog via IPC when native notifications fail. -func (s *Service) fallbackDialog(opts NotificationOptions) error { +func (s *Service) fallbackDialog(options NotificationOptions) error { // Map severity to dialog type var dt dialog.DialogType - switch opts.Severity { + switch options.Severity { case SeverityWarning: dt = dialog.DialogWarning case SeverityError: @@ -90,15 +85,15 @@ func (s *Service) fallbackDialog(opts NotificationOptions) error { dt = dialog.DialogInfo } - msg := opts.Message - if opts.Subtitle != "" { - msg = opts.Subtitle + "\n\n" + msg + msg := options.Message + if options.Subtitle != "" { + msg = options.Subtitle + "\n\n" + msg } _, _, err := s.Core().PERFORM(dialog.TaskMessageDialog{ - Opts: dialog.MessageDialogOptions{ + Options: dialog.MessageDialogOptions{ Type: dt, - Title: opts.Title, + Title: options.Title, Message: msg, Buttons: []string{"OK"}, }, diff --git a/pkg/notification/service_test.go b/pkg/notification/service_test.go index 33db648..8689ddf 100644 --- a/pkg/notification/service_test.go +++ b/pkg/notification/service_test.go @@ -66,7 +66,7 @@ func TestRegister_Good(t *testing.T) { func TestTaskSend_Good(t *testing.T) { mock, c := newTestService(t) _, handled, err := c.PERFORM(TaskSend{ - Opts: NotificationOptions{Title: "Test", Message: "Hello"}, + Options: NotificationOptions{Title: "Test", Message: "Hello"}, }) require.NoError(t, err) assert.True(t, handled) @@ -87,7 +87,7 @@ func TestTaskSend_Fallback_Good(t *testing.T) { require.NoError(t, c.ServiceStartup(context.Background(), nil)) _, handled, err := c.PERFORM(TaskSend{ - Opts: NotificationOptions{Title: "Warn", Message: "Oops", Severity: SeverityWarning}, + Options: NotificationOptions{Title: "Warn", Message: "Oops", Severity: SeverityWarning}, }) assert.True(t, handled) assert.NoError(t, err) // fallback succeeds even though platform failed diff --git a/pkg/systray/messages.go b/pkg/systray/messages.go index 4fc5bfe..6855e22 100644 --- a/pkg/systray/messages.go +++ b/pkg/systray/messages.go @@ -1,30 +1,17 @@ package systray -// QueryConfig requests this service's config section from the display orchestrator. -// Result: map[string]any type QueryConfig struct{} -// --- Tasks --- - -// TaskSetTrayIcon sets the tray icon. type TaskSetTrayIcon struct{ Data []byte } -// TaskSetTrayMenu sets the tray menu items. type TaskSetTrayMenu struct{ Items []TrayMenuItem } -// TaskShowPanel shows the tray panel window. type TaskShowPanel struct{} -// TaskHidePanel hides the tray panel window. type TaskHidePanel struct{} -// TaskSaveConfig persists this service's config section via the display orchestrator. -type TaskSaveConfig struct{ Value map[string]any } +type TaskSaveConfig struct{ Config map[string]any } -// --- Actions --- - -// ActionTrayClicked is broadcast when the tray icon is clicked. type ActionTrayClicked struct{} -// ActionTrayMenuItemClicked is broadcast when a tray menu item is clicked. type ActionTrayMenuItemClicked struct{ ActionID string } diff --git a/pkg/systray/register.go b/pkg/systray/register.go index b4d133b..055f35c 100644 --- a/pkg/systray/register.go +++ b/pkg/systray/register.go @@ -2,7 +2,6 @@ package systray import "forge.lthn.ai/core/go/pkg/core" -// 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{ diff --git a/pkg/systray/service.go b/pkg/systray/service.go index 70eaa04..f585e7e 100644 --- a/pkg/systray/service.go +++ b/pkg/systray/service.go @@ -6,10 +6,8 @@ import ( "forge.lthn.ai/core/go/pkg/core" ) -// Options holds configuration for the systray service. type Options struct{} -// Service is a core.Service managing the system tray via IPC. type Service struct { *core.ServiceRuntime[Options] manager *Manager @@ -17,33 +15,31 @@ type Service struct { iconPath string } -// OnStartup queries config and registers IPC handlers. func (s *Service) OnStartup(ctx context.Context) error { - cfg, handled, _ := s.Core().QUERY(QueryConfig{}) + configValue, handled, _ := s.Core().QUERY(QueryConfig{}) if handled { - if tCfg, ok := cfg.(map[string]any); ok { - s.applyConfig(tCfg) + if trayConfig, ok := configValue.(map[string]any); ok { + s.applyConfig(trayConfig) } } s.Core().RegisterTask(s.handleTask) return nil } -func (s *Service) applyConfig(cfg map[string]any) { - tooltip, _ := cfg["tooltip"].(string) +func (s *Service) applyConfig(configData map[string]any) { + tooltip, _ := configData["tooltip"].(string) if tooltip == "" { tooltip = "Core" } _ = s.manager.Setup(tooltip, tooltip) - if iconPath, ok := cfg["icon"].(string); ok && iconPath != "" { + if iconPath, ok := configData["icon"].(string); ok && iconPath != "" { // Icon loading is deferred to when assets are available. // Store the path for later use. s.iconPath = iconPath } } -// HandleIPCEvents is auto-discovered and registered by core.WithService. func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { return nil } @@ -78,7 +74,6 @@ func (s *Service) taskSetTrayMenu(t TaskSetTrayMenu) error { return s.manager.SetMenu(t.Items) } -// Manager returns the underlying systray Manager. func (s *Service) Manager() *Manager { return s.manager } diff --git a/pkg/webview/service.go b/pkg/webview/service.go index 6713174..edd6fdc 100644 --- a/pkg/webview/service.go +++ b/pkg/webview/service.go @@ -47,7 +47,7 @@ type Options struct { // Service is a core.Service managing webview interactions via IPC. type Service struct { *core.ServiceRuntime[Options] - opts Options + options Options connections map[string]connector mu sync.RWMutex newConn func(debugURL, windowName string) (connector, error) // injectable for tests @@ -55,19 +55,19 @@ type Service struct { } // Register creates a factory closure with the given options. -func Register(opts ...func(*Options)) func(*core.Core) (any, error) { +func Register(optionFns ...func(*Options)) func(*core.Core) (any, error) { o := Options{ DebugURL: "http://localhost:9222", Timeout: 30 * time.Second, ConsoleLimit: 1000, } - for _, fn := range opts { + for _, fn := range optionFns { fn(&o) } return func(c *core.Core) (any, error) { svc := &Service{ ServiceRuntime: core.NewServiceRuntime[Options](c, o), - opts: o, + options: o, connections: make(map[string]connector), newConn: defaultNewConn(o), } @@ -77,7 +77,7 @@ func Register(opts ...func(*Options)) func(*core.Core) (any, error) { } // defaultNewConn creates real go-webview connections. -func defaultNewConn(opts Options) func(string, string) (connector, error) { +func defaultNewConn(options Options) func(string, string) (connector, error) { return func(debugURL, windowName string) (connector, error) { // Enumerate targets, match by title/URL containing window name targets, err := gowebview.ListTargets(debugURL) @@ -105,8 +105,8 @@ func defaultNewConn(opts Options) func(string, string) (connector, error) { } wv, err := gowebview.New( gowebview.WithDebugURL(debugURL), - gowebview.WithTimeout(opts.Timeout), - gowebview.WithConsoleLimit(opts.ConsoleLimit), + gowebview.WithTimeout(options.Timeout), + gowebview.WithConsoleLimit(options.ConsoleLimit), ) if err != nil { return nil, err @@ -201,7 +201,7 @@ func (s *Service) getConn(windowName string) (connector, error) { if conn, ok := s.connections[windowName]; ok { return conn, nil } - conn, err := s.newConn(s.opts.DebugURL, windowName) + conn, err := s.newConn(s.options.DebugURL, windowName) if err != nil { return nil, err } @@ -373,17 +373,17 @@ type realConnector struct { wv *gowebview.Webview } -func (r *realConnector) Navigate(url string) error { return r.wv.Navigate(url) } -func (r *realConnector) Click(sel string) error { return r.wv.Click(sel) } -func (r *realConnector) Type(sel, text string) error { return r.wv.Type(sel, text) } -func (r *realConnector) Evaluate(script string) (any, error) { return r.wv.Evaluate(script) } -func (r *realConnector) Screenshot() ([]byte, error) { return r.wv.Screenshot() } -func (r *realConnector) GetURL() (string, error) { return r.wv.GetURL() } -func (r *realConnector) GetTitle() (string, error) { return r.wv.GetTitle() } -func (r *realConnector) GetHTML(sel string) (string, error) { return r.wv.GetHTML(sel) } -func (r *realConnector) ClearConsole() { r.wv.ClearConsole() } -func (r *realConnector) Close() error { return r.wv.Close() } -func (r *realConnector) SetViewport(w, h int) error { return r.wv.SetViewport(w, h) } +func (r *realConnector) Navigate(url string) error { return r.wv.Navigate(url) } +func (r *realConnector) Click(sel string) error { return r.wv.Click(sel) } +func (r *realConnector) Type(sel, text string) error { return r.wv.Type(sel, text) } +func (r *realConnector) Evaluate(script string) (any, error) { return r.wv.Evaluate(script) } +func (r *realConnector) Screenshot() ([]byte, error) { return r.wv.Screenshot() } +func (r *realConnector) GetURL() (string, error) { return r.wv.GetURL() } +func (r *realConnector) GetTitle() (string, error) { return r.wv.GetTitle() } +func (r *realConnector) GetHTML(sel string) (string, error) { return r.wv.GetHTML(sel) } +func (r *realConnector) ClearConsole() { r.wv.ClearConsole() } +func (r *realConnector) Close() error { return r.wv.Close() } +func (r *realConnector) SetViewport(w, h int) error { return r.wv.SetViewport(w, h) } func (r *realConnector) UploadFile(sel string, p []string) error { return r.wv.UploadFile(sel, p) } func (r *realConnector) Hover(sel string) error { diff --git a/pkg/window/messages.go b/pkg/window/messages.go index b5d1a13..ece680a 100644 --- a/pkg/window/messages.go +++ b/pkg/window/messages.go @@ -1,6 +1,5 @@ package window -// WindowInfo contains information about a window. type WindowInfo struct { Name string `json:"name"` Title string `json:"title"` @@ -12,103 +11,70 @@ type WindowInfo struct { Focused bool `json:"focused"` } -// --- Queries (read-only) --- - -// QueryWindowList returns all tracked windows. Result: []WindowInfo type QueryWindowList struct{} -// QueryWindowByName returns a single window by name. Result: *WindowInfo (nil if not found) type QueryWindowByName struct{ Name string } -// QueryConfig requests this service's config section from the display orchestrator. -// Result: map[string]any type QueryConfig struct{} -// --- Tasks (side-effects) --- +type TaskOpenWindow struct{ Options []WindowOption } -// TaskOpenWindow creates a new window. Result: WindowInfo -type TaskOpenWindow struct{ Opts []WindowOption } - -// TaskCloseWindow closes a window. Handler persists state BEFORE emitting ActionWindowClosed. type TaskCloseWindow struct{ Name string } -// TaskSetPosition moves a window. type TaskSetPosition struct { Name string X, Y int } -// TaskSetSize resizes a window. type TaskSetSize struct { - Name string - W, H int + Name string + Width, Height int } -// TaskMaximise maximises a window. type TaskMaximise struct{ Name string } -// TaskMinimise minimises a window. type TaskMinimise struct{ Name string } -// TaskFocus brings a window to the front. type TaskFocus struct{ Name string } -// TaskRestore restores a maximised or minimised window to its normal state. type TaskRestore struct{ Name string } -// TaskSetTitle changes a window's title. type TaskSetTitle struct { Name string Title string } -// TaskSetVisibility shows or hides a window. type TaskSetVisibility struct { Name string Visible bool } -// TaskFullscreen enters or exits fullscreen mode. type TaskFullscreen struct { Name string Fullscreen bool } -// --- Layout Queries --- - -// QueryLayoutList returns summaries of all saved layouts. Result: []LayoutInfo type QueryLayoutList struct{} -// QueryLayoutGet returns a layout by name. Result: *Layout (nil if not found) type QueryLayoutGet struct{ Name string } -// --- Layout Tasks --- - -// TaskSaveLayout saves the current window arrangement as a named layout. Result: bool type TaskSaveLayout struct{ Name string } -// TaskRestoreLayout restores a saved layout by name. type TaskRestoreLayout struct{ Name string } -// TaskDeleteLayout removes a saved layout by name. type TaskDeleteLayout struct{ Name string } -// TaskTileWindows arranges windows in a tiling mode. type TaskTileWindows struct { Mode string // "left-right", "grid", "left-half", "right-half", etc. Windows []string // window names; empty = all } -// TaskSnapWindow snaps a window to a screen edge/corner. type TaskSnapWindow struct { Name string // window name Position string // "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right", "center" } -// TaskSaveConfig persists this service's config section via the display orchestrator. -type TaskSaveConfig struct{ Value map[string]any } - -// --- Actions (broadcasts) --- +type TaskSaveConfig struct{ Config map[string]any } type ActionWindowOpened struct{ Name string } type ActionWindowClosed struct{ Name string } @@ -119,15 +85,15 @@ type ActionWindowMoved struct { } type ActionWindowResized struct { - Name string - W, H int + Name string + Width, Height int } type ActionWindowFocused struct{ Name string } type ActionWindowBlurred struct{ Name string } type ActionFilesDropped struct { - Name string `json:"name"` // window name + Name string `json:"name"` // window name Paths []string `json:"paths"` TargetID string `json:"targetId,omitempty"` } diff --git a/pkg/window/mock_platform.go b/pkg/window/mock_platform.go index 9dde9a6..1d3176c 100644 --- a/pkg/window/mock_platform.go +++ b/pkg/window/mock_platform.go @@ -10,11 +10,11 @@ func NewMockPlatform() *MockPlatform { return &MockPlatform{} } -func (m *MockPlatform) CreateWindow(opts PlatformWindowOptions) PlatformWindow { +func (m *MockPlatform) CreateWindow(options PlatformWindowOptions) PlatformWindow { w := &MockWindow{ - name: opts.Name, title: opts.Title, url: opts.URL, - width: opts.Width, height: opts.Height, - x: opts.X, y: opts.Y, + name: options.Name, title: options.Title, url: options.URL, + width: options.Width, height: options.Height, + x: options.X, y: options.Y, } m.Windows = append(m.Windows, w) return w @@ -38,28 +38,30 @@ type MockWindow struct { fileDropHandlers []func(paths []string, targetID string) } -func (w *MockWindow) Name() string { return w.name } -func (w *MockWindow) Title() string { return w.title } -func (w *MockWindow) Position() (int, int) { return w.x, w.y } -func (w *MockWindow) Size() (int, int) { return w.width, w.height } -func (w *MockWindow) IsMaximised() bool { return w.maximised } -func (w *MockWindow) IsFocused() bool { return w.focused } -func (w *MockWindow) SetTitle(title string) { w.title = title } -func (w *MockWindow) SetPosition(x, y int) { w.x = x; w.y = y } -func (w *MockWindow) SetSize(width, height int) { w.width = width; w.height = height } -func (w *MockWindow) SetBackgroundColour(r, g, b, a uint8) {} -func (w *MockWindow) SetVisibility(visible bool) { w.visible = visible } -func (w *MockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop } -func (w *MockWindow) Maximise() { w.maximised = true } -func (w *MockWindow) Restore() { w.maximised = false } -func (w *MockWindow) Minimise() {} -func (w *MockWindow) Focus() { w.focused = true } -func (w *MockWindow) Close() { w.closed = true } -func (w *MockWindow) Show() { w.visible = true } -func (w *MockWindow) Hide() { w.visible = false } -func (w *MockWindow) Fullscreen() {} -func (w *MockWindow) UnFullscreen() {} -func (w *MockWindow) OnWindowEvent(handler func(WindowEvent)) { w.eventHandlers = append(w.eventHandlers, handler) } +func (w *MockWindow) Name() string { return w.name } +func (w *MockWindow) Title() string { return w.title } +func (w *MockWindow) Position() (int, int) { return w.x, w.y } +func (w *MockWindow) Size() (int, int) { return w.width, w.height } +func (w *MockWindow) IsMaximised() bool { return w.maximised } +func (w *MockWindow) IsFocused() bool { return w.focused } +func (w *MockWindow) SetTitle(title string) { w.title = title } +func (w *MockWindow) SetPosition(x, y int) { w.x = x; w.y = y } +func (w *MockWindow) SetSize(width, height int) { w.width = width; w.height = height } +func (w *MockWindow) SetBackgroundColour(r, g, b, a uint8) {} +func (w *MockWindow) SetVisibility(visible bool) { w.visible = visible } +func (w *MockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop } +func (w *MockWindow) Maximise() { w.maximised = true } +func (w *MockWindow) Restore() { w.maximised = false } +func (w *MockWindow) Minimise() {} +func (w *MockWindow) Focus() { w.focused = true } +func (w *MockWindow) Close() { w.closed = true } +func (w *MockWindow) Show() { w.visible = true } +func (w *MockWindow) Hide() { w.visible = false } +func (w *MockWindow) Fullscreen() {} +func (w *MockWindow) UnFullscreen() {} +func (w *MockWindow) OnWindowEvent(handler func(WindowEvent)) { + w.eventHandlers = append(w.eventHandlers, handler) +} func (w *MockWindow) OnFileDrop(handler func(paths []string, targetID string)) { w.fileDropHandlers = append(w.fileDropHandlers, handler) } diff --git a/pkg/window/mock_test.go b/pkg/window/mock_test.go index 8bb08a9..0babb6a 100644 --- a/pkg/window/mock_test.go +++ b/pkg/window/mock_test.go @@ -1,4 +1,3 @@ -// pkg/window/mock_test.go package window type mockPlatform struct { @@ -9,11 +8,11 @@ func newMockPlatform() *mockPlatform { return &mockPlatform{} } -func (m *mockPlatform) CreateWindow(opts PlatformWindowOptions) PlatformWindow { +func (m *mockPlatform) CreateWindow(options PlatformWindowOptions) PlatformWindow { w := &mockWindow{ - name: opts.Name, title: opts.Title, url: opts.URL, - width: opts.Width, height: opts.Height, - x: opts.X, y: opts.Y, + name: options.Name, title: options.Title, url: options.URL, + width: options.Width, height: options.Height, + x: options.X, y: options.Y, } m.windows = append(m.windows, w) return w @@ -39,28 +38,30 @@ type mockWindow struct { fileDropHandlers []func(paths []string, targetID string) } -func (w *mockWindow) Name() string { return w.name } -func (w *mockWindow) Title() string { return w.title } -func (w *mockWindow) Position() (int, int) { return w.x, w.y } -func (w *mockWindow) Size() (int, int) { return w.width, w.height } -func (w *mockWindow) IsMaximised() bool { return w.maximised } -func (w *mockWindow) IsFocused() bool { return w.focused } -func (w *mockWindow) SetTitle(title string) { w.title = title } -func (w *mockWindow) SetPosition(x, y int) { w.x = x; w.y = y } -func (w *mockWindow) SetSize(width, height int) { w.width = width; w.height = height } -func (w *mockWindow) SetBackgroundColour(r, g, b, a uint8) {} -func (w *mockWindow) SetVisibility(visible bool) { w.visible = visible } -func (w *mockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop } -func (w *mockWindow) Maximise() { w.maximised = true } -func (w *mockWindow) Restore() { w.maximised = false } -func (w *mockWindow) Minimise() { w.minimised = true } -func (w *mockWindow) Focus() { w.focused = true } -func (w *mockWindow) Close() { w.closed = true } -func (w *mockWindow) Show() { w.visible = true } -func (w *mockWindow) Hide() { w.visible = false } -func (w *mockWindow) Fullscreen() { w.fullscreened = true } -func (w *mockWindow) UnFullscreen() { w.fullscreened = false } -func (w *mockWindow) OnWindowEvent(handler func(WindowEvent)) { w.eventHandlers = append(w.eventHandlers, handler) } +func (w *mockWindow) Name() string { return w.name } +func (w *mockWindow) Title() string { return w.title } +func (w *mockWindow) Position() (int, int) { return w.x, w.y } +func (w *mockWindow) Size() (int, int) { return w.width, w.height } +func (w *mockWindow) IsMaximised() bool { return w.maximised } +func (w *mockWindow) IsFocused() bool { return w.focused } +func (w *mockWindow) SetTitle(title string) { w.title = title } +func (w *mockWindow) SetPosition(x, y int) { w.x = x; w.y = y } +func (w *mockWindow) SetSize(width, height int) { w.width = width; w.height = height } +func (w *mockWindow) SetBackgroundColour(r, g, b, a uint8) {} +func (w *mockWindow) SetVisibility(visible bool) { w.visible = visible } +func (w *mockWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop } +func (w *mockWindow) Maximise() { w.maximised = true } +func (w *mockWindow) Restore() { w.maximised = false } +func (w *mockWindow) Minimise() { w.minimised = true } +func (w *mockWindow) Focus() { w.focused = true } +func (w *mockWindow) Close() { w.closed = true } +func (w *mockWindow) Show() { w.visible = true } +func (w *mockWindow) Hide() { w.visible = false } +func (w *mockWindow) Fullscreen() { w.fullscreened = true } +func (w *mockWindow) UnFullscreen() { w.fullscreened = false } +func (w *mockWindow) OnWindowEvent(handler func(WindowEvent)) { + w.eventHandlers = append(w.eventHandlers, handler) +} func (w *mockWindow) OnFileDrop(handler func(paths []string, targetID string)) { w.fileDropHandlers = append(w.fileDropHandlers, handler) } diff --git a/pkg/window/options.go b/pkg/window/options.go index b677617..38c5064 100644 --- a/pkg/window/options.go +++ b/pkg/window/options.go @@ -5,13 +5,13 @@ package window type WindowOption func(*Window) error // ApplyOptions creates a Window and applies all options in order. -func ApplyOptions(opts ...WindowOption) (*Window, error) { +func ApplyOptions(options ...WindowOption) (*Window, error) { w := &Window{} - for _, opt := range opts { - if opt == nil { + for _, option := range options { + if option == nil { continue } - if err := opt(w); err != nil { + if err := option(w); err != nil { return nil, err } } diff --git a/pkg/window/platform.go b/pkg/window/platform.go index ae4e2e6..c0e56a9 100644 --- a/pkg/window/platform.go +++ b/pkg/window/platform.go @@ -3,25 +3,25 @@ package window // Platform abstracts the windowing backend (Wails v3). type Platform interface { - CreateWindow(opts PlatformWindowOptions) PlatformWindow + CreateWindow(options PlatformWindowOptions) PlatformWindow GetWindows() []PlatformWindow } // PlatformWindowOptions are the backend-specific options passed to CreateWindow. type PlatformWindowOptions struct { - Name string - Title string - URL string - Width, Height int - X, Y int - MinWidth, MinHeight int - MaxWidth, MaxHeight int - Frameless bool - Hidden bool - AlwaysOnTop bool - BackgroundColour [4]uint8 // RGBA - DisableResize bool - EnableFileDrop bool + Name string + Title string + URL string + Width, Height int + X, Y int + MinWidth, MinHeight int + MaxWidth, MaxHeight int + Frameless bool + Hidden bool + AlwaysOnTop bool + BackgroundColour [4]uint8 // RGBA + DisableResize bool + EnableFileDrop bool } // PlatformWindow is a live window handle from the backend. diff --git a/pkg/window/register.go b/pkg/window/register.go index 63812f1..850b57a 100644 --- a/pkg/window/register.go +++ b/pkg/window/register.go @@ -2,8 +2,6 @@ package window 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{ diff --git a/pkg/window/service.go b/pkg/window/service.go index 8260e45..c273eed 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -7,61 +7,51 @@ import ( "forge.lthn.ai/core/go/pkg/core" ) -// Options holds configuration for the window service. type Options struct{} -// Service is a core.Service managing window lifecycle via IPC. -// It embeds ServiceRuntime for Core access and composes Manager for platform operations. type Service struct { *core.ServiceRuntime[Options] manager *Manager platform Platform } -// OnStartup queries config from the display orchestrator and registers IPC handlers. func (s *Service) OnStartup(ctx context.Context) error { // Query config — display registers its handler before us (registration order guarantee). // If display is not registered, handled=false and we skip config. - cfg, handled, _ := s.Core().QUERY(QueryConfig{}) + configValue, handled, _ := s.Core().QUERY(QueryConfig{}) if handled { - if wCfg, ok := cfg.(map[string]any); ok { - s.applyConfig(wCfg) + if windowConfig, ok := configValue.(map[string]any); ok { + s.applyConfig(windowConfig) } } - // Register QUERY and TASK handlers manually. - // ACTION handler (HandleIPCEvents) is auto-registered by WithService — - // do NOT call RegisterAction here or actions will double-fire. s.Core().RegisterQuery(s.handleQuery) s.Core().RegisterTask(s.handleTask) return nil } -func (s *Service) applyConfig(cfg map[string]any) { - if w, ok := cfg["default_width"]; ok { - if _, ok := w.(int); ok { +func (s *Service) applyConfig(configData map[string]any) { + if width, ok := configData["default_width"]; ok { + if _, ok := width.(int); ok { // TODO: s.manager.SetDefaultWidth(width) — add when Manager API is extended } } - if h, ok := cfg["default_height"]; ok { - if _, ok := h.(int); ok { + if height, ok := configData["default_height"]; ok { + if _, ok := height.(int); ok { // TODO: s.manager.SetDefaultHeight(height) — add when Manager API is extended } } - if sf, ok := cfg["state_file"]; ok { - if _, ok := sf.(string); ok { + if stateFile, ok := configData["state_file"]; ok { + if _, ok := stateFile.(string); ok { // TODO: s.manager.State().SetPath(stateFile) — add when StateManager API is extended } } } -// 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 := q.(type) { case QueryWindowList: @@ -123,7 +113,7 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { case TaskSetPosition: return nil, true, s.taskSetPosition(t.Name, t.X, t.Y) case TaskSetSize: - return nil, true, s.taskSetSize(t.Name, t.W, t.H) + return nil, true, s.taskSetSize(t.Name, t.Width, t.Height) case TaskMaximise: return nil, true, s.taskMaximise(t.Name) case TaskMinimise: @@ -155,7 +145,7 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { } func (s *Service) taskOpenWindow(t TaskOpenWindow) (any, bool, error) { - pw, err := s.manager.Open(t.Opts...) + pw, err := s.manager.Open(t.Options...) if err != nil { return nil, true, err } @@ -189,7 +179,7 @@ func (s *Service) trackWindow(pw PlatformWindow) { if data := e.Data; data != nil { w, _ := data["w"].(int) h, _ := data["h"].(int) - _ = s.Core().ACTION(ActionWindowResized{Name: e.Name, W: w, H: h}) + _ = s.Core().ACTION(ActionWindowResized{Name: e.Name, Width: w, Height: h}) } case "close": _ = s.Core().ACTION(ActionWindowClosed{Name: e.Name}) @@ -227,13 +217,13 @@ func (s *Service) taskSetPosition(name string, x, y int) error { return nil } -func (s *Service) taskSetSize(name string, w, h int) error { +func (s *Service) taskSetSize(name string, width, height int) error { pw, ok := s.manager.Get(name) if !ok { return coreerr.E("window.taskSetSize", "window not found: "+name, nil) } - pw.SetSize(w, h) - s.manager.State().UpdateSize(name, w, h) + pw.SetSize(width, height) + s.manager.State().UpdateSize(name, width, height) return nil } diff --git a/pkg/window/service_test.go b/pkg/window/service_test.go index 4df1108..57fb9f4 100644 --- a/pkg/window/service_test.go +++ b/pkg/window/service_test.go @@ -31,7 +31,7 @@ func TestRegister_Good(t *testing.T) { func TestTaskOpenWindow_Good(t *testing.T) { _, c := newTestWindowService(t) result, handled, err := c.PERFORM(TaskOpenWindow{ - Opts: []WindowOption{WithName("test"), WithURL("/")}, + Options: []WindowOption{WithName("test"), WithURL("/")}, }) require.NoError(t, err) assert.True(t, handled) @@ -49,8 +49,8 @@ func TestTaskOpenWindow_Bad(t *testing.T) { func TestQueryWindowList_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("a")}}) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("b")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("a")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("b")}}) result, handled, err := c.QUERY(QueryWindowList{}) require.NoError(t, err) @@ -61,7 +61,7 @@ func TestQueryWindowList_Good(t *testing.T) { func TestQueryWindowByName_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) result, handled, err := c.QUERY(QueryWindowByName{Name: "test"}) require.NoError(t, err) @@ -80,7 +80,7 @@ func TestQueryWindowByName_Bad(t *testing.T) { func TestTaskCloseWindow_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) _, handled, err := c.PERFORM(TaskCloseWindow{Name: "test"}) require.NoError(t, err) @@ -100,7 +100,7 @@ func TestTaskCloseWindow_Bad(t *testing.T) { func TestTaskSetPosition_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) _, handled, err := c.PERFORM(TaskSetPosition{Name: "test", X: 100, Y: 200}) require.NoError(t, err) @@ -114,9 +114,9 @@ func TestTaskSetPosition_Good(t *testing.T) { func TestTaskSetSize_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) - _, handled, err := c.PERFORM(TaskSetSize{Name: "test", W: 800, H: 600}) + _, handled, err := c.PERFORM(TaskSetSize{Name: "test", Width: 800, Height: 600}) require.NoError(t, err) assert.True(t, handled) @@ -128,7 +128,7 @@ func TestTaskSetSize_Good(t *testing.T) { func TestTaskMaximise_Good(t *testing.T) { _, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) _, handled, err := c.PERFORM(TaskMaximise{Name: "test"}) require.NoError(t, err) @@ -144,7 +144,7 @@ func TestFileDrop_Good(t *testing.T) { // Open a window result, _, _ := c.PERFORM(TaskOpenWindow{ - Opts: []WindowOption{WithName("drop-test")}, + Options: []WindowOption{WithName("drop-test")}, }) info := result.(WindowInfo) assert.Equal(t, "drop-test", info.Name) @@ -179,7 +179,7 @@ func TestFileDrop_Good(t *testing.T) { func TestTaskMinimise_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) _, handled, err := c.PERFORM(TaskMinimise{Name: "test"}) require.NoError(t, err) @@ -202,7 +202,7 @@ func TestTaskMinimise_Bad(t *testing.T) { func TestTaskFocus_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) _, handled, err := c.PERFORM(TaskFocus{Name: "test"}) require.NoError(t, err) @@ -225,7 +225,7 @@ func TestTaskFocus_Bad(t *testing.T) { func TestTaskRestore_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) // First maximise, then restore _, _, _ = c.PERFORM(TaskMaximise{Name: "test"}) @@ -256,7 +256,7 @@ func TestTaskRestore_Bad(t *testing.T) { func TestTaskSetTitle_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) _, handled, err := c.PERFORM(TaskSetTitle{Name: "test", Title: "New Title"}) require.NoError(t, err) @@ -278,7 +278,7 @@ func TestTaskSetTitle_Bad(t *testing.T) { func TestTaskSetVisibility_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) _, handled, err := c.PERFORM(TaskSetVisibility{Name: "test", Visible: true}) require.NoError(t, err) @@ -307,7 +307,7 @@ func TestTaskSetVisibility_Bad(t *testing.T) { func TestTaskFullscreen_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("test")}}) + _, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}}) // Enter fullscreen _, handled, err := c.PERFORM(TaskFullscreen{Name: "test", Fullscreen: true}) @@ -337,8 +337,8 @@ func TestTaskFullscreen_Bad(t *testing.T) { func TestTaskSaveLayout_Good(t *testing.T) { svc, c := newTestWindowService(t) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("editor"), WithSize(960, 1080), WithPosition(0, 0)}}) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("terminal"), WithSize(960, 1080), WithPosition(960, 0)}}) + _, _, _ = 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)}}) _, handled, err := c.PERFORM(TaskSaveLayout{Name: "coding"}) require.NoError(t, err) @@ -374,8 +374,8 @@ func TestTaskSaveLayout_Bad(t *testing.T) { func TestTaskRestoreLayout_Good(t *testing.T) { svc, c := newTestWindowService(t) // Open windows - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("editor"), WithSize(800, 600), WithPosition(0, 0)}}) - _, _, _ = c.PERFORM(TaskOpenWindow{Opts: []WindowOption{WithName("terminal"), WithSize(800, 600), WithPosition(0, 0)}}) + _, _, _ = 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)}}) // Save a layout with specific positions _, _, _ = c.PERFORM(TaskSaveLayout{Name: "coding"}) diff --git a/pkg/window/wails.go b/pkg/window/wails.go index 1d2a722..a2587ce 100644 --- a/pkg/window/wails.go +++ b/pkg/window/wails.go @@ -16,28 +16,28 @@ func NewWailsPlatform(app *application.App) *WailsPlatform { return &WailsPlatform{app: app} } -func (wp *WailsPlatform) CreateWindow(opts PlatformWindowOptions) PlatformWindow { +func (wp *WailsPlatform) CreateWindow(options PlatformWindowOptions) PlatformWindow { wOpts := application.WebviewWindowOptions{ - Name: opts.Name, - Title: opts.Title, - URL: opts.URL, - Width: opts.Width, - Height: opts.Height, - X: opts.X, - Y: opts.Y, - MinWidth: opts.MinWidth, - MinHeight: opts.MinHeight, - MaxWidth: opts.MaxWidth, - MaxHeight: opts.MaxHeight, - Frameless: opts.Frameless, - Hidden: opts.Hidden, - AlwaysOnTop: opts.AlwaysOnTop, - DisableResize: opts.DisableResize, - EnableFileDrop: opts.EnableFileDrop, - BackgroundColour: application.NewRGBA(opts.BackgroundColour[0], opts.BackgroundColour[1], opts.BackgroundColour[2], opts.BackgroundColour[3]), + Name: options.Name, + Title: options.Title, + URL: options.URL, + Width: options.Width, + Height: options.Height, + X: options.X, + Y: options.Y, + MinWidth: options.MinWidth, + MinHeight: options.MinHeight, + MaxWidth: options.MaxWidth, + MaxHeight: options.MaxHeight, + Frameless: options.Frameless, + Hidden: options.Hidden, + AlwaysOnTop: options.AlwaysOnTop, + DisableResize: options.DisableResize, + EnableFileDrop: options.EnableFileDrop, + BackgroundColour: application.NewRGBA(options.BackgroundColour[0], options.BackgroundColour[1], options.BackgroundColour[2], options.BackgroundColour[3]), } w := wp.app.Window.NewWithOptions(wOpts) - return &wailsWindow{w: w, title: opts.Title} + return &wailsWindow{w: w, title: options.Title} } func (wp *WailsPlatform) GetWindows() []PlatformWindow { @@ -58,14 +58,14 @@ type wailsWindow struct { title string } -func (ww *wailsWindow) Name() string { return ww.w.Name() } -func (ww *wailsWindow) Title() string { return ww.title } -func (ww *wailsWindow) Position() (int, int) { return ww.w.Position() } -func (ww *wailsWindow) Size() (int, int) { return ww.w.Size() } -func (ww *wailsWindow) IsMaximised() bool { return ww.w.IsMaximised() } -func (ww *wailsWindow) IsFocused() bool { return ww.w.IsFocused() } -func (ww *wailsWindow) SetTitle(title string) { ww.title = title; ww.w.SetTitle(title) } -func (ww *wailsWindow) SetPosition(x, y int) { ww.w.SetPosition(x, y) } +func (ww *wailsWindow) Name() string { return ww.w.Name() } +func (ww *wailsWindow) Title() string { return ww.title } +func (ww *wailsWindow) Position() (int, int) { return ww.w.Position() } +func (ww *wailsWindow) Size() (int, int) { return ww.w.Size() } +func (ww *wailsWindow) IsMaximised() bool { return ww.w.IsMaximised() } +func (ww *wailsWindow) IsFocused() bool { return ww.w.IsFocused() } +func (ww *wailsWindow) SetTitle(title string) { ww.title = title; ww.w.SetTitle(title) } +func (ww *wailsWindow) SetPosition(x, y int) { ww.w.SetPosition(x, y) } func (ww *wailsWindow) SetSize(width, height int) { ww.w.SetSize(width, height) } func (ww *wailsWindow) SetBackgroundColour(r, g, b, a uint8) { ww.w.SetBackgroundColour(application.NewRGBA(r, g, b, a)) @@ -140,4 +140,3 @@ var _ PlatformWindow = (*wailsWindow)(nil) // Ensure WailsPlatform satisfies Platform at compile time. var _ Platform = (*WailsPlatform)(nil) - diff --git a/pkg/window/window.go b/pkg/window/window.go index 937f1fd..28200b7 100644 --- a/pkg/window/window.go +++ b/pkg/window/window.go @@ -9,19 +9,19 @@ import ( // Window is CoreGUI's own window descriptor — NOT a Wails type alias. type Window struct { - Name string - Title string - URL string - Width, Height int - X, Y int + Name string + Title string + URL string + Width, Height int + X, Y int MinWidth, MinHeight int MaxWidth, MaxHeight int - Frameless bool - Hidden bool - AlwaysOnTop bool - BackgroundColour [4]uint8 - DisableResize bool - EnableFileDrop bool + Frameless bool + Hidden bool + AlwaysOnTop bool + BackgroundColour [4]uint8 + DisableResize bool + EnableFileDrop bool } // ToPlatformOptions converts a Window to PlatformWindowOptions for the backend. @@ -68,8 +68,8 @@ func NewManagerWithDir(platform Platform, configDir string) *Manager { } // Open creates a window using functional options, applies saved state, and tracks it. -func (m *Manager) Open(opts ...WindowOption) (PlatformWindow, error) { - w, err := ApplyOptions(opts...) +func (m *Manager) Open(options ...WindowOption) (PlatformWindow, error) { + w, err := ApplyOptions(options...) if err != nil { return nil, coreerr.E("window.Manager.Open", "failed to apply options", err) }