From db535805a179ef11a012ff34994036ede2cb150d Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 18 Apr 2026 08:49:04 +0100 Subject: [PATCH] Harden window state path configuration --- .core/TODO.md | 1 + pkg/window/state.go | 21 +++++++++++++++++++-- pkg/window/state_test.go | 11 +++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.core/TODO.md b/.core/TODO.md index e69de29b..42905e2f 100644 --- a/.core/TODO.md +++ b/.core/TODO.md @@ -0,0 +1 @@ +- @bug pkg/window/layout.go:29 — layout persistence has the same hard-coded config-dir assumption and needs an explicit file-path override for restricted runtimes. diff --git a/pkg/window/state.go b/pkg/window/state.go index 6a2d3662..b12880eb 100644 --- a/pkg/window/state.go +++ b/pkg/window/state.go @@ -10,6 +10,8 @@ import ( coreio "dappco.re/go/core/io" ) +const windowStateFileEnv = "WINDOW_STATE_FILE" + // WindowState holds the persisted position/size of a window. // JSON tags match existing window_state.json format for backward compat. type WindowState struct { @@ -23,7 +25,7 @@ type WindowState struct { UpdatedAt int64 `json:"updatedAt,omitempty"` } -// StateManager persists window positions to ~/.config/Core/window_state.json. +// StateManager persists window positions to the configured window state file. type StateManager struct { configDir string statePath string @@ -32,8 +34,12 @@ type StateManager struct { saveTimer *time.Timer } -// NewStateManager creates a StateManager loading from the default config directory. +// NewStateManager creates a StateManager loading from the configured default path. +// WINDOW_STATE_FILE overrides the directory-based default when present. func NewStateManager() *StateManager { + if stateFile := core.Env(windowStateFileEnv); stateFile != "" { + return NewStateManagerWithPath(stateFile) + } sm := &StateManager{ states: make(map[string]WindowState), } @@ -55,6 +61,17 @@ func NewStateManagerWithDir(configDir string) *StateManager { return sm } +// NewStateManagerWithPath creates a StateManager loading from a custom state file. +// Useful for tests or restricted runtimes that need an explicit writable target. +func NewStateManagerWithPath(path string) *StateManager { + sm := &StateManager{ + statePath: path, + states: make(map[string]WindowState), + } + sm.load() + return sm +} + func (sm *StateManager) filePath() string { if sm.statePath != "" { return sm.statePath diff --git a/pkg/window/state_test.go b/pkg/window/state_test.go index ce7b398a..34c7cfc9 100644 --- a/pkg/window/state_test.go +++ b/pkg/window/state_test.go @@ -20,6 +20,17 @@ func TestStateManagerState_NewStateManagerWithDir_Good(t *testing.T) { assert.Empty(t, sm.ListStates()) } +func TestStateManagerState_NewStateManagerWithPathEnv_Good(t *testing.T) { + path := filepath.Join(t.TempDir(), "custom", "window_state.json") + t.Setenv(windowStateFileEnv, path) + + sm := NewStateManager() + + require.NotNil(t, sm) + assert.Equal(t, path, sm.filePath()) + assert.Equal(t, filepath.Dir(path), sm.dataDir()) +} + func TestStateManagerState_NewStateManagerWithDir_Bad(t *testing.T) { sm := NewStateManagerWithDir("")