gui/pkg/window/state.go
Snider 62ec735c10
Some checks failed
Security Scan / security (push) Has been cancelled
Test / test (push) Has been cancelled
refactor: AX compliance sweep — replace banned stdlib imports with core primitives
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>
2026-04-13 09:32:01 +01:00

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()
}