280 lines
6.7 KiB
Go
280 lines
6.7 KiB
Go
// pkg/window/state.go
|
|
package window
|
|
|
|
import (
|
|
"io/fs"
|
|
"sync"
|
|
"time"
|
|
|
|
core "dappco.re/go/core"
|
|
coreio "dappco.re/go/core/io"
|
|
)
|
|
|
|
// WindowState holds the persisted position/size of a window.
|
|
// JSON tags match existing window_state.json format for backward compat.
|
|
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.
|
|
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.
|
|
func NewStateManager() *StateManager {
|
|
sm := &StateManager{
|
|
states: make(map[string]WindowState),
|
|
}
|
|
if configDir := core.Env("DIR_CONFIG"); configDir != "" {
|
|
sm.configDir = core.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.
|
|
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 core.JoinPath(sm.configDir, "window_state.json")
|
|
}
|
|
|
|
func (sm *StateManager) dataDir() string {
|
|
if sm.statePath != "" {
|
|
return core.PathDir(sm.statePath)
|
|
}
|
|
return sm.configDir
|
|
}
|
|
|
|
func (sm *StateManager) SetPath(path string) {
|
|
if path == "" {
|
|
return
|
|
}
|
|
sm.mu.Lock()
|
|
sm.stopSaveTimerLocked()
|
|
sm.statePath = path
|
|
sm.states = make(map[string]WindowState)
|
|
sm.mu.Unlock()
|
|
sm.load()
|
|
}
|
|
|
|
func (sm *StateManager) load() {
|
|
if sm.configDir == "" && sm.statePath == "" {
|
|
return
|
|
}
|
|
content, err := coreio.Local.Read(sm.filePath())
|
|
if err != nil {
|
|
if core.Is(err, fs.ErrNotExist) {
|
|
return
|
|
}
|
|
core.Error(
|
|
"window state load failed",
|
|
"path", sm.filePath(),
|
|
"err", core.E("window.StateManager.load", "failed to read window state", err),
|
|
)
|
|
return
|
|
}
|
|
loaded := make(map[string]WindowState)
|
|
result := core.JSONUnmarshalString(content, &loaded)
|
|
if !result.OK {
|
|
if decodeErr, ok := result.Value.(error); ok {
|
|
core.Error(
|
|
"window state load failed",
|
|
"path", sm.filePath(),
|
|
"err", core.E("window.StateManager.load", "failed to decode window state", decodeErr),
|
|
)
|
|
}
|
|
return
|
|
}
|
|
sm.mu.Lock()
|
|
sm.states = loaded
|
|
sm.mu.Unlock()
|
|
}
|
|
|
|
func (sm *StateManager) save() error {
|
|
if sm.configDir == "" && sm.statePath == "" {
|
|
return nil
|
|
}
|
|
sm.mu.RLock()
|
|
filePath := sm.filePath()
|
|
states := make(map[string]WindowState, len(sm.states))
|
|
for name, state := range sm.states {
|
|
states[name] = state
|
|
}
|
|
sm.mu.RUnlock()
|
|
result := core.JSONMarshal(states)
|
|
if !result.OK {
|
|
marshalErr, _ := result.Value.(error)
|
|
core.Error(
|
|
"window state save failed",
|
|
"path", filePath,
|
|
"err", core.E("window.StateManager.save", "failed to encode window state", marshalErr),
|
|
)
|
|
return core.E("window.StateManager.save", "failed to encode window state", marshalErr)
|
|
}
|
|
data := result.Value.([]byte)
|
|
if dir := core.PathDir(filePath); dir != "" {
|
|
if err := coreio.Local.EnsureDir(dir); err != nil {
|
|
core.Error(
|
|
"window state save failed",
|
|
"path", filePath,
|
|
"err", core.E("window.StateManager.save", "failed to create window state directory", err),
|
|
)
|
|
return core.E("window.StateManager.save", "failed to create window state directory", err)
|
|
}
|
|
}
|
|
if err := coreio.Local.Write(filePath, string(data)); err != nil {
|
|
core.Error(
|
|
"window state save failed",
|
|
"path", filePath,
|
|
"err", core.E("window.StateManager.save", "failed to write window state", err),
|
|
)
|
|
return core.E("window.StateManager.save", "failed to write window state", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sm *StateManager) scheduleSave() {
|
|
sm.mu.Lock()
|
|
sm.stopSaveTimerLocked()
|
|
sm.saveTimer = time.AfterFunc(500*time.Millisecond, func() {
|
|
_ = sm.save()
|
|
})
|
|
sm.mu.Unlock()
|
|
}
|
|
|
|
// GetState returns the saved state for a window name.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
func (sm *StateManager) Clear() {
|
|
sm.mu.Lock()
|
|
sm.states = make(map[string]WindowState)
|
|
sm.mu.Unlock()
|
|
sm.scheduleSave()
|
|
}
|
|
|
|
func (sm *StateManager) stopSaveTimerLocked() {
|
|
if sm.saveTimer == nil {
|
|
return
|
|
}
|
|
sm.saveTimer.Stop()
|
|
sm.saveTimer = nil
|
|
}
|
|
|
|
// ForceSync writes state to disk immediately.
|
|
func (sm *StateManager) ForceSync() error {
|
|
sm.mu.Lock()
|
|
sm.stopSaveTimerLocked()
|
|
sm.mu.Unlock()
|
|
return sm.save()
|
|
}
|