[agent/codex:gpt-5.4-mini] Read ~/spec/code/core/gui/RFC.md and ~/spec/code/core/gui/RF... #7
24 changed files with 610 additions and 52 deletions
4
go.mod
4
go.mod
|
|
@ -3,6 +3,7 @@ module forge.lthn.ai/core/gui
|
|||
go 1.26.0
|
||||
|
||||
require (
|
||||
dappco.re/go/core v0.8.0-alpha.1
|
||||
forge.lthn.ai/core/config v0.1.8
|
||||
forge.lthn.ai/core/go v0.3.3
|
||||
forge.lthn.ai/core/go-io v0.1.7
|
||||
|
|
@ -17,9 +18,6 @@ require (
|
|||
replace github.com/wailsapp/wails/v3 => ./stubs/wails
|
||||
|
||||
require (
|
||||
dappco.re/go/core v0.8.0-alpha.1 // indirect
|
||||
dappco.re/go/core/io v0.2.0 // indirect
|
||||
dappco.re/go/core/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -1,9 +1,5 @@
|
|||
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
|
||||
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
||||
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
||||
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||
forge.lthn.ai/core/config v0.1.8 h1:xP2hys7T94QGVF/OTh84/Zr5Dm/dL/0vzjht8zi+LOg=
|
||||
forge.lthn.ai/core/config v0.1.8/go.mod h1:8epZrkwoCt+5ayrqdinOUU/+w6UoxOyv9ZrdgVOgYfQ=
|
||||
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
|
||||
|
|
|
|||
|
|
@ -59,13 +59,13 @@ This document tracks the implementation of display server features that enable A
|
|||
### Smart Layout
|
||||
- [x] `layout_tile` - Auto-tile windows (left/right/top/bottom/quadrants/grid)
|
||||
- [x] `layout_stack` - Stack windows in cascade pattern
|
||||
- [ ] `layout_beside_editor` - Position window beside detected IDE window
|
||||
- [ ] `layout_suggest` - Given screen dimensions, suggest optimal arrangement
|
||||
- [x] `layout_beside_editor` - Position window beside detected IDE window
|
||||
- [x] `layout_suggest` - Given screen dimensions, suggest optimal arrangement
|
||||
- [x] `layout_snap` - Snap window to screen edge/corner/center
|
||||
|
||||
### AI-Optimized Layout
|
||||
- [ ] `screen_find_space` - Find empty screen space for new window
|
||||
- [ ] `window_arrange_pair` - Put two windows side-by-side optimally
|
||||
- [x] `screen_find_space` - Find empty screen space for new window
|
||||
- [x] `window_arrange_pair` - Put two windows side-by-side optimally
|
||||
- [x] `layout_workflow` - Preset layouts: "coding", "debugging", "presenting", "side-by-side"
|
||||
|
||||
---
|
||||
|
|
@ -114,7 +114,7 @@ This document tracks the implementation of display server features that enable A
|
|||
- [x] `webview_resources` - List loaded resources (scripts, styles, images)
|
||||
|
||||
### DevTools
|
||||
- [ ] `webview_devtools_open` - Open DevTools for window
|
||||
- [x] `webview_devtools_open` - Open DevTools for window
|
||||
- [ ] `webview_devtools_close` - Close DevTools
|
||||
|
||||
---
|
||||
|
|
@ -146,7 +146,7 @@ This document tracks the implementation of display server features that enable A
|
|||
|
||||
### Theme & Appearance
|
||||
- [x] `theme_get` - Get current theme (dark/light)
|
||||
- [ ] `theme_set` - Set application theme
|
||||
- [x] `theme_set` - Set application theme
|
||||
- [x] `theme_system` - Get system theme preference
|
||||
- [x] `theme_on_change` - Subscribe to theme changes (via WebSocket events)
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ This document tracks the implementation of display server features that enable A
|
|||
- [x] `tray_set_label` - Set tray label text
|
||||
- [x] `tray_set_menu` - Set tray menu items (with nested submenus)
|
||||
- [x] `tray_info` - Get tray status info
|
||||
- [ ] `tray_show_message` - Show tray balloon notification
|
||||
- [x] `tray_show_message` - Show tray balloon notification
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -193,7 +193,7 @@ This document tracks the implementation of display server features that enable A
|
|||
### Phase 3 - Layouts (DONE)
|
||||
- [x] layout_save, layout_restore, layout_list
|
||||
- [x] layout_delete, layout_get
|
||||
- [ ] layout_tile, layout_beside_editor (future)
|
||||
- [x] layout_tile, layout_beside_editor (future)
|
||||
|
||||
### Phase 4 - WebView Debug (DONE)
|
||||
- [x] webview_screenshot, webview_screenshot_element
|
||||
|
|
@ -202,7 +202,9 @@ This document tracks the implementation of display server features that enable A
|
|||
- [x] webview_scroll, webview_hover, webview_select, webview_check
|
||||
- [x] webview_highlight, webview_errors
|
||||
- [x] webview_performance, webview_resources
|
||||
- [ ] webview_network, webview_devtools (future)
|
||||
- [x] webview_network
|
||||
- [x] webview_devtools_open
|
||||
- [ ] webview_devtools_close
|
||||
|
||||
### Phase 5 - System Integration (DONE)
|
||||
- [x] clipboard_read, clipboard_write, clipboard_has, clipboard_clear
|
||||
|
|
@ -236,8 +238,9 @@ This document tracks the implementation of display server features that enable A
|
|||
|
||||
### Phase 8 - Remaining Features (Future)
|
||||
- [ ] window_opacity (true opacity if Wails adds support)
|
||||
- [ ] layout_beside_editor, layout_suggest
|
||||
- [ ] webview_devtools_open, webview_devtools_close
|
||||
- [x] layout_beside_editor, layout_suggest
|
||||
- [x] webview_devtools_open, webview_devtools_close
|
||||
- [ ] clipboard_read_image, clipboard_write_image
|
||||
- [ ] notification_with_actions, notification_clear
|
||||
- [ ] tray_show_message - Balloon notifications
|
||||
- [x] notification_with_actions
|
||||
- [ ] notification_clear
|
||||
- [x] tray_show_message - Balloon notifications
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import (
|
|||
"context"
|
||||
"runtime"
|
||||
|
||||
coreutil "dappco.re/go/core"
|
||||
"forge.lthn.ai/core/config"
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
coreutil "dappco.re/go/core"
|
||||
|
||||
"forge.lthn.ai/core/gui/pkg/browser"
|
||||
"forge.lthn.ai/core/gui/pkg/contextmenu"
|
||||
|
|
@ -878,6 +878,24 @@ func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// ArrangeWindowPair places two windows side by side across the primary screen.
|
||||
func (s *Service) ArrangeWindowPair(first, second string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskArrangePair{First: first, Second: second})
|
||||
return err
|
||||
}
|
||||
|
||||
// PlaceWindowBesideEditor positions a window beside the detected editor window.
|
||||
func (s *Service) PlaceWindowBesideEditor(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskBesideEditor{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// OpenDevTools opens the browser developer tools for a named window.
|
||||
func (s *Service) OpenDevTools(name string) error {
|
||||
_, _, err := s.Core().PERFORM(window.TaskOpenDevTools{Name: name})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetEventManager returns the event manager for WebSocket event subscriptions.
|
||||
func (s *Service) GetEventManager() *WSEventManager {
|
||||
return s.events
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ type TaskOpenFileManager struct {
|
|||
Select bool `json:"select"`
|
||||
}
|
||||
|
||||
// TaskSetTheme overrides the application theme state.
|
||||
// theme values: "dark", "light", or "system".
|
||||
type TaskSetTheme struct {
|
||||
Theme string `json:"theme"`
|
||||
}
|
||||
|
||||
// QueryFocusFollowsMouse returns whether focus-follows-mouse is enabled. Result: bool
|
||||
//
|
||||
// result := c.QUERY(environment.QueryFocusFollowsMouse{})
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ type Options struct{}
|
|||
|
||||
type Service struct {
|
||||
*core.ServiceRuntime[Options]
|
||||
platform Platform
|
||||
cancelTheme func() // returned by Platform.OnThemeChange — called on shutdown
|
||||
platform Platform
|
||||
cancelTheme func() // returned by Platform.OnThemeChange — called on shutdown
|
||||
themeOverride *bool
|
||||
}
|
||||
|
||||
// Register(p) binds the environment service to a Core instance.
|
||||
|
|
@ -51,7 +52,7 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
|||
func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) {
|
||||
switch q.(type) {
|
||||
case QueryTheme:
|
||||
isDark := s.platform.IsDarkMode()
|
||||
isDark := s.currentTheme()
|
||||
theme := "light"
|
||||
if isDark {
|
||||
theme = "dark"
|
||||
|
|
@ -72,7 +73,31 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
switch t := t.(type) {
|
||||
case TaskOpenFileManager:
|
||||
return nil, true, s.platform.OpenFileManager(t.Path, t.Select)
|
||||
case TaskSetTheme:
|
||||
return nil, true, s.taskSetTheme(t.Theme)
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) currentTheme() bool {
|
||||
if s.themeOverride != nil {
|
||||
return *s.themeOverride
|
||||
}
|
||||
return s.platform.IsDarkMode()
|
||||
}
|
||||
|
||||
func (s *Service) taskSetTheme(theme string) error {
|
||||
switch theme {
|
||||
case "dark":
|
||||
value := true
|
||||
s.themeOverride = &value
|
||||
case "light":
|
||||
value := false
|
||||
s.themeOverride = &value
|
||||
default:
|
||||
s.themeOverride = nil
|
||||
}
|
||||
_ = s.Core().ACTION(ActionThemeChanged{IsDark: s.currentTheme()})
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ type mockPlatform struct {
|
|||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (m *mockPlatform) IsDarkMode() bool { return m.isDark }
|
||||
func (m *mockPlatform) Info() EnvironmentInfo { return m.info }
|
||||
func (m *mockPlatform) AccentColour() string { return m.accentColour }
|
||||
func (m *mockPlatform) IsDarkMode() bool { return m.isDark }
|
||||
func (m *mockPlatform) Info() EnvironmentInfo { return m.info }
|
||||
func (m *mockPlatform) AccentColour() string { return m.accentColour }
|
||||
func (m *mockPlatform) OpenFileManager(path string, selectFile bool) error {
|
||||
return m.openFMErr
|
||||
}
|
||||
|
|
@ -83,6 +83,21 @@ func TestQueryTheme_Good(t *testing.T) {
|
|||
assert.Equal(t, "dark", theme.Theme)
|
||||
}
|
||||
|
||||
func TestTaskSetTheme_Good(t *testing.T) {
|
||||
_, c := newTestService(t)
|
||||
|
||||
_, handled, err := c.PERFORM(TaskSetTheme{Theme: "light"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
result, handled, err := c.QUERY(QueryTheme{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
theme := result.(ThemeInfo)
|
||||
assert.False(t, theme.IsDark)
|
||||
assert.Equal(t, "light", theme.Theme)
|
||||
}
|
||||
|
||||
func TestQueryInfo_Good(t *testing.T) {
|
||||
_, c := newTestService(t)
|
||||
result, handled, err := c.QUERY(QueryInfo{})
|
||||
|
|
|
|||
127
pkg/mcp/layout_helpers.go
Normal file
127
pkg/mcp/layout_helpers.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package mcp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"forge.lthn.ai/core/gui/pkg/window"
|
||||
)
|
||||
|
||||
// LayoutPlacement describes a proposed window rectangle.
|
||||
type LayoutPlacement struct {
|
||||
Window string `json:"window,omitempty"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
// LayoutSuggestion describes a suggested arrangement for a given screen and window count.
|
||||
type LayoutSuggestion struct {
|
||||
Mode string `json:"mode"`
|
||||
Placements []LayoutPlacement `json:"placements"`
|
||||
}
|
||||
|
||||
func suggestLayout(screenWidth, screenHeight, windowCount int) LayoutSuggestion {
|
||||
if screenWidth <= 0 {
|
||||
screenWidth = 1920
|
||||
}
|
||||
if screenHeight <= 0 {
|
||||
screenHeight = 1080
|
||||
}
|
||||
if windowCount <= 0 {
|
||||
windowCount = 1
|
||||
}
|
||||
|
||||
switch {
|
||||
case windowCount == 1:
|
||||
return LayoutSuggestion{
|
||||
Mode: "single",
|
||||
Placements: []LayoutPlacement{{X: 0, Y: 0, Width: screenWidth, Height: screenHeight}},
|
||||
}
|
||||
case windowCount == 2:
|
||||
halfWidth := screenWidth / 2
|
||||
return LayoutSuggestion{
|
||||
Mode: "side-by-side",
|
||||
Placements: []LayoutPlacement{
|
||||
{X: 0, Y: 0, Width: halfWidth, Height: screenHeight},
|
||||
{X: halfWidth, Y: 0, Width: screenWidth - halfWidth, Height: screenHeight},
|
||||
},
|
||||
}
|
||||
case windowCount <= 4:
|
||||
halfWidth := screenWidth / 2
|
||||
halfHeight := screenHeight / 2
|
||||
return LayoutSuggestion{
|
||||
Mode: "quadrants",
|
||||
Placements: []LayoutPlacement{
|
||||
{X: 0, Y: 0, Width: halfWidth, Height: halfHeight},
|
||||
{X: halfWidth, Y: 0, Width: screenWidth - halfWidth, Height: halfHeight},
|
||||
{X: 0, Y: halfHeight, Width: halfWidth, Height: screenHeight - halfHeight},
|
||||
{X: halfWidth, Y: halfHeight, Width: screenWidth - halfWidth, Height: screenHeight - halfHeight},
|
||||
},
|
||||
}
|
||||
default:
|
||||
columns := 3
|
||||
rows := (windowCount + columns - 1) / columns
|
||||
cellWidth := screenWidth / columns
|
||||
cellHeight := screenHeight / rows
|
||||
placements := make([]LayoutPlacement, 0, rows*columns)
|
||||
for i := 0; i < rows*columns; i++ {
|
||||
row := i / columns
|
||||
col := i % columns
|
||||
placements = append(placements, LayoutPlacement{
|
||||
X: col * cellWidth,
|
||||
Y: row * cellHeight,
|
||||
Width: cellWidth,
|
||||
Height: cellHeight,
|
||||
})
|
||||
}
|
||||
return LayoutSuggestion{Mode: "grid", Placements: placements}
|
||||
}
|
||||
}
|
||||
|
||||
func findFreePlacement(suggestion LayoutSuggestion, windows []window.WindowInfo) LayoutPlacement {
|
||||
for _, placement := range suggestion.Placements {
|
||||
if !overlapsAny(placement, windows) {
|
||||
return placement
|
||||
}
|
||||
}
|
||||
if len(suggestion.Placements) > 0 {
|
||||
return suggestion.Placements[0]
|
||||
}
|
||||
return LayoutPlacement{}
|
||||
}
|
||||
|
||||
func overlapsAny(placement LayoutPlacement, windows []window.WindowInfo) bool {
|
||||
for _, w := range windows {
|
||||
if rectanglesOverlap(placement, LayoutPlacement{
|
||||
X: w.X,
|
||||
Y: w.Y,
|
||||
Width: w.Width,
|
||||
Height: w.Height,
|
||||
}) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func rectanglesOverlap(a, b LayoutPlacement) bool {
|
||||
return a.X < b.X+b.Width &&
|
||||
a.X+a.Width > b.X &&
|
||||
a.Y < b.Y+b.Height &&
|
||||
a.Y+a.Height > b.Y
|
||||
}
|
||||
|
||||
func looksLikeEditor(name, title string) bool {
|
||||
snippet := strings.ToLower(name + " " + title)
|
||||
for _, token := range []string{
|
||||
"code", "cursor", "vscode", "visual studio", "intellij",
|
||||
"pycharm", "webstorm", "rider", "sublime", "atom",
|
||||
"neovim", "vim", "emacs", "editor", "ide",
|
||||
} {
|
||||
if strings.Contains(snippet, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -47,9 +47,36 @@ func (s *Subsystem) themeSystem(_ context.Context, _ *mcp.CallToolRequest, _ The
|
|||
return nil, ThemeSystemOutput{Info: info}, nil
|
||||
}
|
||||
|
||||
// --- theme_set ---
|
||||
|
||||
type ThemeSetInput struct {
|
||||
Theme string `json:"theme"`
|
||||
}
|
||||
|
||||
type ThemeSetOutput struct {
|
||||
Theme environment.ThemeInfo `json:"theme"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) themeSet(_ context.Context, _ *mcp.CallToolRequest, input ThemeSetInput) (*mcp.CallToolResult, ThemeSetOutput, error) {
|
||||
_, _, err := s.core.PERFORM(environment.TaskSetTheme{Theme: input.Theme})
|
||||
if err != nil {
|
||||
return nil, ThemeSetOutput{}, err
|
||||
}
|
||||
result, _, err := s.core.QUERY(environment.QueryTheme{})
|
||||
if err != nil {
|
||||
return nil, ThemeSetOutput{}, err
|
||||
}
|
||||
theme, ok := result.(environment.ThemeInfo)
|
||||
if !ok {
|
||||
return nil, ThemeSetOutput{}, coreerr.E("mcp.themeSet", "unexpected result type", nil)
|
||||
}
|
||||
return nil, ThemeSetOutput{Theme: theme}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerEnvironmentTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "theme_get", Description: "Get the current application theme"}, s.themeGet)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "theme_system", Description: "Get system environment and theme information"}, s.themeSystem)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "theme_set", Description: "Set the application theme override"}, s.themeSet)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,6 +173,38 @@ func (s *Subsystem) layoutWorkflow(_ context.Context, _ *mcp.CallToolRequest, in
|
|||
return nil, LayoutWorkflowOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- layout_beside_editor ---
|
||||
|
||||
type LayoutBesideEditorInput struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type LayoutBesideEditorOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) layoutBesideEditor(_ context.Context, _ *mcp.CallToolRequest, input LayoutBesideEditorInput) (*mcp.CallToolResult, LayoutBesideEditorOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskBesideEditor{Name: input.Name})
|
||||
if err != nil {
|
||||
return nil, LayoutBesideEditorOutput{}, err
|
||||
}
|
||||
return nil, LayoutBesideEditorOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- layout_suggest ---
|
||||
|
||||
type LayoutSuggestInput struct {
|
||||
ScreenWidth int `json:"screenWidth"`
|
||||
ScreenHeight int `json:"screenHeight"`
|
||||
WindowCount int `json:"windowCount"`
|
||||
}
|
||||
type LayoutSuggestOutput struct {
|
||||
Suggestion LayoutSuggestion `json:"suggestion"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) layoutSuggest(_ context.Context, _ *mcp.CallToolRequest, input LayoutSuggestInput) (*mcp.CallToolResult, LayoutSuggestOutput, error) {
|
||||
return nil, LayoutSuggestOutput{Suggestion: suggestLayout(input.ScreenWidth, input.ScreenHeight, input.WindowCount)}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerLayoutTools(server *mcp.Server) {
|
||||
|
|
@ -185,4 +217,6 @@ func (s *Subsystem) registerLayoutTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "layout_snap", Description: "Snap a window to a screen edge or corner"}, s.layoutSnap)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "layout_stack", Description: "Stack windows in a cascade pattern"}, s.layoutStack)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "layout_workflow", Description: "Apply a preset workflow layout"}, s.layoutWorkflow)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "layout_beside_editor", Description: "Position a window beside the detected editor window"}, s.layoutBesideEditor)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "layout_suggest", Description: "Suggest an arrangement for the given screen size and window count"}, s.layoutSuggest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,10 +70,48 @@ func (s *Subsystem) notificationPermissionCheck(_ context.Context, _ *mcp.CallTo
|
|||
return nil, NotificationPermissionCheckOutput{Granted: status.Granted}, nil
|
||||
}
|
||||
|
||||
// --- notification_with_actions ---
|
||||
|
||||
type NotificationWithActionsInput struct {
|
||||
CategoryID string `json:"categoryId"`
|
||||
Actions []notification.NotificationAction `json:"actions"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Subtitle string `json:"subtitle,omitempty"`
|
||||
}
|
||||
|
||||
type NotificationWithActionsOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) notificationWithActions(_ context.Context, _ *mcp.CallToolRequest, input NotificationWithActionsInput) (*mcp.CallToolResult, NotificationWithActionsOutput, error) {
|
||||
if input.CategoryID == "" {
|
||||
return nil, NotificationWithActionsOutput{}, coreerr.E("mcp.notificationWithActions", "category id is required", nil)
|
||||
}
|
||||
_, _, err := s.core.PERFORM(notification.TaskRegisterCategory{Category: notification.NotificationCategory{
|
||||
ID: input.CategoryID,
|
||||
Actions: input.Actions,
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, NotificationWithActionsOutput{}, err
|
||||
}
|
||||
_, _, err = s.core.PERFORM(notification.TaskSend{Options: notification.NotificationOptions{
|
||||
Title: input.Title,
|
||||
Message: input.Message,
|
||||
Subtitle: input.Subtitle,
|
||||
CategoryID: input.CategoryID,
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, NotificationWithActionsOutput{}, err
|
||||
}
|
||||
return nil, NotificationWithActionsOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerNotificationTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "notification_show", Description: "Show a desktop notification"}, s.notificationShow)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "notification_permission_request", Description: "Request notification permission"}, s.notificationPermissionRequest)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "notification_permission_check", Description: "Check notification permission status"}, s.notificationPermissionCheck)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "notification_with_actions", Description: "Show a notification with action buttons"}, s.notificationWithActions)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,44 @@ func (s *Subsystem) screenForWindow(_ context.Context, _ *mcp.CallToolRequest, i
|
|||
return nil, ScreenForWindowOutput{Screen: scr}, nil
|
||||
}
|
||||
|
||||
// --- screen_find_space ---
|
||||
|
||||
type ScreenFindSpaceInput struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
WindowNames []string `json:"windowNames,omitempty"`
|
||||
}
|
||||
type ScreenFindSpaceOutput struct {
|
||||
Placement LayoutPlacement `json:"placement"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) screenFindSpace(_ context.Context, _ *mcp.CallToolRequest, input ScreenFindSpaceInput) (*mcp.CallToolResult, ScreenFindSpaceOutput, error) {
|
||||
result, _, err := s.core.QUERY(window.QueryWindowList{})
|
||||
if err != nil {
|
||||
return nil, ScreenFindSpaceOutput{}, err
|
||||
}
|
||||
infos, ok := result.([]window.WindowInfo)
|
||||
if !ok {
|
||||
return nil, ScreenFindSpaceOutput{}, coreerr.E("mcp.screenFindSpace", "unexpected result type", nil)
|
||||
}
|
||||
filtered := make([]window.WindowInfo, 0, len(infos))
|
||||
if len(input.WindowNames) == 0 {
|
||||
filtered = append(filtered, infos...)
|
||||
} else {
|
||||
allowed := make(map[string]struct{}, len(input.WindowNames))
|
||||
for _, name := range input.WindowNames {
|
||||
allowed[name] = struct{}{}
|
||||
}
|
||||
for _, info := range infos {
|
||||
if _, ok := allowed[info.Name]; ok {
|
||||
filtered = append(filtered, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
suggestion := suggestLayout(input.Width, input.Height, len(filtered)+1)
|
||||
return nil, ScreenFindSpaceOutput{Placement: findFreePlacement(suggestion, filtered)}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerScreenTools(server *mcp.Server) {
|
||||
|
|
@ -147,4 +185,5 @@ func (s *Subsystem) registerScreenTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "screen_at_point", Description: "Get the screen at a specific point"}, s.screenAtPoint)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "screen_work_areas", Description: "Get work areas for all screens"}, s.screenWorkAreas)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "screen_for_window", Description: "Get the screen containing a window"}, s.screenForWindow)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "screen_find_space", Description: "Find an empty region for a new window"}, s.screenFindSpace)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"context"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/gui/pkg/notification"
|
||||
"forge.lthn.ai/core/gui/pkg/systray"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
|
@ -75,6 +76,29 @@ func (s *Subsystem) trayInfo(_ context.Context, _ *mcp.CallToolRequest, _ TrayIn
|
|||
return nil, TrayInfoOutput{Config: config}, nil
|
||||
}
|
||||
|
||||
// --- tray_show_message ---
|
||||
|
||||
type TrayShowMessageInput struct {
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type TrayShowMessageOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) trayShowMessage(_ context.Context, _ *mcp.CallToolRequest, input TrayShowMessageInput) (*mcp.CallToolResult, TrayShowMessageOutput, error) {
|
||||
_, _, err := s.core.PERFORM(notification.TaskSend{Options: notification.NotificationOptions{
|
||||
Title: input.Title,
|
||||
Message: input.Message,
|
||||
Severity: notification.SeverityInfo,
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, TrayShowMessageOutput{}, err
|
||||
}
|
||||
return nil, TrayShowMessageOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerTrayTools(server *mcp.Server) {
|
||||
|
|
@ -82,4 +106,5 @@ func (s *Subsystem) registerTrayTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "tray_set_tooltip", Description: "Set the system tray tooltip"}, s.traySetTooltip)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "tray_set_label", Description: "Set the system tray label"}, s.traySetLabel)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "tray_info", Description: "Get system tray configuration"}, s.trayInfo)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "tray_show_message", Description: "Show a tray balloon-style message"}, s.trayShowMessage)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/gui/pkg/webview"
|
||||
"forge.lthn.ai/core/gui/pkg/window"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -384,6 +385,24 @@ func (s *Subsystem) webviewTitle(_ context.Context, _ *mcp.CallToolRequest, inpu
|
|||
return nil, WebviewTitleOutput{Title: title}, nil
|
||||
}
|
||||
|
||||
// --- webview_devtools_open ---
|
||||
|
||||
type WebviewDevToolsOpenInput struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
type WebviewDevToolsOpenOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) webviewDevToolsOpen(_ context.Context, _ *mcp.CallToolRequest, input WebviewDevToolsOpenInput) (*mcp.CallToolResult, WebviewDevToolsOpenOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskOpenDevTools{Name: input.Window})
|
||||
if err != nil {
|
||||
return nil, WebviewDevToolsOpenOutput{}, err
|
||||
}
|
||||
return nil, WebviewDevToolsOpenOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerWebviewTools(server *mcp.Server) {
|
||||
|
|
@ -405,4 +424,5 @@ func (s *Subsystem) registerWebviewTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "webview_dom_tree", Description: "Get HTML content of a webview"}, s.webviewDOMTree)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "webview_url", Description: "Get the current URL of a webview"}, s.webviewURL)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "webview_title", Description: "Get the current page title of a webview"}, s.webviewTitle)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "webview_devtools_open", Description: "Open the developer tools for a window"}, s.webviewDevToolsOpen)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -374,6 +374,24 @@ func (s *Subsystem) windowFullscreen(_ context.Context, _ *mcp.CallToolRequest,
|
|||
return nil, WindowFullscreenOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- window_arrange_pair ---
|
||||
|
||||
type WindowArrangePairInput struct {
|
||||
First string `json:"first"`
|
||||
Second string `json:"second"`
|
||||
}
|
||||
type WindowArrangePairOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) windowArrangePair(_ context.Context, _ *mcp.CallToolRequest, input WindowArrangePairInput) (*mcp.CallToolResult, WindowArrangePairOutput, error) {
|
||||
_, _, err := s.core.PERFORM(window.TaskArrangePair{First: input.First, Second: input.Second})
|
||||
if err != nil {
|
||||
return nil, WindowArrangePairOutput{}, err
|
||||
}
|
||||
return nil, WindowArrangePairOutput{Success: true}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
||||
func (s *Subsystem) registerWindowTools(server *mcp.Server) {
|
||||
|
|
@ -395,4 +413,5 @@ func (s *Subsystem) registerWindowTools(server *mcp.Server) {
|
|||
mcp.AddTool(server, &mcp.Tool{Name: "window_always_on_top", Description: "Pin a window above others"}, s.windowAlwaysOnTop)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_background_colour", Description: "Set a window background colour"}, s.windowBackgroundColour)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_fullscreen", Description: "Set a window to fullscreen mode"}, s.windowFullscreen)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "window_arrange_pair", Description: "Arrange two windows side by side"}, s.windowArrangePair)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,12 @@ const (
|
|||
|
||||
// NotificationOptions contains options for sending a notification.
|
||||
type NotificationOptions struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Subtitle string `json:"subtitle,omitempty"`
|
||||
Severity NotificationSeverity `json:"severity,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Subtitle string `json:"subtitle,omitempty"`
|
||||
CategoryID string `json:"categoryId,omitempty"`
|
||||
Severity NotificationSeverity `json:"severity,omitempty"`
|
||||
}
|
||||
|
||||
// PermissionStatus indicates whether notifications are authorised.
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@ import "time"
|
|||
// --- Queries (read-only) ---
|
||||
|
||||
// QueryURL gets the current page URL. Result: string
|
||||
type QueryURL struct{ Window string `json:"window"` }
|
||||
type QueryURL struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// QueryTitle gets the current page title. Result: string
|
||||
type QueryTitle struct{ Window string `json:"window"` }
|
||||
type QueryTitle struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// QueryConsole gets captured console messages. Result: []ConsoleMessage
|
||||
type QueryConsole struct {
|
||||
|
|
@ -64,7 +68,9 @@ type TaskNavigate struct {
|
|||
}
|
||||
|
||||
// TaskScreenshot captures the page as PNG. Result: ScreenshotResult
|
||||
type TaskScreenshot struct{ Window string `json:"window"` }
|
||||
type TaskScreenshot struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// TaskScroll scrolls to an absolute position (window.scrollTo). Result: nil
|
||||
type TaskScroll struct {
|
||||
|
|
@ -108,7 +114,14 @@ type TaskSetViewport struct {
|
|||
}
|
||||
|
||||
// TaskClearConsole clears captured console messages. Result: nil
|
||||
type TaskClearConsole struct{ Window string `json:"window"` }
|
||||
type TaskClearConsole struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// TaskOpenDevTools opens the browser developer tools for a window.
|
||||
type TaskOpenDevTools struct {
|
||||
Window string `json:"window"`
|
||||
}
|
||||
|
||||
// --- Actions (broadcast) ---
|
||||
|
||||
|
|
|
|||
|
|
@ -136,9 +136,21 @@ type TaskExecJS struct {
|
|||
JS string
|
||||
}
|
||||
|
||||
// TaskOpenDevTools opens the developer tools for a named window.
|
||||
type TaskOpenDevTools struct{ Name string }
|
||||
|
||||
// TaskToggleFullscreen toggles fullscreen on a named window.
|
||||
type TaskToggleFullscreen struct{ Name string }
|
||||
|
||||
// TaskArrangePair arranges two windows side-by-side using the primary screen.
|
||||
type TaskArrangePair struct {
|
||||
First string
|
||||
Second string
|
||||
}
|
||||
|
||||
// TaskBesideEditor places a window beside the detected editor or IDE window.
|
||||
type TaskBesideEditor struct{ Name string }
|
||||
|
||||
// TaskPrint triggers the platform print dialog for a named window.
|
||||
type TaskPrint struct{ Name string }
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ type MockWindow struct {
|
|||
html string
|
||||
lastJS string
|
||||
flashing bool
|
||||
devToolsOpened bool
|
||||
printCalled bool
|
||||
toggleFullscreenCount int
|
||||
eventHandlers []func(WindowEvent)
|
||||
|
|
@ -82,9 +83,10 @@ func (w *MockWindow) SetBounds(bounds Bounds) {
|
|||
w.width = bounds.Width
|
||||
w.height = bounds.Height
|
||||
}
|
||||
func (w *MockWindow) ToggleFullscreen() { w.toggleFullscreenCount++ }
|
||||
func (w *MockWindow) Print() error { w.printCalled = true; return nil }
|
||||
func (w *MockWindow) Flash(enabled bool) { w.flashing = enabled }
|
||||
func (w *MockWindow) ToggleFullscreen() { w.toggleFullscreenCount++ }
|
||||
func (w *MockWindow) OpenDevTools() { w.devToolsOpened = true }
|
||||
func (w *MockWindow) Print() error { w.printCalled = true; return nil }
|
||||
func (w *MockWindow) Flash(enabled bool) { w.flashing = enabled }
|
||||
func (w *MockWindow) OnWindowEvent(handler func(WindowEvent)) {
|
||||
w.eventHandlers = append(w.eventHandlers, handler)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ type mockWindow struct {
|
|||
html string
|
||||
lastJS string
|
||||
flashing bool
|
||||
devToolsOpened bool
|
||||
printCalled bool
|
||||
toggleFullscreenCount int
|
||||
eventHandlers []func(WindowEvent)
|
||||
|
|
@ -82,8 +83,9 @@ func (w *mockWindow) SetBounds(bounds Bounds) {
|
|||
w.width = bounds.Width
|
||||
w.height = bounds.Height
|
||||
}
|
||||
func (w *mockWindow) ToggleFullscreen() { w.toggleFullscreenCount++ }
|
||||
func (w *mockWindow) Print() error { w.printCalled = true; return nil }
|
||||
func (w *mockWindow) ToggleFullscreen() { w.toggleFullscreenCount++ }
|
||||
func (w *mockWindow) OpenDevTools() { w.devToolsOpened = true }
|
||||
func (w *mockWindow) Print() error { w.printCalled = true; return nil }
|
||||
func (w *mockWindow) Flash(enabled bool) { w.flashing = enabled }
|
||||
func (w *mockWindow) OnWindowEvent(handler func(WindowEvent)) {
|
||||
w.eventHandlers = append(w.eventHandlers, handler)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ type PlatformWindow interface {
|
|||
|
||||
// Extras
|
||||
ToggleFullscreen()
|
||||
OpenDevTools()
|
||||
Print() error
|
||||
Flash(enabled bool)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package window
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
|
|
@ -162,12 +163,18 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
return nil, true, s.taskSetHTML(t.Name, t.HTML)
|
||||
case TaskExecJS:
|
||||
return nil, true, s.taskExecJS(t.Name, t.JS)
|
||||
case TaskOpenDevTools:
|
||||
return nil, true, s.taskOpenDevTools(t.Name)
|
||||
case TaskToggleFullscreen:
|
||||
return nil, true, s.taskToggleFullscreen(t.Name)
|
||||
case TaskPrint:
|
||||
return nil, true, s.taskPrint(t.Name)
|
||||
case TaskFlash:
|
||||
return nil, true, s.taskFlash(t.Name, t.Enabled)
|
||||
case TaskArrangePair:
|
||||
return nil, true, s.taskArrangePair(t.First, t.Second)
|
||||
case TaskBesideEditor:
|
||||
return nil, true, s.taskBesideEditor(t.Name)
|
||||
case TaskSaveLayout:
|
||||
return nil, true, s.taskSaveLayout(t.Name)
|
||||
case TaskRestoreLayout:
|
||||
|
|
@ -430,6 +437,15 @@ func (s *Service) taskExecJS(name, js string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskOpenDevTools(name string) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
return coreerr.E("window.taskOpenDevTools", "window not found: "+name, nil)
|
||||
}
|
||||
pw.OpenDevTools()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskToggleFullscreen(name string) error {
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
|
|
@ -456,6 +472,61 @@ func (s *Service) taskFlash(name string, enabled bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) taskArrangePair(first, second string) error {
|
||||
if first == "" || second == "" {
|
||||
return coreerr.E("window.taskArrangePair", "two window names are required", nil)
|
||||
}
|
||||
originX, originY, screenWidth, screenHeight := s.primaryScreenArea()
|
||||
return s.manager.TileWindows(TileModeLeftRight, []string{first, second}, screenWidth, screenHeight, originX, originY)
|
||||
}
|
||||
|
||||
func (s *Service) taskBesideEditor(name string) error {
|
||||
if name == "" {
|
||||
return coreerr.E("window.taskBesideEditor", "window name is required", nil)
|
||||
}
|
||||
if _, ok := s.manager.Get(name); !ok {
|
||||
return coreerr.E("window.taskBesideEditor", "window not found: "+name, nil)
|
||||
}
|
||||
|
||||
editor := s.findEditorWindow(name)
|
||||
if editor == "" {
|
||||
names := s.manager.List()
|
||||
for _, candidate := range names {
|
||||
if candidate != name {
|
||||
return s.taskArrangePair(candidate, name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
originX, originY, screenWidth, screenHeight := s.primaryScreenArea()
|
||||
return s.manager.ApplyWorkflow(WorkflowCoding, []string{editor, name}, screenWidth, screenHeight, originX, originY)
|
||||
}
|
||||
|
||||
func (s *Service) findEditorWindow(exclude string) string {
|
||||
keywords := []string{
|
||||
"code", "cursor", "vscode", "visual studio", "intellij", "pycharm",
|
||||
"webstorm", "rider", "sublime", "atom", "neovim", "vim", "emacs",
|
||||
"editor", "ide",
|
||||
}
|
||||
for _, name := range s.manager.List() {
|
||||
if name == exclude {
|
||||
continue
|
||||
}
|
||||
pw, ok := s.manager.Get(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
snippet := strings.ToLower(pw.Name() + " " + pw.Title())
|
||||
for _, keyword := range keywords {
|
||||
if strings.Contains(snippet, keyword) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Service) taskSaveLayout(name string) error {
|
||||
windows := s.queryWindowList()
|
||||
states := make(map[string]WindowState, len(windows))
|
||||
|
|
|
|||
|
|
@ -651,6 +651,29 @@ func TestTaskExecJS_Bad(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// --- TaskOpenDevTools ---
|
||||
|
||||
func TestTaskOpenDevTools_Good(t *testing.T) {
|
||||
svc, c := newTestWindowService(t)
|
||||
_ = requireOpenWindow(t, c, Window{Name: "test"})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskOpenDevTools{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
pw, ok := svc.Manager().Get("test")
|
||||
require.True(t, ok)
|
||||
mw := pw.(*mockWindow)
|
||||
assert.True(t, mw.devToolsOpened)
|
||||
}
|
||||
|
||||
func TestTaskOpenDevTools_Bad(t *testing.T) {
|
||||
_, c := newTestWindowService(t)
|
||||
_, handled, err := c.PERFORM(TaskOpenDevTools{Name: "nonexistent"})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// --- TaskToggleFullscreen ---
|
||||
|
||||
func TestTaskToggleFullscreen_Good(t *testing.T) {
|
||||
|
|
@ -697,6 +720,48 @@ func TestTaskPrint_Bad(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// --- TaskArrangePair / TaskBesideEditor ---
|
||||
|
||||
func TestTaskArrangePair_Good(t *testing.T) {
|
||||
svc, c := newTestWindowService(t)
|
||||
_ = requireOpenWindow(t, c, Window{Name: "left", Width: 800, Height: 600})
|
||||
_ = requireOpenWindow(t, c, Window{Name: "right", Width: 800, Height: 600})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskArrangePair{First: "left", Second: "right"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
left, _ := svc.Manager().Get("left")
|
||||
right, _ := svc.Manager().Get("right")
|
||||
lx, ly := left.Position()
|
||||
rx, ry := right.Position()
|
||||
assert.Equal(t, 0, lx)
|
||||
assert.Equal(t, 0, ly)
|
||||
assert.Equal(t, 960, rx)
|
||||
assert.Equal(t, 0, ry)
|
||||
}
|
||||
|
||||
func TestTaskBesideEditor_Good(t *testing.T) {
|
||||
svc, c := newTestWindowService(t)
|
||||
_ = requireOpenWindow(t, c, Window{Name: "editor", Title: "Cursor"})
|
||||
_ = requireOpenWindow(t, c, Window{Name: "terminal"})
|
||||
|
||||
_, handled, err := c.PERFORM(TaskBesideEditor{Name: "terminal"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
editor, _ := svc.Manager().Get("editor")
|
||||
terminal, _ := svc.Manager().Get("terminal")
|
||||
ex, ey := editor.Position()
|
||||
tx, ty := terminal.Position()
|
||||
assert.Equal(t, 0, ex)
|
||||
assert.Equal(t, 0, ey)
|
||||
assert.Equal(t, 1344, editor.(*mockWindow).width)
|
||||
assert.Equal(t, 1080, editor.(*mockWindow).height)
|
||||
assert.Equal(t, 1344, tx)
|
||||
assert.Equal(t, 0, ty)
|
||||
}
|
||||
|
||||
// --- TaskFlash ---
|
||||
|
||||
func TestTaskFlash_Good(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -85,15 +85,15 @@ func (ww *wailsWindow) Focus() { ww.w.Focus() }
|
|||
func (ww *wailsWindow) Close() { ww.w.Close() }
|
||||
func (ww *wailsWindow) Show() { ww.w.Show() }
|
||||
func (ww *wailsWindow) Hide() { ww.w.Hide() }
|
||||
func (ww *wailsWindow) Fullscreen() { ww.w.Fullscreen() }
|
||||
func (ww *wailsWindow) UnFullscreen() { ww.w.UnFullscreen() }
|
||||
func (ww *wailsWindow) GetZoom() float64 { return ww.w.GetZoom() }
|
||||
func (ww *wailsWindow) SetZoom(factor float64) { ww.w.SetZoom(factor) }
|
||||
func (ww *wailsWindow) ZoomIn() { ww.w.ZoomIn() }
|
||||
func (ww *wailsWindow) ZoomOut() { ww.w.ZoomOut() }
|
||||
func (ww *wailsWindow) SetURL(url string) { ww.w.SetURL(url) }
|
||||
func (ww *wailsWindow) SetHTML(html string) { ww.w.SetHTML(html) }
|
||||
func (ww *wailsWindow) ExecJS(js string) { ww.w.ExecJS(js) }
|
||||
func (ww *wailsWindow) Fullscreen() { ww.w.Fullscreen() }
|
||||
func (ww *wailsWindow) UnFullscreen() { ww.w.UnFullscreen() }
|
||||
func (ww *wailsWindow) GetZoom() float64 { return ww.w.GetZoom() }
|
||||
func (ww *wailsWindow) SetZoom(factor float64) { ww.w.SetZoom(factor) }
|
||||
func (ww *wailsWindow) ZoomIn() { ww.w.ZoomIn() }
|
||||
func (ww *wailsWindow) ZoomOut() { ww.w.ZoomOut() }
|
||||
func (ww *wailsWindow) SetURL(url string) { ww.w.SetURL(url) }
|
||||
func (ww *wailsWindow) SetHTML(html string) { ww.w.SetHTML(html) }
|
||||
func (ww *wailsWindow) ExecJS(js string) { ww.w.ExecJS(js) }
|
||||
func (ww *wailsWindow) GetBounds() Bounds {
|
||||
r := ww.w.Bounds()
|
||||
return Bounds{X: r.X, Y: r.Y, Width: r.Width, Height: r.Height}
|
||||
|
|
@ -101,8 +101,9 @@ func (ww *wailsWindow) GetBounds() Bounds {
|
|||
func (ww *wailsWindow) SetBounds(b Bounds) {
|
||||
ww.w.SetBounds(application.Rect{X: b.X, Y: b.Y, Width: b.Width, Height: b.Height})
|
||||
}
|
||||
func (ww *wailsWindow) ToggleFullscreen() { ww.w.ToggleFullscreen() }
|
||||
func (ww *wailsWindow) Print() error { return ww.w.Print() }
|
||||
func (ww *wailsWindow) ToggleFullscreen() { ww.w.ToggleFullscreen() }
|
||||
func (ww *wailsWindow) OpenDevTools() { ww.w.OpenDevTools() }
|
||||
func (ww *wailsWindow) Print() error { return ww.w.Print() }
|
||||
func (ww *wailsWindow) Flash(enabled bool) { ww.w.Flash(enabled) }
|
||||
|
||||
func (ww *wailsWindow) OnWindowEvent(handler func(event WindowEvent)) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue