refactor(ax): align WebSocket and window naming
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
7deb91ac0b
commit
f854b65720
16 changed files with 154 additions and 122 deletions
|
|
@ -46,7 +46,7 @@ All Wails application APIs are abstracted behind interfaces in `interfaces.go` (
|
|||
| `window.go` | `WindowOption` functional options pattern, `Window` type alias for `application.WebviewWindowOptions` |
|
||||
| `window_state.go` | `WindowStateManager` — persists window position/size across restarts |
|
||||
| `layout.go` | `LayoutManager` — save/restore named window arrangements |
|
||||
| `events.go` | `WSEventManager` — WebSocket pub/sub for window/theme/screen events |
|
||||
| `events.go` | `WebSocketEventManager` — WebSocket pub/sub for window/theme/screen events |
|
||||
| `interfaces.go` | Abstract interfaces + Wails adapter implementations |
|
||||
| `actions.go` | `ActionOpenWindow` IPC message type |
|
||||
| `menu.go` | Application menu construction |
|
||||
|
|
@ -61,7 +61,7 @@ All Wails application APIs are abstracted behind interfaces in `interfaces.go` (
|
|||
|
||||
- **Functional options**: `WindowOption` functions (`WindowName()`, `WindowTitle()`, `WindowWidth()`, etc.) configure `application.WebviewWindowOptions`
|
||||
- **Type alias**: `Window = application.WebviewWindowOptions` — direct alias, not a wrapper
|
||||
- **Event broadcasting**: `WSEventManager` uses gorilla/websocket with a buffered channel (`eventBuffer`) and per-client subscription filtering (supports `"*"` wildcard)
|
||||
- **Event broadcasting**: `WebSocketEventManager` uses gorilla/websocket with a buffered channel (`eventBuffer`) and per-client subscription filtering (supports `"*"` wildcard)
|
||||
- **Window lookup by name**: Most Service methods iterate `s.app.Window().GetAll()` and type-assert to `*application.WebviewWindow`, then match by `Name()`
|
||||
|
||||
## Testing
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Complete API reference for the Display service (`pkg/display`).
|
|||
## Service Creation
|
||||
|
||||
```go
|
||||
func NewService(c *core.Core) (any, error)
|
||||
func NewService() (*Service, error)
|
||||
```
|
||||
|
||||
## Window Management
|
||||
|
|
@ -346,7 +346,7 @@ type Theme struct {
|
|||
## Events
|
||||
|
||||
```go
|
||||
func (s *Service) GetEventManager() *EventManager
|
||||
func (s *Service) GetWebSocketEventManager() *WebSocketEventManager
|
||||
```
|
||||
|
||||
The EventManager handles WebSocket connections for real-time events.
|
||||
The WebSocketEventManager handles WebSocket connections for real-time events.
|
||||
|
|
|
|||
|
|
@ -42,11 +42,11 @@ type Service struct {
|
|||
app App
|
||||
configData map[string]map[string]any
|
||||
configFile *config.Config // config instance for file persistence
|
||||
events *WSEventManager
|
||||
events *WebSocketEventManager
|
||||
}
|
||||
|
||||
// New is the constructor for the display service.
|
||||
func New() (*Service, error) {
|
||||
// NewService constructs the display service.
|
||||
func NewService() (*Service, error) {
|
||||
return &Service{
|
||||
configData: map[string]map[string]any{
|
||||
"window": {},
|
||||
|
|
@ -56,11 +56,16 @@ func New() (*Service, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// New is kept for backward compatibility with older callers.
|
||||
func New() (*Service, error) {
|
||||
return NewService()
|
||||
}
|
||||
|
||||
// Register creates a factory closure that captures the Wails app.
|
||||
// Pass nil for testing without a Wails runtime.
|
||||
func Register(wailsApp *application.App) func(*core.Core) (any, error) {
|
||||
return func(c *core.Core) (any, error) {
|
||||
s, err := New()
|
||||
s, err := NewService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -83,7 +88,7 @@ func (s *Service) OnStartup(ctx context.Context) error {
|
|||
// Initialise Wails wrappers if app is available (nil in tests)
|
||||
if s.wailsApp != nil {
|
||||
s.app = newWailsApp(s.wailsApp)
|
||||
s.events = NewWSEventManager()
|
||||
s.events = NewWebSocketEventManager()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -230,23 +235,26 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// WSMessage represents a command received from a WebSocket client.
|
||||
type WSMessage struct {
|
||||
// WebSocketMessage represents a command received from a WebSocket client.
|
||||
type WebSocketMessage struct {
|
||||
Action string `json:"action"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// wsRequire extracts a string field from WS data and returns an error if it is empty.
|
||||
func wsRequire(data map[string]any, key string) (string, error) {
|
||||
// WSMessage is kept for backward compatibility with older callers.
|
||||
type WSMessage = WebSocketMessage
|
||||
|
||||
// requireWebSocketField extracts a string field from WebSocket data and returns an error if it is empty.
|
||||
func requireWebSocketField(data map[string]any, key string) (string, error) {
|
||||
v, _ := data[key].(string)
|
||||
if v == "" {
|
||||
return "", coreerr.E("display.wsRequire", "missing required field \""+key+"\"", nil)
|
||||
return "", coreerr.E("display.requireWebSocketField", "missing required field \""+key+"\"", nil)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// handleWSMessage bridges WebSocket commands to IPC calls.
|
||||
func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
|
||||
// handleWebSocketMessage bridges WebSocket commands to IPC calls.
|
||||
func (s *Service) handleWebSocketMessage(msg WebSocketMessage) (any, bool, error) {
|
||||
var result any
|
||||
var handled bool
|
||||
var err error
|
||||
|
|
@ -299,51 +307,51 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
|
|||
case "contextmenu:list":
|
||||
result, handled, err = s.Core().QUERY(contextmenu.QueryList{})
|
||||
case "webview:eval":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
script, _ := msg.Data["script"].(string)
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskEvaluate{Window: w, Script: script})
|
||||
case "webview:click":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
sel, e := wsRequire(msg.Data, "selector")
|
||||
sel, e := requireWebSocketField(msg.Data, "selector")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskClick{Window: w, Selector: sel})
|
||||
case "webview:type":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
sel, e := wsRequire(msg.Data, "selector")
|
||||
sel, e := requireWebSocketField(msg.Data, "selector")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
text, _ := msg.Data["text"].(string)
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskType{Window: w, Selector: sel, Text: text})
|
||||
case "webview:navigate":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
url, e := wsRequire(msg.Data, "url")
|
||||
url, e := requireWebSocketField(msg.Data, "url")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskNavigate{Window: w, URL: url})
|
||||
case "webview:screenshot":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskScreenshot{Window: w})
|
||||
case "webview:scroll":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
|
|
@ -351,43 +359,43 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
|
|||
y, _ := msg.Data["y"].(float64)
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskScroll{Window: w, X: int(x), Y: int(y)})
|
||||
case "webview:hover":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
sel, e := wsRequire(msg.Data, "selector")
|
||||
sel, e := requireWebSocketField(msg.Data, "selector")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskHover{Window: w, Selector: sel})
|
||||
case "webview:select":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
sel, e := wsRequire(msg.Data, "selector")
|
||||
sel, e := requireWebSocketField(msg.Data, "selector")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
val, _ := msg.Data["value"].(string)
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskSelect{Window: w, Selector: sel, Value: val})
|
||||
case "webview:check":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
sel, e := wsRequire(msg.Data, "selector")
|
||||
sel, e := requireWebSocketField(msg.Data, "selector")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
checked, _ := msg.Data["checked"].(bool)
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskCheck{Window: w, Selector: sel, Checked: checked})
|
||||
case "webview:upload":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
sel, e := wsRequire(msg.Data, "selector")
|
||||
sel, e := requireWebSocketField(msg.Data, "selector")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
|
|
@ -400,7 +408,7 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
|
|||
}
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskUploadFile{Window: w, Selector: sel, Paths: paths})
|
||||
case "webview:viewport":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
|
|
@ -408,13 +416,13 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
|
|||
height, _ := msg.Data["height"].(float64)
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskSetViewport{Window: w, Width: int(width), Height: int(height)})
|
||||
case "webview:clear-console":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
result, handled, err = s.Core().PERFORM(webview.TaskClearConsole{Window: w})
|
||||
case "webview:console":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
|
|
@ -425,40 +433,40 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
|
|||
}
|
||||
result, handled, err = s.Core().QUERY(webview.QueryConsole{Window: w, Level: level, Limit: limit})
|
||||
case "webview:query":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
sel, e := wsRequire(msg.Data, "selector")
|
||||
sel, e := requireWebSocketField(msg.Data, "selector")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
result, handled, err = s.Core().QUERY(webview.QuerySelector{Window: w, Selector: sel})
|
||||
case "webview:query-all":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
sel, e := wsRequire(msg.Data, "selector")
|
||||
sel, e := requireWebSocketField(msg.Data, "selector")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
result, handled, err = s.Core().QUERY(webview.QuerySelectorAll{Window: w, Selector: sel})
|
||||
case "webview:dom-tree":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
sel, _ := msg.Data["selector"].(string) // selector optional for dom-tree (defaults to root)
|
||||
result, handled, err = s.Core().QUERY(webview.QueryDOMTree{Window: w, Selector: sel})
|
||||
case "webview:url":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
result, handled, err = s.Core().QUERY(webview.QueryURL{Window: w})
|
||||
case "webview:title":
|
||||
w, e := wsRequire(msg.Data, "window")
|
||||
w, e := requireWebSocketField(msg.Data, "window")
|
||||
if e != nil {
|
||||
return nil, false, e
|
||||
}
|
||||
|
|
@ -643,13 +651,13 @@ func (s *Service) SetWindowBounds(name string, x, y, width, height int) error {
|
|||
|
||||
// MaximizeWindow maximizes a window via IPC.
|
||||
func (s *Service) MaximizeWindow(name string) error {
|
||||
_, err := s.performWindowTask("display.MaximizeWindow", window.TaskMaximise{Name: name})
|
||||
_, err := s.performWindowTask("display.MaximizeWindow", window.TaskMaximize{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// MinimizeWindow minimizes a window via IPC.
|
||||
func (s *Service) MinimizeWindow(name string) error {
|
||||
_, err := s.performWindowTask("display.MinimizeWindow", window.TaskMinimise{Name: name})
|
||||
_, err := s.performWindowTask("display.MinimizeWindow", window.TaskMinimize{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -835,11 +843,16 @@ func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// GetEventManager returns the event manager for WebSocket event subscriptions.
|
||||
func (s *Service) GetEventManager() *WSEventManager {
|
||||
// GetWebSocketEventManager returns the event manager for WebSocket event subscriptions.
|
||||
func (s *Service) GetWebSocketEventManager() *WebSocketEventManager {
|
||||
return s.events
|
||||
}
|
||||
|
||||
// GetEventManager is kept for backward compatibility with older callers.
|
||||
func (s *Service) GetEventManager() *WebSocketEventManager {
|
||||
return s.GetWebSocketEventManager()
|
||||
}
|
||||
|
||||
// --- Menu (handlers stay in display, structure delegated via IPC) ---
|
||||
|
||||
func (s *Service) buildMenu() {
|
||||
|
|
|
|||
|
|
@ -46,19 +46,19 @@ func newTestConclave(t *testing.T) *core.Core {
|
|||
|
||||
// --- Tests ---
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
func TestNewService(t *testing.T) {
|
||||
t.Run("creates service successfully", func(t *testing.T) {
|
||||
service, err := New()
|
||||
service, err := NewService()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, service, "New() should return a non-nil service instance")
|
||||
assert.NotNil(t, service, "NewService() should return a non-nil service instance")
|
||||
})
|
||||
|
||||
t.Run("returns independent instances", func(t *testing.T) {
|
||||
service1, err1 := New()
|
||||
service2, err2 := New()
|
||||
service1, err1 := NewService()
|
||||
service2, err2 := NewService()
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.NotSame(t, service1, service2, "New() should return different instances")
|
||||
assert.NotSame(t, service1, service2, "NewService() should return different instances")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -431,7 +431,7 @@ func TestHandleIPCEvents_WindowOpened_Good(t *testing.T) {
|
|||
c := newTestConclave(t)
|
||||
|
||||
// Open a window — this should trigger ActionWindowOpened
|
||||
// which HandleIPCEvents should convert to a WS event
|
||||
// which HandleIPCEvents should convert to a WebSocket event
|
||||
result, handled, err := c.PERFORM(window.TaskOpenWindow{
|
||||
Window: window.Window{Name: "test"},
|
||||
})
|
||||
|
|
@ -451,8 +451,8 @@ func TestHandleListWorkspaces_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestWSEventManager_Good(t *testing.T) {
|
||||
em := NewWSEventManager()
|
||||
func TestWebSocketEventManager_Good(t *testing.T) {
|
||||
em := NewWebSocketEventManager()
|
||||
defer em.Close()
|
||||
|
||||
assert.NotNil(t, em)
|
||||
|
|
@ -476,7 +476,7 @@ menu:
|
|||
show_dev_tools: false
|
||||
`), 0o644))
|
||||
|
||||
s, _ := New()
|
||||
s, _ := NewService()
|
||||
s.loadConfigFrom(cfgPath)
|
||||
|
||||
// Verify configData was populated from file
|
||||
|
|
@ -486,7 +486,7 @@ menu:
|
|||
}
|
||||
|
||||
func TestLoadConfig_Bad_MissingFile(t *testing.T) {
|
||||
s, _ := New()
|
||||
s, _ := NewService()
|
||||
s.loadConfigFrom(filepath.Join(t.TempDir(), "nonexistent.yaml"))
|
||||
|
||||
// Should not panic, configData stays at empty defaults
|
||||
|
|
@ -499,7 +499,7 @@ func TestHandleConfigTask_Persists_Good(t *testing.T) {
|
|||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "config.yaml")
|
||||
|
||||
s, _ := New()
|
||||
s, _ := NewService()
|
||||
s.loadConfigFrom(cfgPath) // Creates empty config (file doesn't exist yet)
|
||||
|
||||
// Simulate a TaskSaveConfig through the handler
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ The backend is written in Go and uses the `forge.lthn.ai/core/gui/pkg/display` p
|
|||
The `Service` struct is the main entry point for the display logic.
|
||||
|
||||
- **Initialization:**
|
||||
- `New() (*Service, error)`: Creates a new instance of the service.
|
||||
- `NewService() (*Service, error)`: Creates a new instance of the service.
|
||||
- `Register(wailsApp *application.App) func(*core.Core) (any, error)`: Captures the Wails app and registers the service with Core.
|
||||
- `OnStartup(ctx context.Context) error`: Loads config and registers IPC handlers.
|
||||
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ type Subscription struct {
|
|||
EventTypes []EventType `json:"eventTypes"`
|
||||
}
|
||||
|
||||
// WSEventManager manages WebSocket connections and event subscriptions.
|
||||
type WSEventManager struct {
|
||||
// WebSocketEventManager manages WebSocket connections and event subscriptions.
|
||||
type WebSocketEventManager struct {
|
||||
upgrader websocket.Upgrader
|
||||
clients map[*websocket.Conn]*clientState
|
||||
mu sync.RWMutex
|
||||
|
|
@ -71,9 +71,9 @@ type clientState struct {
|
|||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewWSEventManager creates a new event manager.
|
||||
func NewWSEventManager() *WSEventManager {
|
||||
em := &WSEventManager{
|
||||
// NewWebSocketEventManager creates a new WebSocket event manager.
|
||||
func NewWebSocketEventManager() *WebSocketEventManager {
|
||||
em := &WebSocketEventManager{
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // Allow all origins for local dev
|
||||
|
|
@ -91,8 +91,14 @@ func NewWSEventManager() *WSEventManager {
|
|||
return em
|
||||
}
|
||||
|
||||
// WSEventManager is kept for backward compatibility with older callers.
|
||||
type WSEventManager = WebSocketEventManager
|
||||
|
||||
// NewWSEventManager is kept for backward compatibility with older callers.
|
||||
func NewWSEventManager() *WebSocketEventManager { return NewWebSocketEventManager() }
|
||||
|
||||
// broadcaster sends events to all subscribed clients.
|
||||
func (em *WSEventManager) broadcaster() {
|
||||
func (em *WebSocketEventManager) broadcaster() {
|
||||
for event := range em.eventBuffer {
|
||||
em.mu.RLock()
|
||||
for conn, state := range em.clients {
|
||||
|
|
@ -105,7 +111,7 @@ func (em *WSEventManager) broadcaster() {
|
|||
}
|
||||
|
||||
// clientSubscribed checks if a client is subscribed to an event type.
|
||||
func (em *WSEventManager) clientSubscribed(state *clientState, eventType EventType) bool {
|
||||
func (em *WebSocketEventManager) clientSubscribed(state *clientState, eventType EventType) bool {
|
||||
state.mu.RLock()
|
||||
defer state.mu.RUnlock()
|
||||
|
||||
|
|
@ -120,7 +126,7 @@ func (em *WSEventManager) clientSubscribed(state *clientState, eventType EventTy
|
|||
}
|
||||
|
||||
// sendEvent sends an event to a specific client.
|
||||
func (em *WSEventManager) sendEvent(conn *websocket.Conn, event Event) {
|
||||
func (em *WebSocketEventManager) sendEvent(conn *websocket.Conn, event Event) {
|
||||
em.mu.RLock()
|
||||
_, exists := em.clients[conn]
|
||||
em.mu.RUnlock()
|
||||
|
|
@ -141,7 +147,7 @@ func (em *WSEventManager) sendEvent(conn *websocket.Conn, event Event) {
|
|||
}
|
||||
|
||||
// HandleWebSocket handles WebSocket upgrade and connection.
|
||||
func (em *WSEventManager) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
func (em *WebSocketEventManager) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := em.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
@ -158,7 +164,7 @@ func (em *WSEventManager) HandleWebSocket(w http.ResponseWriter, r *http.Request
|
|||
}
|
||||
|
||||
// handleMessages processes incoming WebSocket messages.
|
||||
func (em *WSEventManager) handleMessages(conn *websocket.Conn) {
|
||||
func (em *WebSocketEventManager) handleMessages(conn *websocket.Conn) {
|
||||
defer em.removeClient(conn)
|
||||
|
||||
for {
|
||||
|
|
@ -189,7 +195,7 @@ func (em *WSEventManager) handleMessages(conn *websocket.Conn) {
|
|||
}
|
||||
|
||||
// subscribe adds a subscription for a client.
|
||||
func (em *WSEventManager) subscribe(conn *websocket.Conn, id string, eventTypes []EventType) {
|
||||
func (em *WebSocketEventManager) subscribe(conn *websocket.Conn, id string, eventTypes []EventType) {
|
||||
em.mu.RLock()
|
||||
state, exists := em.clients[conn]
|
||||
em.mu.RUnlock()
|
||||
|
|
@ -224,7 +230,7 @@ func (em *WSEventManager) subscribe(conn *websocket.Conn, id string, eventTypes
|
|||
}
|
||||
|
||||
// unsubscribe removes a subscription for a client.
|
||||
func (em *WSEventManager) unsubscribe(conn *websocket.Conn, id string) {
|
||||
func (em *WebSocketEventManager) unsubscribe(conn *websocket.Conn, id string) {
|
||||
em.mu.RLock()
|
||||
state, exists := em.clients[conn]
|
||||
em.mu.RUnlock()
|
||||
|
|
@ -247,7 +253,7 @@ func (em *WSEventManager) unsubscribe(conn *websocket.Conn, id string) {
|
|||
}
|
||||
|
||||
// listSubscriptions sends a list of active subscriptions to a client.
|
||||
func (em *WSEventManager) listSubscriptions(conn *websocket.Conn) {
|
||||
func (em *WebSocketEventManager) listSubscriptions(conn *websocket.Conn) {
|
||||
em.mu.RLock()
|
||||
state, exists := em.clients[conn]
|
||||
em.mu.RUnlock()
|
||||
|
|
@ -272,7 +278,7 @@ func (em *WSEventManager) listSubscriptions(conn *websocket.Conn) {
|
|||
}
|
||||
|
||||
// removeClient removes a client and its subscriptions.
|
||||
func (em *WSEventManager) removeClient(conn *websocket.Conn) {
|
||||
func (em *WebSocketEventManager) removeClient(conn *websocket.Conn) {
|
||||
em.mu.Lock()
|
||||
delete(em.clients, conn)
|
||||
em.mu.Unlock()
|
||||
|
|
@ -280,7 +286,7 @@ func (em *WSEventManager) removeClient(conn *websocket.Conn) {
|
|||
}
|
||||
|
||||
// Emit sends an event to all subscribed clients.
|
||||
func (em *WSEventManager) Emit(event Event) {
|
||||
func (em *WebSocketEventManager) Emit(event Event) {
|
||||
event.Timestamp = time.Now().UnixMilli()
|
||||
select {
|
||||
case em.eventBuffer <- event:
|
||||
|
|
@ -290,7 +296,7 @@ func (em *WSEventManager) Emit(event Event) {
|
|||
}
|
||||
|
||||
// EmitWindowEvent is a helper to emit window-related events.
|
||||
func (em *WSEventManager) EmitWindowEvent(eventType EventType, windowName string, data map[string]any) {
|
||||
func (em *WebSocketEventManager) EmitWindowEvent(eventType EventType, windowName string, data map[string]any) {
|
||||
em.Emit(Event{
|
||||
Type: eventType,
|
||||
Window: windowName,
|
||||
|
|
@ -299,14 +305,14 @@ func (em *WSEventManager) EmitWindowEvent(eventType EventType, windowName string
|
|||
}
|
||||
|
||||
// ConnectedClients returns the number of connected WebSocket clients.
|
||||
func (em *WSEventManager) ConnectedClients() int {
|
||||
func (em *WebSocketEventManager) ConnectedClients() int {
|
||||
em.mu.RLock()
|
||||
defer em.mu.RUnlock()
|
||||
return len(em.clients)
|
||||
}
|
||||
|
||||
// Close shuts down the event manager.
|
||||
func (em *WSEventManager) Close() {
|
||||
func (em *WebSocketEventManager) Close() {
|
||||
em.mu.Lock()
|
||||
for conn := range em.clients {
|
||||
conn.Close()
|
||||
|
|
@ -318,7 +324,7 @@ func (em *WSEventManager) Close() {
|
|||
|
||||
// AttachWindowListeners attaches event listeners to a specific window.
|
||||
// Accepts window.PlatformWindow instead of *application.WebviewWindow.
|
||||
func (em *WSEventManager) AttachWindowListeners(pw window.PlatformWindow) {
|
||||
func (em *WebSocketEventManager) AttachWindowListeners(pw window.PlatformWindow) {
|
||||
if pw == nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@ type ActionIDECommand struct {
|
|||
Command string `json:"command"` // "save", "run", "build"
|
||||
}
|
||||
|
||||
// EventIDECommand is the WS event type for IDE commands.
|
||||
// EventIDECommand is the WebSocket event type for IDE commands.
|
||||
const EventIDECommand EventType = "ide.command"
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ import (
|
|||
|
||||
func TestSubsystem_Good_Name(t *testing.T) {
|
||||
c, _ := core.New(core.WithServiceLock())
|
||||
sub := New(c)
|
||||
sub := NewService(c)
|
||||
assert.Equal(t, "display", sub.Name())
|
||||
}
|
||||
|
||||
func TestSubsystem_Good_RegisterTools(t *testing.T) {
|
||||
c, _ := core.New(core.WithServiceLock())
|
||||
sub := New(c)
|
||||
sub := NewService(c)
|
||||
// RegisterTools should not panic with a real mcp.Server
|
||||
server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil)
|
||||
assert.NotPanics(t, func() { sub.RegisterTools(server) })
|
||||
|
|
@ -34,7 +34,7 @@ type mockClipPlatform struct {
|
|||
}
|
||||
|
||||
func (m *mockClipPlatform) Text() (string, bool) { return m.text, m.ok }
|
||||
func (m *mockClipPlatform) SetText(t string) bool { m.text = t; m.ok = t != ""; return true }
|
||||
func (m *mockClipPlatform) SetText(t string) bool { m.text = t; m.ok = t != ""; return true }
|
||||
|
||||
func TestMCP_Good_ClipboardRoundTrip(t *testing.T) {
|
||||
c, err := core.New(
|
||||
|
|
|
|||
|
|
@ -12,11 +12,16 @@ type Subsystem struct {
|
|||
core *core.Core
|
||||
}
|
||||
|
||||
// New creates a display MCP subsystem backed by the given Core instance.
|
||||
func New(c *core.Core) *Subsystem {
|
||||
// NewService creates a display MCP subsystem backed by the given Core instance.
|
||||
func NewService(c *core.Core) *Subsystem {
|
||||
return &Subsystem{core: c}
|
||||
}
|
||||
|
||||
// New is kept for backward compatibility with older callers.
|
||||
func New(c *core.Core) *Subsystem {
|
||||
return NewService(c)
|
||||
}
|
||||
|
||||
// Name returns the subsystem identifier.
|
||||
func (s *Subsystem) Name() string { return "display" }
|
||||
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ type WindowMaximizeOutput struct {
|
|||
}
|
||||
|
||||
func (s *Subsystem) windowMaximize(_ context.Context, _ *mcp.CallToolRequest, input WindowMaximizeInput) (*mcp.CallToolResult, WindowMaximizeOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskMaximise{Name: input.Name})
|
||||
_, _, err := s.core.PERFORM(window.TaskMaximize{Name: input.Name})
|
||||
if err != nil {
|
||||
return nil, WindowMaximizeOutput{}, err
|
||||
}
|
||||
|
|
@ -200,7 +200,7 @@ type WindowMinimizeOutput struct {
|
|||
}
|
||||
|
||||
func (s *Subsystem) windowMinimize(_ context.Context, _ *mcp.CallToolRequest, input WindowMinimizeInput) (*mcp.CallToolResult, WindowMinimizeOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskMinimise{Name: input.Name})
|
||||
_, _, err := s.core.PERFORM(window.TaskMinimize{Name: input.Name})
|
||||
if err != nil {
|
||||
return nil, WindowMinimizeOutput{}, err
|
||||
}
|
||||
|
|
@ -368,9 +368,9 @@ func (s *Subsystem) registerWindowTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "window_position", Description: "Set the position of a window"}, s.windowPosition)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_size", Description: "Set the size of a window"}, s.windowSize)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_bounds", Description: "Set both position and size of a window"}, s.windowBounds)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_maximize", Description: "Maximise a window"}, s.windowMaximize)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_minimize", Description: "Minimise a window"}, s.windowMinimize)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_restore", Description: "Restore a maximised or minimised window"}, s.windowRestore)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_maximize", Description: "Maximize a window"}, s.windowMaximize)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_minimize", Description: "Minimize a window"}, s.windowMinimize)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_restore", Description: "Restore a maximized or minimized window"}, s.windowRestore)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_focus", Description: "Bring a window to the front"}, s.windowFocus)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_title", Description: "Set the title of a window"}, s.windowTitle)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_title_get", Description: "Get the title of a window"}, s.windowTitleGet)
|
||||
|
|
|
|||
|
|
@ -84,23 +84,23 @@ func defaultNewConn(options Options) func(string, string) (connector, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var wsURL string
|
||||
var webSocketURL string
|
||||
for _, t := range targets {
|
||||
if t.Type == "page" && (bytes.Contains([]byte(t.Title), []byte(windowName)) || bytes.Contains([]byte(t.URL), []byte(windowName))) {
|
||||
wsURL = t.WebSocketDebuggerURL
|
||||
webSocketURL = t.WebSocketDebuggerURL
|
||||
break
|
||||
}
|
||||
}
|
||||
// Fallback: first page target
|
||||
if wsURL == "" {
|
||||
if webSocketURL == "" {
|
||||
for _, t := range targets {
|
||||
if t.Type == "page" {
|
||||
wsURL = t.WebSocketDebuggerURL
|
||||
webSocketURL = t.WebSocketDebuggerURL
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if wsURL == "" {
|
||||
if webSocketURL == "" {
|
||||
return nil, core.E("webview.connect", "no page target found", nil)
|
||||
}
|
||||
wv, err := gowebview.New(
|
||||
|
|
@ -123,8 +123,8 @@ func (s *Service) defaultWatcherSetup(conn connector, windowName string) {
|
|||
return // test mocks don't need watchers
|
||||
}
|
||||
|
||||
cw := gowebview.NewConsoleWatcher(rc.wv)
|
||||
cw.AddHandler(func(msg gowebview.ConsoleMessage) {
|
||||
consoleWatcher := gowebview.NewConsoleWatcher(rc.wv)
|
||||
consoleWatcher.AddHandler(func(msg gowebview.ConsoleMessage) {
|
||||
_ = s.Core().ACTION(ActionConsoleMessage{
|
||||
Window: windowName,
|
||||
Message: ConsoleMessage{
|
||||
|
|
@ -138,8 +138,8 @@ func (s *Service) defaultWatcherSetup(conn connector, windowName string) {
|
|||
})
|
||||
})
|
||||
|
||||
ew := gowebview.NewExceptionWatcher(rc.wv)
|
||||
ew.AddHandler(func(exc gowebview.ExceptionInfo) {
|
||||
exceptionWatcher := gowebview.NewExceptionWatcher(rc.wv)
|
||||
exceptionWatcher.AddHandler(func(exc gowebview.ExceptionInfo) {
|
||||
_ = s.Core().ACTION(ActionException{
|
||||
Window: windowName,
|
||||
Exception: ExceptionInfo{
|
||||
|
|
|
|||
|
|
@ -35,9 +35,17 @@ type TaskSetSize struct {
|
|||
Width, Height int
|
||||
}
|
||||
|
||||
type TaskMaximise struct{ Name string }
|
||||
// TaskMaximize maximizes a named window.
|
||||
type TaskMaximize struct{ Name string }
|
||||
|
||||
type TaskMinimise struct{ Name string }
|
||||
// TaskMinimize minimizes a named window.
|
||||
type TaskMinimize struct{ Name string }
|
||||
|
||||
// TaskMaximise is kept for backward compatibility with older callers.
|
||||
type TaskMaximise = TaskMaximize
|
||||
|
||||
// TaskMinimise is kept for backward compatibility with older callers.
|
||||
type TaskMinimise = TaskMinimize
|
||||
|
||||
type TaskFocus struct{ Name string }
|
||||
|
||||
|
|
|
|||
|
|
@ -136,10 +136,10 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
return nil, true, s.taskSetPosition(t.Name, t.X, t.Y)
|
||||
case TaskSetSize:
|
||||
return nil, true, s.taskSetSize(t.Name, t.Width, t.Height)
|
||||
case TaskMaximise:
|
||||
return nil, true, s.taskMaximise(t.Name)
|
||||
case TaskMinimise:
|
||||
return nil, true, s.taskMinimise(t.Name)
|
||||
case TaskMaximize:
|
||||
return nil, true, s.taskMaximize(t.Name)
|
||||
case TaskMinimize:
|
||||
return nil, true, s.taskMinimize(t.Name)
|
||||
case TaskFocus:
|
||||
return nil, true, s.taskFocus(t.Name)
|
||||
case TaskRestore:
|
||||
|
|
@ -299,8 +299,8 @@ func (s *Service) taskSetSize(name string, width, height int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskMaximise(name string) error {
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskMaximise")
|
||||
func (s *Service) taskMaximize(name string) error {
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskMaximize")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -309,8 +309,8 @@ func (s *Service) taskMaximise(name string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskMinimise(name string) error {
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskMinimise")
|
||||
func (s *Service) taskMinimize(name string) error {
|
||||
platformWindow, err := s.requireWindow(name, "window.Service.taskMinimize")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,11 +166,11 @@ func TestTaskSetSize_Good(t *testing.T) {
|
|||
assert.Equal(t, 600, info.Height)
|
||||
}
|
||||
|
||||
func TestTaskMaximise_Good(t *testing.T) {
|
||||
func TestTaskMaximize_Good(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskMaximise{Name: "test"})
|
||||
_, handled, err := c.PERFORM(TaskMaximize{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
|
|
@ -215,13 +215,13 @@ func TestFileDrop_Good(t *testing.T) {
|
|||
mu.Unlock()
|
||||
}
|
||||
|
||||
// --- TaskMinimise ---
|
||||
// --- TaskMinimize ---
|
||||
|
||||
func TestTaskMinimise_Good(t *testing.T) {
|
||||
func TestTaskMinimize_Good(t *testing.T) {
|
||||
svc, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskMinimise{Name: "test"})
|
||||
_, handled, err := c.PERFORM(TaskMinimize{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
|
|
@ -231,9 +231,9 @@ func TestTaskMinimise_Good(t *testing.T) {
|
|||
assert.True(t, mw.minimised)
|
||||
}
|
||||
|
||||
func TestTaskMinimise_Bad(t *testing.T) {
|
||||
func TestTaskMinimize_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskMinimise{Name: "nonexistent"})
|
||||
_, handled, err := c.PERFORM(TaskMinimize{Name: "nonexistent"})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
|
@ -267,8 +267,8 @@ func TestTaskRestore_Good(t *testing.T) {
|
|||
svc, c := newTestWindowService(t)
|
||||
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
|
||||
|
||||
// First maximise, then restore
|
||||
_, _, _ = c.PERFORM(TaskMaximise{Name: "test"})
|
||||
// First maximize, then restore
|
||||
_, _, _ = c.PERFORM(TaskMaximize{Name: "test"})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskRestore{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ type WorkflowLayout int
|
|||
const (
|
||||
WorkflowCoding WorkflowLayout = iota // 70/30 split
|
||||
WorkflowDebugging // 60/40 split
|
||||
WorkflowPresenting // maximised
|
||||
WorkflowPresenting // maximized
|
||||
WorkflowSideBySide // 50/50 split
|
||||
)
|
||||
|
||||
|
|
@ -276,7 +276,7 @@ func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW
|
|||
}
|
||||
}
|
||||
case WorkflowPresenting:
|
||||
// Maximise first window
|
||||
// Maximize first window
|
||||
if pw, ok := m.Get(names[0]); ok {
|
||||
pw.SetPosition(originX, originY)
|
||||
pw.SetSize(screenW, screenH)
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ func TestApplyWorkflow_AllLayouts_Good(t *testing.T) {
|
|||
{
|
||||
"Presenting",
|
||||
WorkflowPresenting,
|
||||
0, 0, screenW, screenH, // maximised
|
||||
0, 0, screenW, screenH, // maximized
|
||||
0, 0, 800, 600, // second window untouched
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue