diff --git a/pkg/display/clipboard.go b/pkg/display/clipboard.go deleted file mode 100644 index 32ffba1..0000000 --- a/pkg/display/clipboard.go +++ /dev/null @@ -1,61 +0,0 @@ -package display - -import ( - "fmt" - - "github.com/wailsapp/wails/v3/pkg/application" -) - -// ClipboardContentType represents the type of content in the clipboard. -type ClipboardContentType string - -const ( - ClipboardText ClipboardContentType = "text" - ClipboardImage ClipboardContentType = "image" - ClipboardHTML ClipboardContentType = "html" -) - -// ClipboardContent holds clipboard data. -type ClipboardContent struct { - Type ClipboardContentType `json:"type"` - Text string `json:"text,omitempty"` - HTML string `json:"html,omitempty"` -} - -// ReadClipboard reads text content from the system clipboard. -func (s *Service) ReadClipboard() (string, error) { - app := application.Get() - if app == nil || app.Clipboard == nil { - return "", fmt.Errorf("application or clipboard not available") - } - - text, ok := app.Clipboard.Text() - if !ok { - return "", fmt.Errorf("failed to read clipboard") - } - return text, nil -} - -// WriteClipboard writes text content to the system clipboard. -func (s *Service) WriteClipboard(text string) error { - app := application.Get() - if app == nil || app.Clipboard == nil { - return fmt.Errorf("application or clipboard not available") - } - - if !app.Clipboard.SetText(text) { - return fmt.Errorf("failed to write to clipboard") - } - return nil -} - -// HasClipboard checks if the clipboard has content. -func (s *Service) HasClipboard() bool { - text, err := s.ReadClipboard() - return err == nil && text != "" -} - -// ClearClipboard clears the clipboard by setting empty text. -func (s *Service) ClearClipboard() error { - return s.WriteClipboard("") -} diff --git a/pkg/display/dialog.go b/pkg/display/dialog.go deleted file mode 100644 index f9078a1..0000000 --- a/pkg/display/dialog.go +++ /dev/null @@ -1,192 +0,0 @@ -package display - -import ( - "fmt" - - "github.com/wailsapp/wails/v3/pkg/application" -) - -// FileFilter represents a file type filter for dialogs. -type FileFilter struct { - DisplayName string `json:"displayName"` - Pattern string `json:"pattern"` - Extensions []string `json:"extensions,omitempty"` -} - -// OpenFileOptions contains options for the open file dialog. -type OpenFileOptions struct { - Title string `json:"title,omitempty"` - DefaultDirectory string `json:"defaultDirectory,omitempty"` - DefaultFilename string `json:"defaultFilename,omitempty"` - Filters []FileFilter `json:"filters,omitempty"` - AllowMultiple bool `json:"allowMultiple,omitempty"` -} - -// SaveFileOptions contains options for the save file dialog. -type SaveFileOptions struct { - Title string `json:"title,omitempty"` - DefaultDirectory string `json:"defaultDirectory,omitempty"` - DefaultFilename string `json:"defaultFilename,omitempty"` - Filters []FileFilter `json:"filters,omitempty"` -} - -// OpenDirectoryOptions contains options for the directory picker. -type OpenDirectoryOptions struct { - Title string `json:"title,omitempty"` - DefaultDirectory string `json:"defaultDirectory,omitempty"` - AllowMultiple bool `json:"allowMultiple,omitempty"` -} - -// OpenFileDialog shows a file open dialog and returns selected path(s). -func (s *Service) OpenFileDialog(opts OpenFileOptions) ([]string, error) { - app := application.Get() - if app == nil { - return nil, fmt.Errorf("application not available") - } - - dialog := app.Dialog.OpenFile() - - if opts.Title != "" { - dialog.SetTitle(opts.Title) - } - if opts.DefaultDirectory != "" { - dialog.SetDirectory(opts.DefaultDirectory) - } - - // Add filters - for _, f := range opts.Filters { - dialog.AddFilter(f.DisplayName, f.Pattern) - } - - if opts.AllowMultiple { - dialog.CanChooseFiles(true) - // Use PromptForMultipleSelection for multiple files - paths, err := dialog.PromptForMultipleSelection() - if err != nil { - return nil, fmt.Errorf("dialog error: %w", err) - } - return paths, nil - } - - // Single selection - path, err := dialog.PromptForSingleSelection() - if err != nil { - return nil, fmt.Errorf("dialog error: %w", err) - } - - if path == "" { - return []string{}, nil - } - return []string{path}, nil -} - -// OpenSingleFileDialog shows a file open dialog for a single file. -func (s *Service) OpenSingleFileDialog(opts OpenFileOptions) (string, error) { - app := application.Get() - if app == nil { - return "", fmt.Errorf("application not available") - } - - dialog := app.Dialog.OpenFile() - - if opts.Title != "" { - dialog.SetTitle(opts.Title) - } - if opts.DefaultDirectory != "" { - dialog.SetDirectory(opts.DefaultDirectory) - } - - for _, f := range opts.Filters { - dialog.AddFilter(f.DisplayName, f.Pattern) - } - - path, err := dialog.PromptForSingleSelection() - if err != nil { - return "", fmt.Errorf("dialog error: %w", err) - } - - return path, nil -} - -// SaveFileDialog shows a save file dialog and returns the selected path. -func (s *Service) SaveFileDialog(opts SaveFileOptions) (string, error) { - app := application.Get() - if app == nil { - return "", fmt.Errorf("application not available") - } - - dialog := app.Dialog.SaveFile() - - if opts.DefaultDirectory != "" { - dialog.SetDirectory(opts.DefaultDirectory) - } - if opts.DefaultFilename != "" { - dialog.SetFilename(opts.DefaultFilename) - } - - for _, f := range opts.Filters { - dialog.AddFilter(f.DisplayName, f.Pattern) - } - - path, err := dialog.PromptForSingleSelection() - if err != nil { - return "", fmt.Errorf("dialog error: %w", err) - } - - return path, nil -} - -// OpenDirectoryDialog shows a directory picker. -func (s *Service) OpenDirectoryDialog(opts OpenDirectoryOptions) (string, error) { - app := application.Get() - if app == nil { - return "", fmt.Errorf("application not available") - } - - // Use OpenFile dialog with directory selection - dialog := app.Dialog.OpenFile() - dialog.CanChooseDirectories(true) - dialog.CanChooseFiles(false) - - if opts.Title != "" { - dialog.SetTitle(opts.Title) - } - if opts.DefaultDirectory != "" { - dialog.SetDirectory(opts.DefaultDirectory) - } - - path, err := dialog.PromptForSingleSelection() - if err != nil { - return "", fmt.Errorf("dialog error: %w", err) - } - - return path, nil -} - -// ConfirmDialog shows a confirmation dialog and returns the user's choice. -func (s *Service) ConfirmDialog(title, message string) (bool, error) { - app := application.Get() - if app == nil { - return false, fmt.Errorf("application not available") - } - - dialog := app.Dialog.Question() - dialog.SetTitle(title) - dialog.SetMessage(message) - dialog.AddButton("Yes").SetAsDefault() - dialog.AddButton("No") - - dialog.Show() - // Note: Wails v3 Question dialog Show() doesn't return a value - // The button callbacks would need to be used for async handling - // For now, return true as we showed the dialog - return true, nil -} - -// PromptDialog shows an input prompt dialog. -// Note: Wails v3 doesn't have a native prompt dialog, so this uses a question dialog. -func (s *Service) PromptDialog(title, message string) (string, bool, error) { - // Wails v3 doesn't have a native text input dialog - // For now, return an error suggesting to use webview-based input - return "", false, fmt.Errorf("text input dialogs not supported natively; use webview-based input instead") -} diff --git a/pkg/display/display.go b/pkg/display/display.go index 33a3b78..cb0405a 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -9,6 +9,7 @@ import ( "forge.lthn.ai/core/go-config" "forge.lthn.ai/core/go/pkg/core" + "forge.lthn.ai/core/gui/pkg/dialog" "forge.lthn.ai/core/gui/pkg/environment" "forge.lthn.ai/core/gui/pkg/menu" "forge.lthn.ai/core/gui/pkg/notification" @@ -16,7 +17,6 @@ import ( "forge.lthn.ai/core/gui/pkg/systray" "forge.lthn.ai/core/gui/pkg/window" "github.com/wailsapp/wails/v3/pkg/application" - "github.com/wailsapp/wails/v3/pkg/services/notifications" ) // Options holds configuration for the display service. @@ -35,8 +35,7 @@ type Service struct { config Options configData map[string]map[string]any cfg *config.Config // go-config instance for file persistence - notifier *notifications.NotificationService - events *WSEventManager + events *WSEventManager } // New is the constructor for the display service. @@ -77,8 +76,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(newWailsEventSource(s.wailsApp)) - s.events.SetupWindowEventListeners() + s.events = NewWSEventManager() } return nil @@ -165,8 +163,18 @@ func (s *Service) handleTrayAction(actionID string) { case "close-desktop": // Hide all windows — future: add TaskHideWindow case "env-info": - if s.app != nil { - s.ShowEnvironmentDialog() + // Query environment info via IPC and show as dialog + result, handled, _ := s.Core().QUERY(environment.QueryInfo{}) + if handled { + info := result.(environment.EnvironmentInfo) + details := fmt.Sprintf("OS: %s\nArch: %s\nPlatform: %s %s", + info.OS, info.Arch, info.Platform.Name, info.Platform.Version) + _, _, _ = s.Core().PERFORM(dialog.TaskMessageDialog{ + Opts: dialog.MessageDialogOptions{ + Type: dialog.DialogInfo, Title: "Environment", + Message: details, Buttons: []string{"OK"}, + }, + }) } case "quit": if s.app != nil { @@ -619,171 +627,6 @@ func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error { return ws.Manager().ApplyWorkflow(workflow, ws.Manager().List(), 1920, 1080) } -// --- Screen queries (remain in display — use application.Get() directly) --- - -// ScreenInfo contains information about a display screen. -type ScreenInfo struct { - ID string `json:"id"` - Name string `json:"name"` - X int `json:"x"` - Y int `json:"y"` - Width int `json:"width"` - Height int `json:"height"` - Primary bool `json:"primary"` -} - -// WorkArea represents usable screen space. -type WorkArea struct { - ScreenID string `json:"screenId"` - X int `json:"x"` - Y int `json:"y"` - Width int `json:"width"` - Height int `json:"height"` -} - -// GetScreens returns information about all available screens. -func (s *Service) GetScreens() []ScreenInfo { - app := application.Get() - if app == nil || app.Screen == nil { - return nil - } - screens := app.Screen.GetAll() - if screens == nil { - return nil - } - result := make([]ScreenInfo, 0, len(screens)) - for _, screen := range screens { - result = append(result, ScreenInfo{ - ID: screen.ID, - Name: screen.Name, - X: screen.Bounds.X, - Y: screen.Bounds.Y, - Width: screen.Bounds.Width, - Height: screen.Bounds.Height, - Primary: screen.IsPrimary, - }) - } - return result -} - -// GetWorkAreas returns the usable work area for all screens. -func (s *Service) GetWorkAreas() []WorkArea { - app := application.Get() - if app == nil || app.Screen == nil { - return nil - } - screens := app.Screen.GetAll() - if screens == nil { - return nil - } - result := make([]WorkArea, 0, len(screens)) - for _, screen := range screens { - result = append(result, WorkArea{ - ScreenID: screen.ID, - X: screen.WorkArea.X, - Y: screen.WorkArea.Y, - Width: screen.WorkArea.Width, - Height: screen.WorkArea.Height, - }) - } - return result -} - -// GetPrimaryScreen returns information about the primary screen. -func (s *Service) GetPrimaryScreen() (*ScreenInfo, error) { - app := application.Get() - if app == nil || app.Screen == nil { - return nil, fmt.Errorf("screen service not available") - } - screens := app.Screen.GetAll() - for _, screen := range screens { - if screen.IsPrimary { - return &ScreenInfo{ - ID: screen.ID, Name: screen.Name, - X: screen.Bounds.X, Y: screen.Bounds.Y, - Width: screen.Bounds.Width, Height: screen.Bounds.Height, - Primary: true, - }, nil - } - } - return nil, fmt.Errorf("no primary screen found") -} - -// GetScreen returns information about a specific screen by ID. -func (s *Service) GetScreen(id string) (*ScreenInfo, error) { - app := application.Get() - if app == nil || app.Screen == nil { - return nil, fmt.Errorf("screen service not available") - } - screens := app.Screen.GetAll() - for _, screen := range screens { - if screen.ID == id { - return &ScreenInfo{ - ID: screen.ID, Name: screen.Name, - X: screen.Bounds.X, Y: screen.Bounds.Y, - Width: screen.Bounds.Width, Height: screen.Bounds.Height, - Primary: screen.IsPrimary, - }, nil - } - } - return nil, fmt.Errorf("screen not found: %s", id) -} - -// GetScreenAtPoint returns the screen containing a specific point. -func (s *Service) GetScreenAtPoint(x, y int) (*ScreenInfo, error) { - app := application.Get() - if app == nil || app.Screen == nil { - return nil, fmt.Errorf("screen service not available") - } - screens := app.Screen.GetAll() - for _, screen := range screens { - bounds := screen.Bounds - if x >= bounds.X && x < bounds.X+bounds.Width && - y >= bounds.Y && y < bounds.Y+bounds.Height { - return &ScreenInfo{ - ID: screen.ID, Name: screen.Name, - X: bounds.X, Y: bounds.Y, - Width: bounds.Width, Height: bounds.Height, - Primary: screen.IsPrimary, - }, nil - } - } - return nil, fmt.Errorf("no screen found at point (%d, %d)", x, y) -} - -// GetScreenForWindow returns the screen containing a specific window. -func (s *Service) GetScreenForWindow(name string) (*ScreenInfo, error) { - info, err := s.GetWindowInfo(name) - if err != nil { - return nil, err - } - centerX := info.X + info.Width/2 - centerY := info.Y + info.Height/2 - return s.GetScreenAtPoint(centerX, centerY) -} - -// ShowEnvironmentDialog displays environment information. -func (s *Service) ShowEnvironmentDialog() { - envInfo := s.app.Env().Info() - details := "Environment Information:\n\n" - details += fmt.Sprintf("Operating System: %s\n", envInfo.OS) - details += fmt.Sprintf("Architecture: %s\n", envInfo.Arch) - details += fmt.Sprintf("Debug Mode: %t\n\n", envInfo.Debug) - details += fmt.Sprintf("Dark Mode: %t\n\n", s.app.Env().IsDarkMode()) - details += "Platform Information:" - for key, value := range envInfo.PlatformInfo { - details += fmt.Sprintf("\n%s: %v", key, value) - } - if envInfo.OSInfo != nil { - details += fmt.Sprintf("\n\nOS Details:\nName: %s\nVersion: %s", - envInfo.OSInfo.Name, envInfo.OSInfo.Version) - } - dialog := s.app.Dialog().Info() - dialog.SetTitle("Environment Information") - dialog.SetMessage(details) - dialog.Show() -} - // GetEventManager returns the event manager for WebSocket event subscriptions. func (s *Service) GetEventManager() *WSEventManager { return s.events diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index 657d252..03eb1d2 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -432,25 +432,13 @@ func TestHandleListWorkspaces_Good(t *testing.T) { } func TestWSEventManager_Good(t *testing.T) { - es := newMockEventSource() - em := NewWSEventManager(es) + em := NewWSEventManager() defer em.Close() assert.NotNil(t, em) assert.Equal(t, 0, em.ConnectedClients()) } -func TestWSEventManager_SetupWindowEventListeners_Good(t *testing.T) { - es := newMockEventSource() - em := NewWSEventManager(es) - defer em.Close() - - em.SetupWindowEventListeners() - - // Verify theme handler was registered - assert.Len(t, es.themeHandlers, 1) -} - // --- Config file loading tests --- func TestLoadConfig_Good(t *testing.T) { diff --git a/pkg/display/events.go b/pkg/display/events.go index 7ac6bcf..e0812d1 100644 --- a/pkg/display/events.go +++ b/pkg/display/events.go @@ -47,7 +47,6 @@ type WSEventManager struct { upgrader websocket.Upgrader clients map[*websocket.Conn]*clientState mu sync.RWMutex - eventSource EventSource nextSubID int eventBuffer chan Event } @@ -59,8 +58,7 @@ type clientState struct { } // NewWSEventManager creates a new event manager. -// It accepts an EventSource for theme change events instead of using application.Get() directly. -func NewWSEventManager(es EventSource) *WSEventManager { +func NewWSEventManager() *WSEventManager { em := &WSEventManager{ upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { @@ -70,7 +68,6 @@ func NewWSEventManager(es EventSource) *WSEventManager { WriteBufferSize: 1024, }, clients: make(map[*websocket.Conn]*clientState), - eventSource: es, eventBuffer: make(chan Event, 100), } @@ -305,26 +302,6 @@ func (em *WSEventManager) Close() { close(em.eventBuffer) } -// SetupWindowEventListeners registers listeners for application-level events. -// Uses EventSource instead of application.Get() directly. -func (em *WSEventManager) SetupWindowEventListeners() { - if em.eventSource != nil { - em.eventSource.OnThemeChange(func(isDark bool) { - theme := "light" - if isDark { - theme = "dark" - } - em.Emit(Event{ - Type: EventThemeChange, - Data: map[string]any{ - "isDark": isDark, - "theme": theme, - }, - }) - }) - } -} - // AttachWindowListeners attaches event listeners to a specific window. // Accepts window.PlatformWindow instead of *application.WebviewWindow. func (em *WSEventManager) AttachWindowListeners(pw window.PlatformWindow) { diff --git a/pkg/display/interfaces.go b/pkg/display/interfaces.go index c1a8b3a..324bd97 100644 --- a/pkg/display/interfaces.go +++ b/pkg/display/interfaces.go @@ -76,19 +76,3 @@ func (ev *wailsEventManager) Emit(name string, data ...any) bool { return ev.app.Event.Emit(name, data...) } -// wailsEventSource implements EventSource using a Wails app. -type wailsEventSource struct{ app *application.App } - -func newWailsEventSource(app *application.App) EventSource { - return &wailsEventSource{app: app} -} - -func (es *wailsEventSource) OnThemeChange(handler func(isDark bool)) func() { - return es.app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(_ *application.ApplicationEvent) { - handler(es.app.Env.IsDarkMode()) - }) -} - -func (es *wailsEventSource) Emit(name string, data ...any) bool { - return es.app.Event.Emit(name, data...) -} diff --git a/pkg/display/mocks_test.go b/pkg/display/mocks_test.go index c008e43..197a8fe 100644 --- a/pkg/display/mocks_test.go +++ b/pkg/display/mocks_test.go @@ -1,19 +1 @@ package display - -// mockEventSource implements EventSource for testing. -type mockEventSource struct { - themeHandlers []func(isDark bool) -} - -func newMockEventSource() *mockEventSource { - return &mockEventSource{} -} - -func (m *mockEventSource) OnThemeChange(handler func(isDark bool)) func() { - m.themeHandlers = append(m.themeHandlers, handler) - return func() {} -} - -func (m *mockEventSource) Emit(name string, data ...any) bool { - return true -} diff --git a/pkg/display/notification.go b/pkg/display/notification.go deleted file mode 100644 index 0c8150f..0000000 --- a/pkg/display/notification.go +++ /dev/null @@ -1,127 +0,0 @@ -package display - -import ( - "fmt" - "time" - - "github.com/wailsapp/wails/v3/pkg/application" - "github.com/wailsapp/wails/v3/pkg/services/notifications" -) - -// NotificationOptions contains options for showing a notification. -type NotificationOptions struct { - ID string `json:"id,omitempty"` - Title string `json:"title"` - Message string `json:"message"` - Subtitle string `json:"subtitle,omitempty"` -} - -// SetNotifier sets the notifications service for native notifications. -func (s *Service) SetNotifier(notifier *notifications.NotificationService) { - s.notifier = notifier -} - -// ShowNotification displays a native system notification. -// Falls back to dialog if notifier is not available. -func (s *Service) ShowNotification(opts NotificationOptions) error { - // Try native notification first - if s.notifier != nil { - // Generate ID if not provided - id := opts.ID - if id == "" { - id = fmt.Sprintf("core-%d", time.Now().UnixNano()) - } - - return s.notifier.SendNotification(notifications.NotificationOptions{ - ID: id, - Title: opts.Title, - Subtitle: opts.Subtitle, - Body: opts.Message, - }) - } - - // Fall back to dialog-based notification - return s.showDialogNotification(opts) -} - -// showDialogNotification shows a notification using dialogs as fallback. -func (s *Service) showDialogNotification(opts NotificationOptions) error { - app := application.Get() - if app == nil { - return fmt.Errorf("application not available") - } - - // Build message with optional subtitle - msg := opts.Message - if opts.Subtitle != "" { - msg = opts.Subtitle + "\n\n" + msg - } - - dialog := app.Dialog.Info() - dialog.SetTitle(opts.Title) - dialog.SetMessage(msg) - dialog.Show() - - return nil -} - -// ShowInfoNotification shows an info notification with a simple message. -func (s *Service) ShowInfoNotification(title, message string) error { - return s.ShowNotification(NotificationOptions{ - Title: title, - Message: message, - }) -} - -// ShowWarningNotification shows a warning notification. -func (s *Service) ShowWarningNotification(title, message string) error { - app := application.Get() - if app == nil { - return fmt.Errorf("application not available") - } - - dialog := app.Dialog.Warning() - dialog.SetTitle(title) - dialog.SetMessage(message) - dialog.Show() - - return nil -} - -// ShowErrorNotification shows an error notification. -func (s *Service) ShowErrorNotification(title, message string) error { - app := application.Get() - if app == nil { - return fmt.Errorf("application not available") - } - - dialog := app.Dialog.Error() - dialog.SetTitle(title) - dialog.SetMessage(message) - dialog.Show() - - return nil -} - -// RequestNotificationPermission requests permission for native notifications. -func (s *Service) RequestNotificationPermission() (bool, error) { - if s.notifier == nil { - return false, fmt.Errorf("notification service not available") - } - - granted, err := s.notifier.RequestNotificationAuthorization() - if err != nil { - return false, fmt.Errorf("failed to request notification permission: %w", err) - } - - return granted, nil -} - -// CheckNotificationPermission checks if notifications are authorized. -func (s *Service) CheckNotificationPermission() (bool, error) { - if s.notifier == nil { - return false, fmt.Errorf("notification service not available") - } - - return s.notifier.CheckNotificationAuthorization() -} diff --git a/pkg/display/theme.go b/pkg/display/theme.go deleted file mode 100644 index 223a475..0000000 --- a/pkg/display/theme.go +++ /dev/null @@ -1,38 +0,0 @@ -package display - -import ( - "github.com/wailsapp/wails/v3/pkg/application" -) - -// ThemeInfo contains information about the current theme. -type ThemeInfo struct { - IsDark bool `json:"isDark"` - Theme string `json:"theme"` // "dark" or "light" - System bool `json:"system"` // Whether following system theme -} - -// GetTheme returns the current application theme. -func (s *Service) GetTheme() ThemeInfo { - app := application.Get() - if app == nil { - return ThemeInfo{Theme: "unknown"} - } - - isDark := app.Env.IsDarkMode() - theme := "light" - if isDark { - theme = "dark" - } - - return ThemeInfo{ - IsDark: isDark, - Theme: theme, - System: true, // Wails follows system theme by default - } -} - -// GetSystemTheme returns the system's theme preference. -// This is the same as GetTheme since Wails follows the system theme. -func (s *Service) GetSystemTheme() ThemeInfo { - return s.GetTheme() -} diff --git a/pkg/display/types.go b/pkg/display/types.go deleted file mode 100644 index c1fd64b..0000000 --- a/pkg/display/types.go +++ /dev/null @@ -1,9 +0,0 @@ -// pkg/display/types.go -package display - -// EventSource abstracts the application event system (Wails insulation for WSEventManager). -// WSEventManager receives this instead of calling application.Get() directly. -type EventSource interface { - OnThemeChange(handler func(isDark bool)) func() - Emit(name string, data ...any) bool -}