// pkg/window/layout.go package window import ( "sync" "time" core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" ) // 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), } if configDir := core.Env("DIR_CONFIG"); configDir != "" { lm.configDir = core.JoinPath(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 core.JoinPath(lm.configDir, "layouts.json") } func (lm *LayoutManager) load() { if lm.configDir == "" { return } content, err := coreio.Local.Read(lm.filePath()) if err != nil { return } lm.mu.Lock() defer lm.mu.Unlock() _ = core.JSONUnmarshalString(content, &lm.layouts) } func (lm *LayoutManager) save() { if lm.configDir == "" { return } lm.mu.RLock() result := core.JSONMarshal(lm.layouts) lm.mu.RUnlock() if !result.OK { return } data := result.Value.([]byte) _ = coreio.Local.EnsureDir(lm.configDir) _ = coreio.Local.Write(lm.filePath(), string(data)) } // SaveLayout creates or updates a named layout. func (lm *LayoutManager) SaveLayout(name string, windowStates map[string]WindowState) error { if name == "" { return coreerr.E("window.LayoutManager.SaveLayout", "layout name cannot be empty", nil) } 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() }