feat(display): convert delegation to IPC, full conclave integration
Display methods now route through IPC bus instead of direct Manager calls. Menu/tray setup uses PERFORM. Tray click actions handled via HandleIPCEvents. WindowInfo aliased from window package. Direct Manager refs removed. Integration tests verify full 4-service conclave startup and communication. Service struct no longer holds windows/tray/menus fields — uses windowService() for direct Manager access where IPC messages are not yet defined. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
0893456a9e
commit
49e1faed54
3 changed files with 430 additions and 723 deletions
|
|
@ -16,6 +16,9 @@ import (
|
|||
// Options holds configuration for the display service.
|
||||
type Options struct{}
|
||||
|
||||
// WindowInfo is an alias for window.WindowInfo (backward compatibility).
|
||||
type WindowInfo = window.WindowInfo
|
||||
|
||||
// Service manages windowing, dialogs, and other visual elements.
|
||||
// It orchestrates sub-services (window, systray, menu) via IPC and bridges
|
||||
// IPC actions to WebSocket events for TypeScript apps.
|
||||
|
|
@ -25,9 +28,6 @@ type Service struct {
|
|||
app App
|
||||
config Options
|
||||
configData map[string]map[string]any
|
||||
windows *window.Manager
|
||||
tray *systray.Manager
|
||||
menus *menu.Manager
|
||||
notifier *notifications.NotificationService
|
||||
events *WSEventManager
|
||||
}
|
||||
|
|
@ -80,19 +80,11 @@ func (s *Service) OnStartup(ctx context.Context) error {
|
|||
// HandleIPCEvents is auto-discovered and registered by core.WithService.
|
||||
// It bridges sub-service IPC actions to WebSocket events for TS apps.
|
||||
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
||||
if s.events == nil && s.wailsApp != nil {
|
||||
return nil // No WS event manager (testing without Wails)
|
||||
}
|
||||
|
||||
switch m := msg.(type) {
|
||||
case core.ActionServiceStartup:
|
||||
// All services have completed OnStartup — safe to PERFORM on sub-services
|
||||
if s.menus != nil {
|
||||
s.buildMenu()
|
||||
}
|
||||
if s.tray != nil {
|
||||
s.setupTray()
|
||||
}
|
||||
s.buildMenu()
|
||||
s.setupTray()
|
||||
case window.ActionWindowOpened:
|
||||
if s.events != nil {
|
||||
s.events.Emit(Event{Type: EventWindowCreate, Window: m.Name,
|
||||
|
|
@ -140,22 +132,12 @@ func (s *Service) handleTrayAction(actionID string) {
|
|||
switch actionID {
|
||||
case "open-desktop":
|
||||
// Show all windows
|
||||
if s.windows != nil {
|
||||
for _, name := range s.windows.List() {
|
||||
if pw, ok := s.windows.Get(name); ok {
|
||||
pw.Show()
|
||||
}
|
||||
}
|
||||
infos := s.ListWindowInfos()
|
||||
for _, info := range infos {
|
||||
_, _, _ = s.Core().PERFORM(window.TaskFocus{Name: info.Name})
|
||||
}
|
||||
case "close-desktop":
|
||||
// Hide all windows — future: add TaskHideWindow
|
||||
if s.windows != nil {
|
||||
for _, name := range s.windows.List() {
|
||||
if pw, ok := s.windows.Get(name); ok {
|
||||
pw.Hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
case "env-info":
|
||||
if s.app != nil {
|
||||
s.ShowEnvironmentDialog()
|
||||
|
|
@ -207,114 +189,101 @@ func (s *Service) handleConfigTask(c *core.Core, t core.Task) (any, bool, error)
|
|||
}
|
||||
}
|
||||
|
||||
// --- Window Management (delegates to window.Manager) ---
|
||||
// --- Service accessors ---
|
||||
|
||||
// OpenWindow creates a new window with the given options.
|
||||
func (s *Service) OpenWindow(opts ...window.WindowOption) error {
|
||||
pw, err := s.windows.Open(opts...)
|
||||
// windowService returns the window service from Core, or nil if not registered.
|
||||
func (s *Service) windowService() *window.Service {
|
||||
svc, err := core.ServiceFor[*window.Service](s.Core(), "window")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
// --- 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})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetWindowInfo returns information about a window via IPC.
|
||||
func (s *Service) GetWindowInfo(name string) (*window.WindowInfo, error) {
|
||||
result, handled, err := s.Core().QUERY(window.QueryWindowByName{Name: name})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !handled {
|
||||
return nil, fmt.Errorf("window service not available")
|
||||
}
|
||||
info, _ := result.(*window.WindowInfo)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// ListWindowInfos returns information about all tracked windows via IPC.
|
||||
func (s *Service) ListWindowInfos() []window.WindowInfo {
|
||||
result, handled, _ := s.Core().QUERY(window.QueryWindowList{})
|
||||
if !handled {
|
||||
return nil
|
||||
}
|
||||
list, _ := result.([]window.WindowInfo)
|
||||
return list
|
||||
}
|
||||
|
||||
// SetWindowPosition moves a window via IPC.
|
||||
func (s *Service) SetWindowPosition(name string, x, y int) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetPosition{Name: name, X: x, Y: y})
|
||||
return err
|
||||
}
|
||||
|
||||
// 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})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWindowBounds sets both position and size of a window via IPC.
|
||||
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
|
||||
}
|
||||
s.trackWindow(pw)
|
||||
return nil
|
||||
_, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, W: width, H: height})
|
||||
return err
|
||||
}
|
||||
|
||||
// trackWindow attaches event listeners for state persistence and WebSocket events.
|
||||
func (s *Service) trackWindow(pw window.PlatformWindow) {
|
||||
if s.events != nil {
|
||||
s.events.EmitWindowEvent(EventWindowCreate, pw.Name(), map[string]any{
|
||||
"name": pw.Name(),
|
||||
})
|
||||
s.events.AttachWindowListeners(pw)
|
||||
}
|
||||
}
|
||||
|
||||
// GetWindowInfo returns information about a window by name.
|
||||
func (s *Service) GetWindowInfo(name string) (*WindowInfo, error) {
|
||||
pw, ok := s.windows.Get(name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
x, y := pw.Position()
|
||||
w, h := pw.Size()
|
||||
return &WindowInfo{
|
||||
Name: name,
|
||||
X: x,
|
||||
Y: y,
|
||||
Width: w,
|
||||
Height: h,
|
||||
Maximized: pw.IsMaximised(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListWindowInfos returns information about all tracked windows.
|
||||
func (s *Service) ListWindowInfos() []WindowInfo {
|
||||
names := s.windows.List()
|
||||
result := make([]WindowInfo, 0, len(names))
|
||||
for _, name := range names {
|
||||
if pw, ok := s.windows.Get(name); ok {
|
||||
x, y := pw.Position()
|
||||
w, h := pw.Size()
|
||||
result = append(result, WindowInfo{
|
||||
Name: name,
|
||||
X: x,
|
||||
Y: y,
|
||||
Width: w,
|
||||
Height: h,
|
||||
Maximized: pw.IsMaximised(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SetWindowPosition moves a window to the specified position.
|
||||
func (s *Service) SetWindowPosition(name string, x, y int) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
pw.SetPosition(x, y)
|
||||
s.windows.State().UpdatePosition(name, x, y)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWindowSize resizes a window.
|
||||
func (s *Service) SetWindowSize(name string, width, height int) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
pw.SetSize(width, height)
|
||||
s.windows.State().UpdateSize(name, width, height)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWindowBounds sets both position and size of a window.
|
||||
func (s *Service) SetWindowBounds(name string, x, y, width, height int) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
pw.SetPosition(x, y)
|
||||
pw.SetSize(width, height)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaximizeWindow maximizes a window.
|
||||
// MaximizeWindow maximizes a window via IPC.
|
||||
func (s *Service) MaximizeWindow(name string) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
pw.Maximise()
|
||||
s.windows.State().UpdateMaximized(name, true)
|
||||
return nil
|
||||
_, _, err := s.Core().PERFORM(window.TaskMaximise{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// MinimizeWindow minimizes a window via IPC.
|
||||
func (s *Service) MinimizeWindow(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskMinimise{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// FocusWindow brings a window to the front via IPC.
|
||||
func (s *Service) FocusWindow(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskFocus{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// CloseWindow closes a window via IPC.
|
||||
func (s *Service) CloseWindow(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskCloseWindow{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// RestoreWindow restores a maximized/minimized window.
|
||||
// Uses direct Manager access (no IPC task for restore yet).
|
||||
func (s *Service) RestoreWindow(name string) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
|
|
@ -322,41 +291,14 @@ func (s *Service) RestoreWindow(name string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// MinimizeWindow minimizes a window.
|
||||
func (s *Service) MinimizeWindow(name string) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
pw.Minimise()
|
||||
return nil
|
||||
}
|
||||
|
||||
// FocusWindow brings a window to the front.
|
||||
func (s *Service) FocusWindow(name string) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
pw.Focus()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseWindow closes a window by name.
|
||||
func (s *Service) CloseWindow(name string) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
s.windows.State().CaptureState(pw)
|
||||
pw.Close()
|
||||
s.windows.Remove(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWindowVisibility shows or hides a window.
|
||||
// Uses direct Manager access (no IPC task for visibility yet).
|
||||
func (s *Service) SetWindowVisibility(name string, visible bool) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
|
|
@ -365,8 +307,13 @@ func (s *Service) SetWindowVisibility(name string, visible bool) error {
|
|||
}
|
||||
|
||||
// SetWindowAlwaysOnTop sets whether a window stays on top.
|
||||
// Uses direct Manager access (no IPC task for always-on-top yet).
|
||||
func (s *Service) SetWindowAlwaysOnTop(name string, alwaysOnTop bool) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
|
|
@ -375,8 +322,13 @@ func (s *Service) SetWindowAlwaysOnTop(name string, alwaysOnTop bool) error {
|
|||
}
|
||||
|
||||
// SetWindowTitle changes a window's title.
|
||||
// Uses direct Manager access (no IPC task for title yet).
|
||||
func (s *Service) SetWindowTitle(name string, title string) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
|
|
@ -385,8 +337,13 @@ func (s *Service) SetWindowTitle(name string, title string) error {
|
|||
}
|
||||
|
||||
// SetWindowFullscreen sets a window to fullscreen mode.
|
||||
// Uses direct Manager access (no IPC task for fullscreen yet).
|
||||
func (s *Service) SetWindowFullscreen(name string, fullscreen bool) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
|
|
@ -399,8 +356,13 @@ func (s *Service) SetWindowFullscreen(name string, fullscreen bool) error {
|
|||
}
|
||||
|
||||
// SetWindowBackgroundColour sets the background colour of a window.
|
||||
// Uses direct Manager access (no IPC task for background colour yet).
|
||||
func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error {
|
||||
pw, ok := s.windows.Get(name)
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
pw, ok := ws.Manager().Get(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
|
|
@ -410,11 +372,10 @@ func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error
|
|||
|
||||
// GetFocusedWindow returns the name of the currently focused window.
|
||||
func (s *Service) GetFocusedWindow() string {
|
||||
for _, name := range s.windows.List() {
|
||||
if pw, ok := s.windows.Get(name); ok {
|
||||
if pw.IsFocused() {
|
||||
return name
|
||||
}
|
||||
infos := s.ListWindowInfos()
|
||||
for _, info := range infos {
|
||||
if info.Focused {
|
||||
return info.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
|
@ -422,45 +383,40 @@ func (s *Service) GetFocusedWindow() string {
|
|||
|
||||
// GetWindowTitle returns the title of a window by name.
|
||||
func (s *Service) GetWindowTitle(name string) (string, error) {
|
||||
_, ok := s.windows.Get(name)
|
||||
if !ok {
|
||||
info, err := s.GetWindowInfo(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info == nil {
|
||||
return "", fmt.Errorf("window not found: %s", name)
|
||||
}
|
||||
return name, nil // Wails v3 doesn't expose a title getter
|
||||
return info.Name, nil // Wails v3 doesn't expose a title getter
|
||||
}
|
||||
|
||||
// ResetWindowState clears saved window positions.
|
||||
func (s *Service) ResetWindowState() error {
|
||||
if s.windows != nil {
|
||||
s.windows.State().Clear()
|
||||
ws := s.windowService()
|
||||
if ws != nil {
|
||||
ws.Manager().State().Clear()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSavedWindowStates returns all saved window states.
|
||||
func (s *Service) GetSavedWindowStates() map[string]window.WindowState {
|
||||
if s.windows == nil {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return nil
|
||||
}
|
||||
result := make(map[string]window.WindowState)
|
||||
for _, name := range s.windows.State().ListStates() {
|
||||
if state, ok := s.windows.State().GetState(name); ok {
|
||||
for _, name := range ws.Manager().State().ListStates() {
|
||||
if state, ok := ws.Manager().State().GetState(name); ok {
|
||||
result[name] = state
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// WindowInfo contains information about a window for MCP.
|
||||
type WindowInfo struct {
|
||||
Name string `json:"name"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Maximized bool `json:"maximized"`
|
||||
}
|
||||
|
||||
// CreateWindowOptions contains options for creating a new window.
|
||||
type CreateWindowOptions struct {
|
||||
Name string `json:"name"`
|
||||
|
|
@ -473,58 +429,57 @@ type CreateWindowOptions struct {
|
|||
}
|
||||
|
||||
// CreateWindow creates a new window with the specified options.
|
||||
func (s *Service) CreateWindow(opts CreateWindowOptions) (*WindowInfo, error) {
|
||||
func (s *Service) CreateWindow(opts CreateWindowOptions) (*window.WindowInfo, error) {
|
||||
if opts.Name == "" {
|
||||
return nil, fmt.Errorf("window name is required")
|
||||
}
|
||||
err := s.OpenWindow(
|
||||
window.WithName(opts.Name),
|
||||
window.WithTitle(opts.Title),
|
||||
window.WithURL(opts.URL),
|
||||
window.WithSize(opts.Width, opts.Height),
|
||||
window.WithPosition(opts.X, opts.Y),
|
||||
)
|
||||
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),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WindowInfo{
|
||||
Name: opts.Name,
|
||||
X: opts.X,
|
||||
Y: opts.Y,
|
||||
Width: opts.Width,
|
||||
Height: opts.Height,
|
||||
}, nil
|
||||
info := result.(window.WindowInfo)
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// --- Layout delegation ---
|
||||
|
||||
// SaveLayout saves the current window arrangement as a named layout.
|
||||
func (s *Service) SaveLayout(name string) error {
|
||||
if s.windows == nil {
|
||||
return fmt.Errorf("window manager not initialized")
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
states := make(map[string]window.WindowState)
|
||||
for _, n := range s.windows.List() {
|
||||
if pw, ok := s.windows.Get(n); ok {
|
||||
for _, n := range ws.Manager().List() {
|
||||
if pw, ok := ws.Manager().Get(n); ok {
|
||||
x, y := pw.Position()
|
||||
w, h := pw.Size()
|
||||
states[n] = window.WindowState{X: x, Y: y, Width: w, Height: h, Maximized: pw.IsMaximised()}
|
||||
}
|
||||
}
|
||||
return s.windows.Layout().SaveLayout(name, states)
|
||||
return ws.Manager().Layout().SaveLayout(name, states)
|
||||
}
|
||||
|
||||
// RestoreLayout applies a saved layout.
|
||||
func (s *Service) RestoreLayout(name string) error {
|
||||
if s.windows == nil {
|
||||
return fmt.Errorf("window manager not initialized")
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
layout, ok := s.windows.Layout().GetLayout(name)
|
||||
layout, ok := ws.Manager().Layout().GetLayout(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("layout not found: %s", name)
|
||||
}
|
||||
for wName, state := range layout.Windows {
|
||||
if pw, ok := s.windows.Get(wName); ok {
|
||||
if pw, ok := ws.Manager().Get(wName); ok {
|
||||
pw.SetPosition(state.X, state.Y)
|
||||
pw.SetSize(state.Width, state.Height)
|
||||
if state.Maximized {
|
||||
|
|
@ -539,27 +494,30 @@ func (s *Service) RestoreLayout(name string) error {
|
|||
|
||||
// ListLayouts returns all saved layout names with metadata.
|
||||
func (s *Service) ListLayouts() []window.LayoutInfo {
|
||||
if s.windows == nil {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return nil
|
||||
}
|
||||
return s.windows.Layout().ListLayouts()
|
||||
return ws.Manager().Layout().ListLayouts()
|
||||
}
|
||||
|
||||
// DeleteLayout removes a saved layout by name.
|
||||
func (s *Service) DeleteLayout(name string) error {
|
||||
if s.windows == nil {
|
||||
return fmt.Errorf("window manager not initialized")
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
s.windows.Layout().DeleteLayout(name)
|
||||
ws.Manager().Layout().DeleteLayout(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLayout returns a specific layout by name.
|
||||
func (s *Service) GetLayout(name string) *window.Layout {
|
||||
if s.windows == nil {
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return nil
|
||||
}
|
||||
layout, ok := s.windows.Layout().GetLayout(name)
|
||||
layout, ok := ws.Manager().Layout().GetLayout(name)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -570,22 +528,38 @@ func (s *Service) GetLayout(name string) *window.Layout {
|
|||
|
||||
// TileWindows arranges windows in a tiled layout.
|
||||
func (s *Service) TileWindows(mode window.TileMode, windowNames []string) error {
|
||||
return s.windows.TileWindows(mode, windowNames, 1920, 1080) // TODO: use actual screen size
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
return ws.Manager().TileWindows(mode, windowNames, 1920, 1080) // TODO: use actual screen size
|
||||
}
|
||||
|
||||
// SnapWindow snaps a window to a screen edge or corner.
|
||||
func (s *Service) SnapWindow(name string, position window.SnapPosition) error {
|
||||
return s.windows.SnapWindow(name, position, 1920, 1080) // TODO: use actual screen size
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
return ws.Manager().SnapWindow(name, position, 1920, 1080) // TODO: use actual screen size
|
||||
}
|
||||
|
||||
// StackWindows arranges windows in a cascade pattern.
|
||||
func (s *Service) StackWindows(windowNames []string, offsetX, offsetY int) error {
|
||||
return s.windows.StackWindows(windowNames, offsetX, offsetY)
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
return ws.Manager().StackWindows(windowNames, offsetX, offsetY)
|
||||
}
|
||||
|
||||
// ApplyWorkflowLayout applies a predefined layout for a specific workflow.
|
||||
func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error {
|
||||
return s.windows.ApplyWorkflow(workflow, s.windows.List(), 1920, 1080)
|
||||
ws := s.windowService()
|
||||
if ws == nil {
|
||||
return fmt.Errorf("window service not available")
|
||||
}
|
||||
return ws.Manager().ApplyWorkflow(workflow, ws.Manager().List(), 1920, 1080)
|
||||
}
|
||||
|
||||
// --- Screen queries (remain in display — use application.Get() directly) ---
|
||||
|
|
@ -758,7 +732,7 @@ func (s *Service) GetEventManager() *WSEventManager {
|
|||
return s.events
|
||||
}
|
||||
|
||||
// --- Menu (handlers stay in display, structure delegated to menu.Manager) ---
|
||||
// --- Menu (handlers stay in display, structure delegated via IPC) ---
|
||||
|
||||
func (s *Service) buildMenu() {
|
||||
items := []menu.MenuItem{
|
||||
|
|
@ -790,7 +764,7 @@ func (s *Service) buildMenu() {
|
|||
items = items[1:] // skip AppMenu
|
||||
}
|
||||
|
||||
s.menus.SetApplicationMenu(items)
|
||||
_, _, _ = s.Core().PERFORM(menu.TaskSetAppMenu{Items: items})
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
|
@ -798,8 +772,14 @@ func ptr[T any](v T) *T { return &v }
|
|||
// --- Menu handler methods ---
|
||||
|
||||
func (s *Service) handleNewWorkspace() {
|
||||
_ = s.OpenWindow(window.WithName("workspace-new"), window.WithTitle("New Workspace"),
|
||||
window.WithURL("/workspace/new"), window.WithSize(500, 400))
|
||||
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Opts: []window.WindowOption{
|
||||
window.WithName("workspace-new"),
|
||||
window.WithTitle("New Workspace"),
|
||||
window.WithURL("/workspace/new"),
|
||||
window.WithSize(500, 400),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) handleListWorkspaces() {
|
||||
|
|
@ -815,8 +795,14 @@ func (s *Service) handleListWorkspaces() {
|
|||
}
|
||||
|
||||
func (s *Service) handleNewFile() {
|
||||
_ = s.OpenWindow(window.WithName("editor"), window.WithTitle("New File - Editor"),
|
||||
window.WithURL("/#/developer/editor?new=true"), window.WithSize(1200, 800))
|
||||
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Opts: []window.WindowOption{
|
||||
window.WithName("editor"),
|
||||
window.WithTitle("New File - Editor"),
|
||||
window.WithURL("/#/developer/editor?new=true"),
|
||||
window.WithSize(1200, 800),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) handleOpenFile() {
|
||||
|
|
@ -828,48 +814,49 @@ func (s *Service) handleOpenFile() {
|
|||
if err != nil || result == "" {
|
||||
return
|
||||
}
|
||||
_ = s.OpenWindow(window.WithName("editor"), window.WithTitle(result+" - Editor"),
|
||||
window.WithURL("/#/developer/editor?file="+result), window.WithSize(1200, 800))
|
||||
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Opts: []window.WindowOption{
|
||||
window.WithName("editor"),
|
||||
window.WithTitle(result + " - Editor"),
|
||||
window.WithURL("/#/developer/editor?file=" + result),
|
||||
window.WithSize(1200, 800),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) handleSaveFile() { s.app.Event().Emit("ide:save") }
|
||||
func (s *Service) handleOpenEditor() {
|
||||
_ = s.OpenWindow(window.WithName("editor"), window.WithTitle("Editor"),
|
||||
window.WithURL("/#/developer/editor"), window.WithSize(1200, 800))
|
||||
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Opts: []window.WindowOption{
|
||||
window.WithName("editor"),
|
||||
window.WithTitle("Editor"),
|
||||
window.WithURL("/#/developer/editor"),
|
||||
window.WithSize(1200, 800),
|
||||
},
|
||||
})
|
||||
}
|
||||
func (s *Service) handleOpenTerminal() {
|
||||
_ = s.OpenWindow(window.WithName("terminal"), window.WithTitle("Terminal"),
|
||||
window.WithURL("/#/developer/terminal"), window.WithSize(800, 500))
|
||||
_, _, _ = s.Core().PERFORM(window.TaskOpenWindow{
|
||||
Opts: []window.WindowOption{
|
||||
window.WithName("terminal"),
|
||||
window.WithTitle("Terminal"),
|
||||
window.WithURL("/#/developer/terminal"),
|
||||
window.WithSize(800, 500),
|
||||
},
|
||||
})
|
||||
}
|
||||
func (s *Service) handleRun() { s.app.Event().Emit("ide:run") }
|
||||
func (s *Service) handleBuild() { s.app.Event().Emit("ide:build") }
|
||||
|
||||
// --- Tray (setup delegated to systray.Manager) ---
|
||||
// --- Tray (setup delegated via IPC) ---
|
||||
|
||||
func (s *Service) setupTray() {
|
||||
_ = s.tray.Setup("Core", "Core")
|
||||
s.tray.RegisterCallback("open-desktop", func() {
|
||||
for _, name := range s.windows.List() {
|
||||
if pw, ok := s.windows.Get(name); ok {
|
||||
pw.Show()
|
||||
}
|
||||
}
|
||||
})
|
||||
s.tray.RegisterCallback("close-desktop", func() {
|
||||
for _, name := range s.windows.List() {
|
||||
if pw, ok := s.windows.Get(name); ok {
|
||||
pw.Hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
s.tray.RegisterCallback("env-info", func() { s.ShowEnvironmentDialog() })
|
||||
s.tray.RegisterCallback("quit", func() { s.app.Quit() })
|
||||
_ = s.tray.SetMenu([]systray.TrayMenuItem{
|
||||
_, _, _ = s.Core().PERFORM(systray.TaskSetTrayMenu{Items: []systray.TrayMenuItem{
|
||||
{Label: "Open Desktop", ActionID: "open-desktop"},
|
||||
{Label: "Close Desktop", ActionID: "close-desktop"},
|
||||
{Type: "separator"},
|
||||
{Label: "Environment Info", ActionID: "env-info"},
|
||||
{Type: "separator"},
|
||||
{Label: "Quit", ActionID: "quit"},
|
||||
})
|
||||
}})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,173 +12,8 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// --- Mock platform implementations for sub-packages ---
|
||||
|
||||
// displayMockPlatformWindow implements window.PlatformWindow for display tests.
|
||||
type displayMockPlatformWindow struct {
|
||||
name, title, url string
|
||||
width, height, x, y int
|
||||
maximised, focused bool
|
||||
visible, alwaysOnTop bool
|
||||
closed bool
|
||||
eventHandlers []func(window.WindowEvent)
|
||||
}
|
||||
|
||||
func (w *displayMockPlatformWindow) Name() string { return w.name }
|
||||
func (w *displayMockPlatformWindow) Position() (int, int) { return w.x, w.y }
|
||||
func (w *displayMockPlatformWindow) Size() (int, int) { return w.width, w.height }
|
||||
func (w *displayMockPlatformWindow) IsMaximised() bool { return w.maximised }
|
||||
func (w *displayMockPlatformWindow) IsFocused() bool { return w.focused }
|
||||
func (w *displayMockPlatformWindow) SetTitle(title string) { w.title = title }
|
||||
func (w *displayMockPlatformWindow) SetPosition(x, y int) { w.x = x; w.y = y }
|
||||
func (w *displayMockPlatformWindow) SetSize(width, height int) {
|
||||
w.width = width
|
||||
w.height = height
|
||||
}
|
||||
func (w *displayMockPlatformWindow) SetBackgroundColour(r, g, b, a uint8) {}
|
||||
func (w *displayMockPlatformWindow) SetVisibility(visible bool) { w.visible = visible }
|
||||
func (w *displayMockPlatformWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop }
|
||||
func (w *displayMockPlatformWindow) Maximise() { w.maximised = true }
|
||||
func (w *displayMockPlatformWindow) Restore() { w.maximised = false }
|
||||
func (w *displayMockPlatformWindow) Minimise() {}
|
||||
func (w *displayMockPlatformWindow) Focus() { w.focused = true }
|
||||
func (w *displayMockPlatformWindow) Close() { w.closed = true }
|
||||
func (w *displayMockPlatformWindow) Show() { w.visible = true }
|
||||
func (w *displayMockPlatformWindow) Hide() { w.visible = false }
|
||||
func (w *displayMockPlatformWindow) Fullscreen() {}
|
||||
func (w *displayMockPlatformWindow) UnFullscreen() {}
|
||||
func (w *displayMockPlatformWindow) OnWindowEvent(handler func(window.WindowEvent)) {
|
||||
w.eventHandlers = append(w.eventHandlers, handler)
|
||||
}
|
||||
|
||||
// displayMockWindowPlatform implements window.Platform for display tests.
|
||||
type displayMockWindowPlatform struct {
|
||||
windows []*displayMockPlatformWindow
|
||||
}
|
||||
|
||||
func (p *displayMockWindowPlatform) CreateWindow(opts window.PlatformWindowOptions) window.PlatformWindow {
|
||||
w := &displayMockPlatformWindow{
|
||||
name: opts.Name, title: opts.Title, url: opts.URL,
|
||||
width: opts.Width, height: opts.Height,
|
||||
x: opts.X, y: opts.Y,
|
||||
}
|
||||
p.windows = append(p.windows, w)
|
||||
return w
|
||||
}
|
||||
|
||||
func (p *displayMockWindowPlatform) GetWindows() []window.PlatformWindow {
|
||||
out := make([]window.PlatformWindow, len(p.windows))
|
||||
for i, w := range p.windows {
|
||||
out[i] = w
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// displayMockSystrayPlatform implements systray.Platform for display tests.
|
||||
type displayMockSystrayPlatform struct {
|
||||
trays []*displayMockTray
|
||||
menus []*displayMockSystrayMenu
|
||||
}
|
||||
|
||||
func (p *displayMockSystrayPlatform) NewTray() systray.PlatformTray {
|
||||
t := &displayMockTray{}
|
||||
p.trays = append(p.trays, t)
|
||||
return t
|
||||
}
|
||||
|
||||
func (p *displayMockSystrayPlatform) NewMenu() systray.PlatformMenu {
|
||||
m := &displayMockSystrayMenu{}
|
||||
p.menus = append(p.menus, m)
|
||||
return m
|
||||
}
|
||||
|
||||
type displayMockTray struct {
|
||||
tooltip, label string
|
||||
menu systray.PlatformMenu
|
||||
}
|
||||
|
||||
func (t *displayMockTray) SetIcon(data []byte) {}
|
||||
func (t *displayMockTray) SetTemplateIcon(data []byte) {}
|
||||
func (t *displayMockTray) SetTooltip(text string) { t.tooltip = text }
|
||||
func (t *displayMockTray) SetLabel(text string) { t.label = text }
|
||||
func (t *displayMockTray) SetMenu(m systray.PlatformMenu) { t.menu = m }
|
||||
func (t *displayMockTray) AttachWindow(w systray.WindowHandle) {}
|
||||
|
||||
type displayMockSystrayMenu struct {
|
||||
items []string
|
||||
}
|
||||
|
||||
func (m *displayMockSystrayMenu) Add(label string) systray.PlatformMenuItem {
|
||||
m.items = append(m.items, label)
|
||||
return &displayMockSystrayMenuItem{}
|
||||
}
|
||||
func (m *displayMockSystrayMenu) AddSeparator() { m.items = append(m.items, "---") }
|
||||
|
||||
type displayMockSystrayMenuItem struct{}
|
||||
|
||||
func (mi *displayMockSystrayMenuItem) SetTooltip(text string) {}
|
||||
func (mi *displayMockSystrayMenuItem) SetChecked(checked bool) {}
|
||||
func (mi *displayMockSystrayMenuItem) SetEnabled(enabled bool) {}
|
||||
func (mi *displayMockSystrayMenuItem) OnClick(fn func()) {}
|
||||
func (mi *displayMockSystrayMenuItem) AddSubmenu() systray.PlatformMenu {
|
||||
return &displayMockSystrayMenu{}
|
||||
}
|
||||
|
||||
// displayMockMenuPlatform implements menu.Platform for display tests.
|
||||
type displayMockMenuPlatform struct {
|
||||
appMenu menu.PlatformMenu
|
||||
}
|
||||
|
||||
func (p *displayMockMenuPlatform) NewMenu() menu.PlatformMenu {
|
||||
return &displayMockMenu{}
|
||||
}
|
||||
|
||||
func (p *displayMockMenuPlatform) SetApplicationMenu(m menu.PlatformMenu) {
|
||||
p.appMenu = m
|
||||
}
|
||||
|
||||
type displayMockMenu struct {
|
||||
items []string
|
||||
}
|
||||
|
||||
func (m *displayMockMenu) Add(label string) menu.PlatformMenuItem {
|
||||
m.items = append(m.items, label)
|
||||
return &displayMockMenuItem{}
|
||||
}
|
||||
func (m *displayMockMenu) AddSeparator() { m.items = append(m.items, "---") }
|
||||
func (m *displayMockMenu) AddSubmenu(label string) menu.PlatformMenu {
|
||||
m.items = append(m.items, label)
|
||||
return &displayMockMenu{}
|
||||
}
|
||||
func (m *displayMockMenu) AddRole(role menu.MenuRole) {}
|
||||
|
||||
type displayMockMenuItem struct{}
|
||||
|
||||
func (mi *displayMockMenuItem) SetAccelerator(accel string) menu.PlatformMenuItem { return mi }
|
||||
func (mi *displayMockMenuItem) SetTooltip(text string) menu.PlatformMenuItem { return mi }
|
||||
func (mi *displayMockMenuItem) SetChecked(checked bool) menu.PlatformMenuItem { return mi }
|
||||
func (mi *displayMockMenuItem) SetEnabled(enabled bool) menu.PlatformMenuItem { return mi }
|
||||
func (mi *displayMockMenuItem) OnClick(fn func()) menu.PlatformMenuItem { return mi }
|
||||
|
||||
// --- Test helpers ---
|
||||
|
||||
// newServiceWithMocks creates a Service with mock sub-managers for testing.
|
||||
// Uses a temp directory for state/layout persistence to avoid loading real saved state.
|
||||
func newServiceWithMocks(t *testing.T) (*Service, *mockApp, *displayMockWindowPlatform) {
|
||||
service, err := New()
|
||||
require.NoError(t, err)
|
||||
|
||||
mock := newMockApp()
|
||||
service.app = mock
|
||||
|
||||
wp := &displayMockWindowPlatform{}
|
||||
service.windows = window.NewManagerWithDir(wp, t.TempDir())
|
||||
service.tray = systray.NewManager(&displayMockSystrayPlatform{})
|
||||
service.menus = menu.NewManager(&displayMockMenuPlatform{})
|
||||
|
||||
return service, mock, wp
|
||||
}
|
||||
|
||||
// newTestDisplayService creates a display service registered with Core for IPC testing.
|
||||
func newTestDisplayService(t *testing.T) (*Service, *core.Core) {
|
||||
t.Helper()
|
||||
|
|
@ -192,6 +27,21 @@ func newTestDisplayService(t *testing.T) (*Service, *core.Core) {
|
|||
return svc, c
|
||||
}
|
||||
|
||||
// newTestConclave creates a full 4-service conclave for integration testing.
|
||||
func newTestConclave(t *testing.T) *core.Core {
|
||||
t.Helper()
|
||||
c, err := core.New(
|
||||
core.WithService(Register(nil)),
|
||||
core.WithService(window.Register(window.NewMockPlatform())),
|
||||
core.WithService(systray.Register(systray.NewMockPlatform())),
|
||||
core.WithService(menu.Register(menu.NewMockPlatform())),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
||||
return c
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
|
|
@ -262,25 +112,70 @@ func TestConfigTask_Good(t *testing.T) {
|
|||
assert.Equal(t, 800, cfg["default_width"])
|
||||
}
|
||||
|
||||
func TestOpenWindow_Good(t *testing.T) {
|
||||
t.Run("creates window with default options", func(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
// --- Conclave integration tests ---
|
||||
|
||||
err := service.OpenWindow()
|
||||
func TestServiceConclave_Good(t *testing.T) {
|
||||
c := newTestConclave(t)
|
||||
|
||||
// Open a window via IPC
|
||||
result, handled, err := c.PERFORM(window.TaskOpenWindow{
|
||||
Opts: []window.WindowOption{window.WithName("main")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
info := result.(window.WindowInfo)
|
||||
assert.Equal(t, "main", info.Name)
|
||||
|
||||
// Query window config from display
|
||||
val, handled, err := c.QUERY(window.QueryConfig{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.NotNil(t, val)
|
||||
|
||||
// Set app menu via IPC
|
||||
_, handled, err = c.PERFORM(menu.TaskSetAppMenu{Items: []menu.MenuItem{
|
||||
{Label: "File"},
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
// Query app menu via IPC
|
||||
menuResult, handled, _ := c.QUERY(menu.QueryGetAppMenu{})
|
||||
assert.True(t, handled)
|
||||
items := menuResult.([]menu.MenuItem)
|
||||
assert.Len(t, items, 1)
|
||||
}
|
||||
|
||||
func TestServiceConclave_Bad(t *testing.T) {
|
||||
// Sub-service starts without display — config QUERY returns handled=false
|
||||
c, err := core.New(
|
||||
core.WithService(window.Register(window.NewMockPlatform())),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
||||
|
||||
_, handled, _ := c.QUERY(window.QueryConfig{})
|
||||
assert.False(t, handled, "no display service means no config handler")
|
||||
}
|
||||
|
||||
// --- IPC delegation tests (full conclave) ---
|
||||
|
||||
func TestOpenWindow_Good(t *testing.T) {
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
||||
t.Run("creates window with default options", func(t *testing.T) {
|
||||
err := svc.OpenWindow()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify window was created through the platform
|
||||
assert.Len(t, wp.windows, 1)
|
||||
assert.Equal(t, "main", wp.windows[0].name)
|
||||
assert.Equal(t, "Core", wp.windows[0].title)
|
||||
assert.Equal(t, 1280, wp.windows[0].width)
|
||||
assert.Equal(t, 800, wp.windows[0].height)
|
||||
// Verify via IPC query
|
||||
infos := svc.ListWindowInfos()
|
||||
assert.GreaterOrEqual(t, len(infos), 1)
|
||||
})
|
||||
|
||||
t.Run("creates window with custom options", func(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
|
||||
err := service.OpenWindow(
|
||||
err := svc.OpenWindow(
|
||||
window.WithName("custom-window"),
|
||||
window.WithTitle("Custom Title"),
|
||||
window.WithSize(640, 480),
|
||||
|
|
@ -288,27 +183,25 @@ func TestOpenWindow_Good(t *testing.T) {
|
|||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, wp.windows, 1)
|
||||
assert.Equal(t, "custom-window", wp.windows[0].name)
|
||||
assert.Equal(t, "Custom Title", wp.windows[0].title)
|
||||
assert.Equal(t, 640, wp.windows[0].width)
|
||||
assert.Equal(t, 480, wp.windows[0].height)
|
||||
result, _, _ := c.QUERY(window.QueryWindowByName{Name: "custom-window"})
|
||||
info := result.(*window.WindowInfo)
|
||||
assert.Equal(t, "custom-window", info.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetWindowInfo_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
||||
_ = service.OpenWindow(
|
||||
_ = svc.OpenWindow(
|
||||
window.WithName("test-win"),
|
||||
window.WithSize(800, 600),
|
||||
)
|
||||
|
||||
// Set position on the mock window
|
||||
wp.windows[0].x = 100
|
||||
wp.windows[0].y = 200
|
||||
// Modify position via IPC
|
||||
_, _, _ = c.PERFORM(window.TaskSetPosition{Name: "test-win", X: 100, Y: 200})
|
||||
|
||||
info, err := service.GetWindowInfo("test-win")
|
||||
info, err := svc.GetWindowInfo("test-win")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-win", info.Name)
|
||||
assert.Equal(t, 100, info.X)
|
||||
|
|
@ -318,144 +211,165 @@ func TestGetWindowInfo_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetWindowInfo_Bad(t *testing.T) {
|
||||
service, _, _ := newServiceWithMocks(t)
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
||||
_, err := service.GetWindowInfo("nonexistent")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "window not found")
|
||||
info, err := svc.GetWindowInfo("nonexistent")
|
||||
// QueryWindowByName returns nil for nonexistent — handled=true, result=nil
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, info)
|
||||
}
|
||||
|
||||
func TestListWindowInfos_Good(t *testing.T) {
|
||||
service, _, _ := newServiceWithMocks(t)
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
||||
_ = service.OpenWindow(window.WithName("win-1"))
|
||||
_ = service.OpenWindow(window.WithName("win-2"))
|
||||
_ = svc.OpenWindow(window.WithName("win-1"))
|
||||
_ = svc.OpenWindow(window.WithName("win-2"))
|
||||
|
||||
infos := service.ListWindowInfos()
|
||||
infos := svc.ListWindowInfos()
|
||||
assert.Len(t, infos, 2)
|
||||
}
|
||||
|
||||
func TestSetWindowPosition_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("pos-win"))
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("pos-win"))
|
||||
|
||||
err := service.SetWindowPosition("pos-win", 300, 400)
|
||||
err := svc.SetWindowPosition("pos-win", 300, 400)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 300, wp.windows[0].x)
|
||||
assert.Equal(t, 400, wp.windows[0].y)
|
||||
|
||||
info, _ := svc.GetWindowInfo("pos-win")
|
||||
assert.Equal(t, 300, info.X)
|
||||
assert.Equal(t, 400, info.Y)
|
||||
}
|
||||
|
||||
func TestSetWindowPosition_Bad(t *testing.T) {
|
||||
service, _, _ := newServiceWithMocks(t)
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
||||
err := service.SetWindowPosition("nonexistent", 0, 0)
|
||||
err := svc.SetWindowPosition("nonexistent", 0, 0)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetWindowSize_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("size-win"))
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("size-win"))
|
||||
|
||||
err := service.SetWindowSize("size-win", 1024, 768)
|
||||
err := svc.SetWindowSize("size-win", 1024, 768)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1024, wp.windows[0].width)
|
||||
assert.Equal(t, 768, wp.windows[0].height)
|
||||
|
||||
info, _ := svc.GetWindowInfo("size-win")
|
||||
assert.Equal(t, 1024, info.Width)
|
||||
assert.Equal(t, 768, info.Height)
|
||||
}
|
||||
|
||||
func TestMaximizeWindow_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("max-win"))
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("max-win"))
|
||||
|
||||
err := service.MaximizeWindow("max-win")
|
||||
err := svc.MaximizeWindow("max-win")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, wp.windows[0].maximised)
|
||||
|
||||
info, _ := svc.GetWindowInfo("max-win")
|
||||
assert.True(t, info.Maximized)
|
||||
}
|
||||
|
||||
func TestRestoreWindow_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("restore-win"))
|
||||
wp.windows[0].maximised = true
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("restore-win"))
|
||||
_ = svc.MaximizeWindow("restore-win")
|
||||
|
||||
err := service.RestoreWindow("restore-win")
|
||||
err := svc.RestoreWindow("restore-win")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, wp.windows[0].maximised)
|
||||
|
||||
info, _ := svc.GetWindowInfo("restore-win")
|
||||
assert.False(t, info.Maximized)
|
||||
}
|
||||
|
||||
func TestFocusWindow_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("focus-win"))
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("focus-win"))
|
||||
|
||||
err := service.FocusWindow("focus-win")
|
||||
err := svc.FocusWindow("focus-win")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, wp.windows[0].focused)
|
||||
|
||||
info, _ := svc.GetWindowInfo("focus-win")
|
||||
assert.True(t, info.Focused)
|
||||
}
|
||||
|
||||
func TestCloseWindow_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("close-win"))
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("close-win"))
|
||||
|
||||
err := service.CloseWindow("close-win")
|
||||
err := svc.CloseWindow("close-win")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, wp.windows[0].closed)
|
||||
|
||||
// Window should be removed from manager
|
||||
_, ok := service.windows.Get("close-win")
|
||||
assert.False(t, ok)
|
||||
// Window should be removed
|
||||
info, _ := svc.GetWindowInfo("close-win")
|
||||
assert.Nil(t, info)
|
||||
}
|
||||
|
||||
func TestSetWindowVisibility_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("vis-win"))
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("vis-win"))
|
||||
|
||||
err := service.SetWindowVisibility("vis-win", false)
|
||||
err := svc.SetWindowVisibility("vis-win", false)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, wp.windows[0].visible)
|
||||
|
||||
err = service.SetWindowVisibility("vis-win", true)
|
||||
err = svc.SetWindowVisibility("vis-win", true)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, wp.windows[0].visible)
|
||||
}
|
||||
|
||||
func TestSetWindowAlwaysOnTop_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("ontop-win"))
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("ontop-win"))
|
||||
|
||||
err := service.SetWindowAlwaysOnTop("ontop-win", true)
|
||||
err := svc.SetWindowAlwaysOnTop("ontop-win", true)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, wp.windows[0].alwaysOnTop)
|
||||
}
|
||||
|
||||
func TestSetWindowTitle_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("title-win"))
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("title-win"))
|
||||
|
||||
err := service.SetWindowTitle("title-win", "New Title")
|
||||
err := svc.SetWindowTitle("title-win", "New Title")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "New Title", wp.windows[0].title)
|
||||
}
|
||||
|
||||
func TestGetFocusedWindow_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("win-a"))
|
||||
_ = service.OpenWindow(window.WithName("win-b"))
|
||||
wp.windows[1].focused = true
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("win-a"))
|
||||
_ = svc.OpenWindow(window.WithName("win-b"))
|
||||
_ = svc.FocusWindow("win-b")
|
||||
|
||||
focused := service.GetFocusedWindow()
|
||||
focused := svc.GetFocusedWindow()
|
||||
assert.Equal(t, "win-b", focused)
|
||||
}
|
||||
|
||||
func TestGetFocusedWindow_NoneSelected(t *testing.T) {
|
||||
service, _, _ := newServiceWithMocks(t)
|
||||
_ = service.OpenWindow(window.WithName("win-a"))
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
_ = svc.OpenWindow(window.WithName("win-a"))
|
||||
|
||||
focused := service.GetFocusedWindow()
|
||||
focused := svc.GetFocusedWindow()
|
||||
assert.Equal(t, "", focused)
|
||||
}
|
||||
|
||||
func TestCreateWindow_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
||||
info, err := service.CreateWindow(CreateWindowOptions{
|
||||
info, err := svc.CreateWindow(CreateWindowOptions{
|
||||
Name: "new-win",
|
||||
Title: "New Window",
|
||||
URL: "/new",
|
||||
|
|
@ -464,106 +378,57 @@ func TestCreateWindow_Good(t *testing.T) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "new-win", info.Name)
|
||||
assert.Equal(t, 600, info.Width)
|
||||
assert.Equal(t, 400, info.Height)
|
||||
assert.Len(t, wp.windows, 1)
|
||||
}
|
||||
|
||||
func TestCreateWindow_Bad(t *testing.T) {
|
||||
service, _, _ := newServiceWithMocks(t)
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
||||
_, err := service.CreateWindow(CreateWindowOptions{})
|
||||
_, err := svc.CreateWindow(CreateWindowOptions{})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "window name is required")
|
||||
}
|
||||
|
||||
func TestShowEnvironmentDialog_Good(t *testing.T) {
|
||||
service, mock, _ := newServiceWithMocks(t)
|
||||
func TestResetWindowState_Good(t *testing.T) {
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
||||
// This will panic because Dialog().Info() returns nil
|
||||
// We're verifying the env info is accessed, not that a dialog shows
|
||||
assert.NotPanics(t, func() {
|
||||
defer func() { recover() }() // Recover from nil dialog
|
||||
service.ShowEnvironmentDialog()
|
||||
})
|
||||
|
||||
// Verify dialog was requested (even though it's nil)
|
||||
assert.Equal(t, 1, mock.dialogManager.infoDialogsCreated)
|
||||
err := svc.ResetWindowState()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBuildMenu_Good(t *testing.T) {
|
||||
service, _, _ := newServiceWithMocks(t)
|
||||
c, _ := core.New()
|
||||
service.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{})
|
||||
func TestGetSavedWindowStates_Good(t *testing.T) {
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
||||
// buildMenu should not panic with mock platforms
|
||||
assert.NotPanics(t, func() {
|
||||
service.buildMenu()
|
||||
})
|
||||
states := svc.GetSavedWindowStates()
|
||||
assert.NotNil(t, states)
|
||||
}
|
||||
|
||||
func TestSetupTray_Good(t *testing.T) {
|
||||
service, _, _ := newServiceWithMocks(t)
|
||||
c, _ := core.New()
|
||||
service.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{})
|
||||
func TestHandleIPCEvents_WindowOpened_Good(t *testing.T) {
|
||||
c := newTestConclave(t)
|
||||
|
||||
// setupTray should not panic with mock platforms
|
||||
assert.NotPanics(t, func() {
|
||||
service.setupTray()
|
||||
// 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")},
|
||||
})
|
||||
|
||||
// Verify tray is active
|
||||
assert.True(t, service.tray.IsActive())
|
||||
}
|
||||
|
||||
func TestHandleNewWorkspace_Good(t *testing.T) {
|
||||
service, _, wp := newServiceWithMocks(t)
|
||||
|
||||
service.handleNewWorkspace()
|
||||
|
||||
// Verify a window was created with correct options
|
||||
assert.Len(t, wp.windows, 1)
|
||||
assert.Equal(t, "workspace-new", wp.windows[0].name)
|
||||
assert.Equal(t, "New Workspace", wp.windows[0].title)
|
||||
assert.Equal(t, 500, wp.windows[0].width)
|
||||
assert.Equal(t, 400, wp.windows[0].height)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
info := result.(window.WindowInfo)
|
||||
assert.Equal(t, "test", info.Name)
|
||||
}
|
||||
|
||||
func TestHandleListWorkspaces_Good(t *testing.T) {
|
||||
service, _, _ := newServiceWithMocks(t)
|
||||
c, _ := core.New()
|
||||
service.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{})
|
||||
c := newTestConclave(t)
|
||||
svc := core.MustServiceFor[*Service](c, "display")
|
||||
|
||||
// handleListWorkspaces should not panic when workspace service is not available
|
||||
assert.NotPanics(t, func() {
|
||||
service.handleListWorkspaces()
|
||||
svc.handleListWorkspaces()
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleSaveFile_Good(t *testing.T) {
|
||||
service, mock, _ := newServiceWithMocks(t)
|
||||
|
||||
service.handleSaveFile()
|
||||
|
||||
assert.Contains(t, mock.eventManager.emittedEvents, "ide:save")
|
||||
}
|
||||
|
||||
func TestHandleRun_Good(t *testing.T) {
|
||||
service, mock, _ := newServiceWithMocks(t)
|
||||
|
||||
service.handleRun()
|
||||
|
||||
assert.Contains(t, mock.eventManager.emittedEvents, "ide:run")
|
||||
}
|
||||
|
||||
func TestHandleBuild_Good(t *testing.T) {
|
||||
service, mock, _ := newServiceWithMocks(t)
|
||||
|
||||
service.handleBuild()
|
||||
|
||||
assert.Contains(t, mock.eventManager.emittedEvents, "ide:build")
|
||||
}
|
||||
|
||||
func TestWSEventManager_Good(t *testing.T) {
|
||||
es := newMockEventSource()
|
||||
em := NewWSEventManager(es)
|
||||
|
|
@ -583,37 +448,3 @@ func TestWSEventManager_SetupWindowEventListeners_Good(t *testing.T) {
|
|||
// Verify theme handler was registered
|
||||
assert.Len(t, es.themeHandlers, 1)
|
||||
}
|
||||
|
||||
func TestResetWindowState_Good(t *testing.T) {
|
||||
service, _, _ := newServiceWithMocks(t)
|
||||
|
||||
err := service.ResetWindowState()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetSavedWindowStates_Good(t *testing.T) {
|
||||
service, _, _ := newServiceWithMocks(t)
|
||||
|
||||
states := service.GetSavedWindowStates()
|
||||
assert.NotNil(t, states)
|
||||
}
|
||||
|
||||
func TestHandleIPCEvents_WindowOpened_Good(t *testing.T) {
|
||||
c, err := core.New(
|
||||
core.WithService(Register(nil)),
|
||||
core.WithService(window.Register(window.NewMockPlatform())),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
||||
|
||||
// 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")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
info := result.(window.WindowInfo)
|
||||
assert.Equal(t, "test", info.Name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,116 +1,5 @@
|
|||
package display
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
// mockApp is a mock implementation of the App interface for testing.
|
||||
type mockApp struct {
|
||||
dialogManager *mockDialogManager
|
||||
envManager *mockEnvManager
|
||||
eventManager *mockEventManager
|
||||
logger *mockLogger
|
||||
quitCalled bool
|
||||
}
|
||||
|
||||
func newMockApp() *mockApp {
|
||||
return &mockApp{
|
||||
dialogManager: newMockDialogManager(),
|
||||
envManager: newMockEnvManager(),
|
||||
eventManager: newMockEventManager(),
|
||||
logger: &mockLogger{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockApp) Dialog() DialogManager { return m.dialogManager }
|
||||
func (m *mockApp) Env() EnvManager { return m.envManager }
|
||||
func (m *mockApp) Event() EventManager { return m.eventManager }
|
||||
func (m *mockApp) Logger() Logger { return m.logger }
|
||||
func (m *mockApp) Quit() { m.quitCalled = true }
|
||||
|
||||
// mockDialogManager tracks dialog creation calls.
|
||||
type mockDialogManager struct {
|
||||
infoDialogsCreated int
|
||||
warningDialogsCreated int
|
||||
}
|
||||
|
||||
func newMockDialogManager() *mockDialogManager {
|
||||
return &mockDialogManager{}
|
||||
}
|
||||
|
||||
func (m *mockDialogManager) Info() *application.MessageDialog {
|
||||
m.infoDialogsCreated++
|
||||
return nil // Can't create real dialog without Wails runtime
|
||||
}
|
||||
|
||||
func (m *mockDialogManager) Warning() *application.MessageDialog {
|
||||
m.warningDialogsCreated++
|
||||
return nil // Can't create real dialog without Wails runtime
|
||||
}
|
||||
|
||||
func (m *mockDialogManager) OpenFile() *application.OpenFileDialogStruct {
|
||||
return nil // Can't create real dialog without Wails runtime
|
||||
}
|
||||
|
||||
// mockEnvManager provides mock environment info.
|
||||
type mockEnvManager struct {
|
||||
envInfo application.EnvironmentInfo
|
||||
darkMode bool
|
||||
}
|
||||
|
||||
func newMockEnvManager() *mockEnvManager {
|
||||
return &mockEnvManager{
|
||||
envInfo: application.EnvironmentInfo{
|
||||
OS: "test-os",
|
||||
Arch: "test-arch",
|
||||
Debug: true,
|
||||
PlatformInfo: map[string]any{"test": "value"},
|
||||
},
|
||||
darkMode: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockEnvManager) Info() application.EnvironmentInfo {
|
||||
return m.envInfo
|
||||
}
|
||||
|
||||
func (m *mockEnvManager) IsDarkMode() bool {
|
||||
return m.darkMode
|
||||
}
|
||||
|
||||
// mockEventManager tracks event registration.
|
||||
type mockEventManager struct {
|
||||
registeredEvents []events.ApplicationEventType
|
||||
emittedEvents []string
|
||||
}
|
||||
|
||||
func newMockEventManager() *mockEventManager {
|
||||
return &mockEventManager{
|
||||
registeredEvents: make([]events.ApplicationEventType, 0),
|
||||
emittedEvents: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockEventManager) OnApplicationEvent(eventType events.ApplicationEventType, handler func(*application.ApplicationEvent)) func() {
|
||||
m.registeredEvents = append(m.registeredEvents, eventType)
|
||||
return func() {} // Return a no-op unsubscribe function
|
||||
}
|
||||
|
||||
func (m *mockEventManager) Emit(name string, data ...any) bool {
|
||||
m.emittedEvents = append(m.emittedEvents, name)
|
||||
return true // Pretend emission succeeded
|
||||
}
|
||||
|
||||
// mockLogger tracks log calls.
|
||||
type mockLogger struct {
|
||||
infoMessages []string
|
||||
}
|
||||
|
||||
func (m *mockLogger) Info(message string, args ...any) {
|
||||
m.infoMessages = append(m.infoMessages, message)
|
||||
}
|
||||
|
||||
// mockEventSource implements EventSource for testing.
|
||||
type mockEventSource struct {
|
||||
themeHandlers []func(isDark bool)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue