gui/pkg/window/layout.go
Snider 4814f960fb refactor(display): compose window/systray/menu sub-packages into orchestrator
Service now delegates to window.Manager, systray.Manager, and menu.Manager
instead of directly using Wails types. WSEventManager accepts EventSource
interface instead of calling application.Get() directly.
AttachWindowListeners now accepts window.PlatformWindow.

Removes migrated files: window.go, window_state.go, layout.go, tray.go, menu.go.
Tests rewritten against mock platform implementations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 12:27:19 +00:00

143 lines
3.3 KiB
Go

// pkg/window/layout.go
package window
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
// Layout is a named window arrangement.
type Layout struct {
Name string `json:"name"`
Windows map[string]WindowState `json:"windows"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
// LayoutInfo is a summary of a layout.
type LayoutInfo struct {
Name string `json:"name"`
WindowCount int `json:"windowCount"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
// LayoutManager persists named window arrangements to ~/.config/Core/layouts.json.
type LayoutManager struct {
configDir string
layouts map[string]Layout
mu sync.RWMutex
}
// NewLayoutManager creates a LayoutManager loading from the default config directory.
func NewLayoutManager() *LayoutManager {
lm := &LayoutManager{
layouts: make(map[string]Layout),
}
configDir, err := os.UserConfigDir()
if err == nil {
lm.configDir = filepath.Join(configDir, "Core")
}
lm.load()
return lm
}
// NewLayoutManagerWithDir creates a LayoutManager loading from a custom config directory.
// Useful for testing or when the default config directory is not appropriate.
func NewLayoutManagerWithDir(configDir string) *LayoutManager {
lm := &LayoutManager{
configDir: configDir,
layouts: make(map[string]Layout),
}
lm.load()
return lm
}
func (lm *LayoutManager) filePath() string {
return filepath.Join(lm.configDir, "layouts.json")
}
func (lm *LayoutManager) load() {
if lm.configDir == "" {
return
}
data, err := os.ReadFile(lm.filePath())
if err != nil {
return
}
lm.mu.Lock()
defer lm.mu.Unlock()
_ = json.Unmarshal(data, &lm.layouts)
}
func (lm *LayoutManager) save() {
if lm.configDir == "" {
return
}
lm.mu.RLock()
data, err := json.MarshalIndent(lm.layouts, "", " ")
lm.mu.RUnlock()
if err != nil {
return
}
_ = os.MkdirAll(lm.configDir, 0o755)
_ = os.WriteFile(lm.filePath(), data, 0o644)
}
// SaveLayout creates or updates a named layout.
func (lm *LayoutManager) SaveLayout(name string, windowStates map[string]WindowState) error {
if name == "" {
return fmt.Errorf("layout name cannot be empty")
}
now := time.Now().UnixMilli()
lm.mu.Lock()
existing, exists := lm.layouts[name]
layout := Layout{
Name: name,
Windows: windowStates,
UpdatedAt: now,
}
if exists {
layout.CreatedAt = existing.CreatedAt
} else {
layout.CreatedAt = now
}
lm.layouts[name] = layout
lm.mu.Unlock()
lm.save()
return nil
}
// GetLayout returns a layout by name.
func (lm *LayoutManager) GetLayout(name string) (Layout, bool) {
lm.mu.RLock()
defer lm.mu.RUnlock()
l, ok := lm.layouts[name]
return l, ok
}
// ListLayouts returns info summaries for all layouts.
func (lm *LayoutManager) ListLayouts() []LayoutInfo {
lm.mu.RLock()
defer lm.mu.RUnlock()
infos := make([]LayoutInfo, 0, len(lm.layouts))
for _, l := range lm.layouts {
infos = append(infos, LayoutInfo{
Name: l.Name, WindowCount: len(l.Windows),
CreatedAt: l.CreatedAt, UpdatedAt: l.UpdatedAt,
})
}
return infos
}
// DeleteLayout removes a layout by name.
func (lm *LayoutManager) DeleteLayout(name string) {
lm.mu.Lock()
delete(lm.layouts, name)
lm.mu.Unlock()
lm.save()
}