Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath, errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim, core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(), core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives. Framework boundary exceptions preserved where stdlib types are required by external interfaces (Gin, net/http, CGo, Wails, bubbletea). Co-Authored-By: Virgil <virgil@lethean.io>
241 lines
5.8 KiB
Go
241 lines
5.8 KiB
Go
// pkg/window/state.go
|
|
package window
|
|
|
|
import (
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
corego "dappco.re/go/core"
|
|
)
|
|
|
|
// WindowState holds the persisted position/size of a window.
|
|
// JSON tags match existing window_state.json format for backward compat.
|
|
// Use: state := window.WindowState{X: 10, Y: 20, Width: 1280, Height: 800}
|
|
type WindowState struct {
|
|
X int `json:"x,omitempty"`
|
|
Y int `json:"y,omitempty"`
|
|
Width int `json:"width,omitempty"`
|
|
Height int `json:"height,omitempty"`
|
|
Maximized bool `json:"maximized,omitempty"`
|
|
Screen string `json:"screen,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
UpdatedAt int64 `json:"updatedAt,omitempty"`
|
|
}
|
|
|
|
// StateManager persists window positions to ~/.config/Core/window_state.json.
|
|
// Use: sm := window.NewStateManager()
|
|
type StateManager struct {
|
|
configDir string
|
|
statePath string
|
|
states map[string]WindowState
|
|
mu sync.RWMutex
|
|
saveTimer *time.Timer
|
|
}
|
|
|
|
// NewStateManager creates a StateManager loading from the default config directory.
|
|
// Use: sm := window.NewStateManager()
|
|
func NewStateManager() *StateManager {
|
|
sm := &StateManager{
|
|
states: make(map[string]WindowState),
|
|
}
|
|
configDir, err := os.UserConfigDir()
|
|
if err == nil {
|
|
sm.configDir = corego.JoinPath(configDir, "Core")
|
|
}
|
|
sm.load()
|
|
return sm
|
|
}
|
|
|
|
// NewStateManagerWithDir creates a StateManager loading from a custom config directory.
|
|
// Useful for testing or when the default config directory is not appropriate.
|
|
// Use: sm := window.NewStateManagerWithDir(t.TempDir())
|
|
func NewStateManagerWithDir(configDir string) *StateManager {
|
|
sm := &StateManager{
|
|
configDir: configDir,
|
|
states: make(map[string]WindowState),
|
|
}
|
|
sm.load()
|
|
return sm
|
|
}
|
|
|
|
func (sm *StateManager) filePath() string {
|
|
if sm.statePath != "" {
|
|
return sm.statePath
|
|
}
|
|
return corego.JoinPath(sm.configDir, "window_state.json")
|
|
}
|
|
|
|
func (sm *StateManager) dataDir() string {
|
|
if sm.statePath != "" {
|
|
return corego.PathDir(sm.statePath)
|
|
}
|
|
return sm.configDir
|
|
}
|
|
|
|
// SetPath overrides the persisted state file path.
|
|
// Use: sm.SetPath(filepath.Join(t.TempDir(), "window_state.json"))
|
|
func (sm *StateManager) SetPath(path string) {
|
|
if path == "" {
|
|
return
|
|
}
|
|
sm.mu.Lock()
|
|
if sm.saveTimer != nil {
|
|
sm.saveTimer.Stop()
|
|
sm.saveTimer = nil
|
|
}
|
|
sm.statePath = path
|
|
sm.states = make(map[string]WindowState)
|
|
sm.mu.Unlock()
|
|
sm.load()
|
|
}
|
|
|
|
func (sm *StateManager) load() {
|
|
if sm.configDir == "" && sm.statePath == "" {
|
|
return
|
|
}
|
|
data, err := os.ReadFile(sm.filePath())
|
|
if err != nil {
|
|
return
|
|
}
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
_ = corego.JSONUnmarshal(data, &sm.states)
|
|
}
|
|
|
|
func (sm *StateManager) save() {
|
|
if sm.configDir == "" && sm.statePath == "" {
|
|
return
|
|
}
|
|
sm.mu.RLock()
|
|
r := corego.JSONMarshal(sm.states)
|
|
sm.mu.RUnlock()
|
|
if !r.OK {
|
|
return
|
|
}
|
|
data := r.Value.([]byte)
|
|
_ = os.MkdirAll(sm.dataDir(), 0o755)
|
|
_ = os.WriteFile(sm.filePath(), data, 0o644)
|
|
}
|
|
|
|
func (sm *StateManager) scheduleSave() {
|
|
if sm.saveTimer != nil {
|
|
sm.saveTimer.Stop()
|
|
}
|
|
sm.saveTimer = time.AfterFunc(500*time.Millisecond, sm.save)
|
|
}
|
|
|
|
// GetState returns the saved state for a window name.
|
|
// Use: state, ok := sm.GetState("editor")
|
|
func (sm *StateManager) GetState(name string) (WindowState, bool) {
|
|
sm.mu.RLock()
|
|
defer sm.mu.RUnlock()
|
|
s, ok := sm.states[name]
|
|
return s, ok
|
|
}
|
|
|
|
// SetState saves state for a window name (debounced disk write).
|
|
// Use: sm.SetState("editor", window.WindowState{Width: 1280, Height: 800})
|
|
func (sm *StateManager) SetState(name string, state WindowState) {
|
|
state.UpdatedAt = time.Now().UnixMilli()
|
|
sm.mu.Lock()
|
|
sm.states[name] = state
|
|
sm.mu.Unlock()
|
|
sm.scheduleSave()
|
|
}
|
|
|
|
// UpdatePosition updates only the position fields.
|
|
// Use: sm.UpdatePosition("editor", 160, 120)
|
|
func (sm *StateManager) UpdatePosition(name string, x, y int) {
|
|
sm.mu.Lock()
|
|
s := sm.states[name]
|
|
s.X = x
|
|
s.Y = y
|
|
s.UpdatedAt = time.Now().UnixMilli()
|
|
sm.states[name] = s
|
|
sm.mu.Unlock()
|
|
sm.scheduleSave()
|
|
}
|
|
|
|
// UpdateSize updates only the size fields.
|
|
// Use: sm.UpdateSize("editor", 1280, 800)
|
|
func (sm *StateManager) UpdateSize(name string, width, height int) {
|
|
sm.mu.Lock()
|
|
s := sm.states[name]
|
|
s.Width = width
|
|
s.Height = height
|
|
s.UpdatedAt = time.Now().UnixMilli()
|
|
sm.states[name] = s
|
|
sm.mu.Unlock()
|
|
sm.scheduleSave()
|
|
}
|
|
|
|
// UpdateMaximized updates the maximized flag.
|
|
// Use: sm.UpdateMaximized("editor", true)
|
|
func (sm *StateManager) UpdateMaximized(name string, maximized bool) {
|
|
sm.mu.Lock()
|
|
s := sm.states[name]
|
|
s.Maximized = maximized
|
|
s.UpdatedAt = time.Now().UnixMilli()
|
|
sm.states[name] = s
|
|
sm.mu.Unlock()
|
|
sm.scheduleSave()
|
|
}
|
|
|
|
// CaptureState snapshots the current state from a PlatformWindow.
|
|
// Use: sm.CaptureState(pw)
|
|
func (sm *StateManager) CaptureState(pw PlatformWindow) {
|
|
x, y := pw.Position()
|
|
w, h := pw.Size()
|
|
sm.SetState(pw.Name(), WindowState{
|
|
X: x, Y: y, Width: w, Height: h,
|
|
Maximized: pw.IsMaximised(),
|
|
})
|
|
}
|
|
|
|
// ApplyState restores saved position/size to a Window descriptor.
|
|
// Use: sm.ApplyState(&window.Window{Name: "editor"})
|
|
func (sm *StateManager) ApplyState(w *Window) {
|
|
s, ok := sm.GetState(w.Name)
|
|
if !ok {
|
|
return
|
|
}
|
|
if s.Width > 0 {
|
|
w.Width = s.Width
|
|
}
|
|
if s.Height > 0 {
|
|
w.Height = s.Height
|
|
}
|
|
w.X = s.X
|
|
w.Y = s.Y
|
|
}
|
|
|
|
// ListStates returns all stored window names.
|
|
// Use: names := sm.ListStates()
|
|
func (sm *StateManager) ListStates() []string {
|
|
sm.mu.RLock()
|
|
defer sm.mu.RUnlock()
|
|
names := make([]string, 0, len(sm.states))
|
|
for name := range sm.states {
|
|
names = append(names, name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// Clear removes all stored states.
|
|
// Use: sm.Clear()
|
|
func (sm *StateManager) Clear() {
|
|
sm.mu.Lock()
|
|
sm.states = make(map[string]WindowState)
|
|
sm.mu.Unlock()
|
|
sm.scheduleSave()
|
|
}
|
|
|
|
// ForceSync writes state to disk immediately.
|
|
// Use: sm.ForceSync()
|
|
func (sm *StateManager) ForceSync() {
|
|
if sm.saveTimer != nil {
|
|
sm.saveTimer.Stop()
|
|
}
|
|
sm.save()
|
|
}
|