Preserve window metadata during state capture and route compound bounds updates through a single window abstraction. Co-Authored-By: Virgil <virgil@lethean.io>
247 lines
5.6 KiB
Go
247 lines
5.6 KiB
Go
// pkg/window/state.go
|
|
package window
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
coreio "forge.lthn.ai/core/go-io"
|
|
)
|
|
|
|
// WindowState holds the persisted position/size of a window.
|
|
// JSON tags match the existing window_state.json format.
|
|
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),
|
|
}
|
|
configDir, err := os.UserConfigDir()
|
|
if err == nil {
|
|
sm.configDir = filepath.Join(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 filepath.Join(sm.configDir, "window_state.json")
|
|
}
|
|
|
|
func (sm *StateManager) dataDir() string {
|
|
if sm.statePath != "" {
|
|
return filepath.Dir(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 {
|
|
return
|
|
}
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
_ = json.Unmarshal([]byte(content), &sm.states)
|
|
}
|
|
|
|
func (sm *StateManager) save() {
|
|
if sm.configDir == "" && sm.statePath == "" {
|
|
return
|
|
}
|
|
sm.mu.RLock()
|
|
data, err := json.MarshalIndent(sm.states, "", " ")
|
|
sm.mu.RUnlock()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if dir := sm.dataDir(); dir != "" {
|
|
_ = coreio.Local.EnsureDir(dir)
|
|
}
|
|
_ = coreio.Local.Write(sm.filePath(), string(data))
|
|
}
|
|
|
|
func (sm *StateManager) scheduleSave() {
|
|
sm.mu.Lock()
|
|
sm.stopSaveTimerLocked()
|
|
sm.saveTimer = time.AfterFunc(500*time.Millisecond, sm.save)
|
|
sm.mu.Unlock()
|
|
}
|
|
|
|
func (sm *StateManager) stopSaveTimerLocked() {
|
|
if sm.saveTimer != nil {
|
|
sm.saveTimer.Stop()
|
|
sm.saveTimer = nil
|
|
}
|
|
}
|
|
|
|
func (sm *StateManager) updateState(name string, mutate func(*WindowState)) {
|
|
sm.mu.Lock()
|
|
state := sm.states[name]
|
|
mutate(&state)
|
|
state.UpdatedAt = time.Now().UnixMilli()
|
|
sm.states[name] = state
|
|
sm.mu.Unlock()
|
|
sm.scheduleSave()
|
|
}
|
|
|
|
// 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) {
|
|
sm.updateState(name, func(current *WindowState) {
|
|
*current = state
|
|
})
|
|
}
|
|
|
|
// UpdatePosition updates only the position fields.
|
|
func (sm *StateManager) UpdatePosition(name string, x, y int) {
|
|
sm.updateState(name, func(state *WindowState) {
|
|
state.X = x
|
|
state.Y = y
|
|
})
|
|
}
|
|
|
|
// UpdateSize updates only the size fields.
|
|
func (sm *StateManager) UpdateSize(name string, width, height int) {
|
|
sm.updateState(name, func(state *WindowState) {
|
|
state.Width = width
|
|
state.Height = height
|
|
})
|
|
}
|
|
|
|
// UpdateBounds updates position and size in one state write.
|
|
func (sm *StateManager) UpdateBounds(name string, x, y, width, height int) {
|
|
sm.updateState(name, func(state *WindowState) {
|
|
state.X = x
|
|
state.Y = y
|
|
state.Width = width
|
|
state.Height = height
|
|
})
|
|
}
|
|
|
|
// UpdateMaximized updates the maximized flag.
|
|
func (sm *StateManager) UpdateMaximized(name string, maximized bool) {
|
|
sm.updateState(name, func(state *WindowState) {
|
|
state.Maximized = maximized
|
|
})
|
|
}
|
|
|
|
// CaptureState snapshots the current state from a PlatformWindow.
|
|
func (sm *StateManager) CaptureState(pw PlatformWindow) {
|
|
if pw == nil {
|
|
return
|
|
}
|
|
x, y := pw.Position()
|
|
w, h := pw.Size()
|
|
name := pw.Name()
|
|
sm.updateState(name, func(state *WindowState) {
|
|
state.X = x
|
|
state.Y = y
|
|
state.Width = w
|
|
state.Height = h
|
|
state.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)
|
|
}
|
|
sort.Strings(names)
|
|
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()
|
|
}
|
|
|
|
// ForceSync writes state to disk immediately.
|
|
func (sm *StateManager) ForceSync() {
|
|
sm.mu.Lock()
|
|
sm.stopSaveTimerLocked()
|
|
sm.mu.Unlock()
|
|
sm.save()
|
|
}
|