feat(display): add HandleIPCEvents IPC->WS bridge
Display HandleIPCEvents converts sub-service actions to WS events. ActionServiceStartup triggers buildMenu/setupTray after all services start. Export mock platforms from each sub-package for integration tests. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
d29cf62e6f
commit
0893456a9e
5 changed files with 236 additions and 0 deletions
|
|
@ -77,6 +77,96 @@ func (s *Service) OnStartup(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
case window.ActionWindowOpened:
|
||||
if s.events != nil {
|
||||
s.events.Emit(Event{Type: EventWindowCreate, Window: m.Name,
|
||||
Data: map[string]any{"name": m.Name}})
|
||||
}
|
||||
case window.ActionWindowClosed:
|
||||
if s.events != nil {
|
||||
s.events.Emit(Event{Type: EventWindowClose, Window: m.Name,
|
||||
Data: map[string]any{"name": m.Name}})
|
||||
}
|
||||
case window.ActionWindowMoved:
|
||||
if s.events != nil {
|
||||
s.events.Emit(Event{Type: EventWindowMove, Window: m.Name,
|
||||
Data: map[string]any{"x": m.X, "y": m.Y}})
|
||||
}
|
||||
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}})
|
||||
}
|
||||
case window.ActionWindowFocused:
|
||||
if s.events != nil {
|
||||
s.events.Emit(Event{Type: EventWindowFocus, Window: m.Name})
|
||||
}
|
||||
case window.ActionWindowBlurred:
|
||||
if s.events != nil {
|
||||
s.events.Emit(Event{Type: EventWindowBlur, Window: m.Name})
|
||||
}
|
||||
case systray.ActionTrayClicked:
|
||||
if s.events != nil {
|
||||
s.events.Emit(Event{Type: EventTrayClick})
|
||||
}
|
||||
case systray.ActionTrayMenuItemClicked:
|
||||
if s.events != nil {
|
||||
s.events.Emit(Event{Type: EventTrayMenuItemClick,
|
||||
Data: map[string]any{"actionId": m.ActionID}})
|
||||
}
|
||||
s.handleTrayAction(m.ActionID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleTrayAction processes tray menu item clicks.
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
case "quit":
|
||||
if s.app != nil {
|
||||
s.app.Quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) loadConfig() {
|
||||
// In-memory defaults. go-config integration is deferred work.
|
||||
if s.configData == nil {
|
||||
|
|
|
|||
|
|
@ -597,3 +597,23 @@ func TestGetSavedWindowStates_Good(t *testing.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)
|
||||
}
|
||||
|
|
|
|||
24
pkg/menu/mock_platform.go
Normal file
24
pkg/menu/mock_platform.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package menu
|
||||
|
||||
// MockPlatform is an exported mock for cross-package integration tests.
|
||||
type MockPlatform struct{}
|
||||
|
||||
func NewMockPlatform() *MockPlatform { return &MockPlatform{} }
|
||||
|
||||
func (m *MockPlatform) NewMenu() PlatformMenu { return &exportedMockPlatformMenu{} }
|
||||
func (m *MockPlatform) SetApplicationMenu(menu PlatformMenu) {}
|
||||
|
||||
type exportedMockPlatformMenu struct{}
|
||||
|
||||
func (m *exportedMockPlatformMenu) Add(label string) PlatformMenuItem { return &exportedMockPlatformMenuItem{} }
|
||||
func (m *exportedMockPlatformMenu) AddSeparator() {}
|
||||
func (m *exportedMockPlatformMenu) AddSubmenu(label string) PlatformMenu { return &exportedMockPlatformMenu{} }
|
||||
func (m *exportedMockPlatformMenu) AddRole(role MenuRole) {}
|
||||
|
||||
type exportedMockPlatformMenuItem struct{}
|
||||
|
||||
func (mi *exportedMockPlatformMenuItem) SetAccelerator(acc string) PlatformMenuItem { return mi }
|
||||
func (mi *exportedMockPlatformMenuItem) SetTooltip(tip string) PlatformMenuItem { return mi }
|
||||
func (mi *exportedMockPlatformMenuItem) SetChecked(checked bool) PlatformMenuItem { return mi }
|
||||
func (mi *exportedMockPlatformMenuItem) SetEnabled(enabled bool) PlatformMenuItem { return mi }
|
||||
func (mi *exportedMockPlatformMenuItem) OnClick(fn func()) PlatformMenuItem { return mi }
|
||||
42
pkg/systray/mock_platform.go
Normal file
42
pkg/systray/mock_platform.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package systray
|
||||
|
||||
// MockPlatform is an exported mock for cross-package integration tests.
|
||||
type MockPlatform struct{}
|
||||
|
||||
func NewMockPlatform() *MockPlatform { return &MockPlatform{} }
|
||||
|
||||
func (m *MockPlatform) NewTray() PlatformTray { return &exportedMockTray{} }
|
||||
func (m *MockPlatform) NewMenu() PlatformMenu { return &exportedMockMenu{} }
|
||||
|
||||
type exportedMockTray struct {
|
||||
icon, templateIcon []byte
|
||||
tooltip, label string
|
||||
}
|
||||
|
||||
func (t *exportedMockTray) SetIcon(data []byte) { t.icon = data }
|
||||
func (t *exportedMockTray) SetTemplateIcon(data []byte) { t.templateIcon = data }
|
||||
func (t *exportedMockTray) SetTooltip(text string) { t.tooltip = text }
|
||||
func (t *exportedMockTray) SetLabel(text string) { t.label = text }
|
||||
func (t *exportedMockTray) SetMenu(menu PlatformMenu) {}
|
||||
func (t *exportedMockTray) AttachWindow(w WindowHandle) {}
|
||||
|
||||
type exportedMockMenu struct{ items []exportedMockMenuItem }
|
||||
|
||||
func (m *exportedMockMenu) Add(label string) PlatformMenuItem {
|
||||
mi := &exportedMockMenuItem{label: label}
|
||||
m.items = append(m.items, *mi)
|
||||
return mi
|
||||
}
|
||||
func (m *exportedMockMenu) AddSeparator() {}
|
||||
|
||||
type exportedMockMenuItem struct {
|
||||
label, tooltip string
|
||||
checked, enabled bool
|
||||
onClick func()
|
||||
}
|
||||
|
||||
func (mi *exportedMockMenuItem) SetTooltip(tip string) { mi.tooltip = tip }
|
||||
func (mi *exportedMockMenuItem) SetChecked(checked bool) { mi.checked = checked }
|
||||
func (mi *exportedMockMenuItem) SetEnabled(enabled bool) { mi.enabled = enabled }
|
||||
func (mi *exportedMockMenuItem) OnClick(fn func()) { mi.onClick = fn }
|
||||
func (mi *exportedMockMenuItem) AddSubmenu() PlatformMenu { return &exportedMockMenu{} }
|
||||
60
pkg/window/mock_platform.go
Normal file
60
pkg/window/mock_platform.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package window
|
||||
|
||||
// MockPlatform is an exported mock for cross-package integration tests.
|
||||
// For internal tests, use the unexported mockPlatform in mock_test.go.
|
||||
type MockPlatform struct {
|
||||
Windows []*MockWindow
|
||||
}
|
||||
|
||||
func NewMockPlatform() *MockPlatform {
|
||||
return &MockPlatform{}
|
||||
}
|
||||
|
||||
func (m *MockPlatform) CreateWindow(opts 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,
|
||||
}
|
||||
m.Windows = append(m.Windows, w)
|
||||
return w
|
||||
}
|
||||
|
||||
func (m *MockPlatform) GetWindows() []PlatformWindow {
|
||||
out := make([]PlatformWindow, len(m.Windows))
|
||||
for i, w := range m.Windows {
|
||||
out[i] = w
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type MockWindow struct {
|
||||
name, title, url string
|
||||
width, height, x, y int
|
||||
maximised, focused bool
|
||||
visible, alwaysOnTop bool
|
||||
closed bool
|
||||
eventHandlers []func(WindowEvent)
|
||||
}
|
||||
|
||||
func (w *MockWindow) Name() string { return w.name }
|
||||
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) }
|
||||
Loading…
Add table
Reference in a new issue