cli/pkg/display/window_state.go

261 lines
5.5 KiB
Go

package display
import (
"encoding/json"
"os"
"path/filepath"
"sync"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
// WindowState holds the persisted state of a window.
type WindowState struct {
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
Maximized bool `json:"maximized"`
Screen string `json:"screen,omitempty"` // Screen identifier for multi-monitor
URL string `json:"url,omitempty"` // Last URL/route
UpdatedAt int64 `json:"updatedAt"`
}
// WindowStateManager handles saving and restoring window positions.
type WindowStateManager struct {
states map[string]*WindowState
filePath string
mu sync.RWMutex
dirty bool
saveTimer *time.Timer
}
// NewWindowStateManager creates a new window state manager.
// It loads existing state from the config directory.
func NewWindowStateManager() *WindowStateManager {
m := &WindowStateManager{
states: make(map[string]*WindowState),
}
// Determine config path
configDir, err := os.UserConfigDir()
if err != nil {
configDir = "."
}
m.filePath = filepath.Join(configDir, "Core", "window_state.json")
// Ensure directory exists
os.MkdirAll(filepath.Dir(m.filePath), 0755)
// Load existing state
m.load()
return m
}
// load reads window states from disk.
func (m *WindowStateManager) load() error {
m.mu.Lock()
defer m.mu.Unlock()
data, err := os.ReadFile(m.filePath)
if err != nil {
if os.IsNotExist(err) {
return nil // No saved state yet
}
return err
}
return json.Unmarshal(data, &m.states)
}
// save writes window states to disk.
func (m *WindowStateManager) save() error {
m.mu.RLock()
data, err := json.MarshalIndent(m.states, "", " ")
m.mu.RUnlock()
if err != nil {
return err
}
return os.WriteFile(m.filePath, data, 0644)
}
// scheduleSave debounces saves to avoid excessive disk writes.
func (m *WindowStateManager) scheduleSave() {
m.mu.Lock()
defer m.mu.Unlock()
m.dirty = true
// Cancel existing timer
if m.saveTimer != nil {
m.saveTimer.Stop()
}
// Schedule save after 500ms of no changes
m.saveTimer = time.AfterFunc(500*time.Millisecond, func() {
m.mu.Lock()
if m.dirty {
m.dirty = false
m.mu.Unlock()
m.save()
} else {
m.mu.Unlock()
}
})
}
// GetState returns the saved state for a window, or nil if none.
func (m *WindowStateManager) GetState(name string) *WindowState {
m.mu.RLock()
defer m.mu.RUnlock()
return m.states[name]
}
// SetState saves the state for a window.
func (m *WindowStateManager) SetState(name string, state *WindowState) {
m.mu.Lock()
state.UpdatedAt = time.Now().Unix()
m.states[name] = state
m.mu.Unlock()
m.scheduleSave()
}
// UpdatePosition updates just the position of a window.
func (m *WindowStateManager) UpdatePosition(name string, x, y int) {
m.mu.Lock()
state, ok := m.states[name]
if !ok {
state = &WindowState{}
m.states[name] = state
}
state.X = x
state.Y = y
state.UpdatedAt = time.Now().Unix()
m.mu.Unlock()
m.scheduleSave()
}
// UpdateSize updates just the size of a window.
func (m *WindowStateManager) UpdateSize(name string, width, height int) {
m.mu.Lock()
state, ok := m.states[name]
if !ok {
state = &WindowState{}
m.states[name] = state
}
state.Width = width
state.Height = height
state.UpdatedAt = time.Now().Unix()
m.mu.Unlock()
m.scheduleSave()
}
// UpdateMaximized updates the maximized state of a window.
func (m *WindowStateManager) UpdateMaximized(name string, maximized bool) {
m.mu.Lock()
state, ok := m.states[name]
if !ok {
state = &WindowState{}
m.states[name] = state
}
state.Maximized = maximized
state.UpdatedAt = time.Now().Unix()
m.mu.Unlock()
m.scheduleSave()
}
// CaptureState captures the current state from a window.
func (m *WindowStateManager) CaptureState(name string, window *application.WebviewWindow) {
if window == nil {
return
}
x, y := window.Position()
width, height := window.Size()
m.mu.Lock()
state, ok := m.states[name]
if !ok {
state = &WindowState{}
m.states[name] = state
}
state.X = x
state.Y = y
state.Width = width
state.Height = height
state.Maximized = window.IsMaximised()
state.UpdatedAt = time.Now().Unix()
m.mu.Unlock()
m.scheduleSave()
}
// ApplyState applies saved state to window options.
// Returns the modified options with position/size restored.
func (m *WindowStateManager) ApplyState(opts application.WebviewWindowOptions) application.WebviewWindowOptions {
state := m.GetState(opts.Name)
if state == nil {
return opts
}
// Only apply if we have valid saved dimensions
if state.Width > 0 && state.Height > 0 {
opts.Width = state.Width
opts.Height = state.Height
}
// Apply position (check for reasonable values)
if state.X != 0 || state.Y != 0 {
opts.X = state.X
opts.Y = state.Y
}
// Apply maximized state
if state.Maximized {
opts.StartState = application.WindowStateMaximised
}
return opts
}
// ForceSync immediately saves all state to disk.
func (m *WindowStateManager) ForceSync() error {
m.mu.Lock()
if m.saveTimer != nil {
m.saveTimer.Stop()
m.saveTimer = nil
}
m.dirty = false
m.mu.Unlock()
return m.save()
}
// Clear removes all saved window states.
func (m *WindowStateManager) Clear() error {
m.mu.Lock()
m.states = make(map[string]*WindowState)
m.mu.Unlock()
return m.save()
}
// ListStates returns all saved window names.
func (m *WindowStateManager) ListStates() []string {
m.mu.RLock()
defer m.mu.RUnlock()
names := make([]string, 0, len(m.states))
for name := range m.states {
names = append(names, name)
}
return names
}