From fe3fa854bbf42163bb676c7c52b350f253030544 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Sun, 29 Mar 2026 15:41:44 -0700 Subject: [PATCH] Support KeePassGO state dir via flag and env --- main.go | 68 ++++++++++++++++++++++++++++++++++------------------ main_test.go | 28 ++++++++++++++++++++++ 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/main.go b/main.go index 088eb4b..9959034 100644 --- a/main.go +++ b/main.go @@ -71,6 +71,11 @@ type attachmentItem struct { Size int } +type statePaths struct { + DefaultSaveAsPath string + RecentVaultsPath string +} + type ui struct { mode string theme *material.Theme @@ -184,19 +189,23 @@ const ( errSaveAsPathRequired = "save-as path is required" ) -func newUI(mode string) *ui { - return newUIWithSession(mode, &session.Manager{}) +func newUI(mode string, paths statePaths) *ui { + return newUIWithSession(mode, &session.Manager{}, paths) } func newUIWithModel(mode string, model vault.Model) *ui { - return newUIWithState(mode, &uiSession{model: model}) + return newUIWithState(mode, &uiSession{model: model}, defaultStatePaths("")) } -func newUIWithSession(mode string, sess appstate.CurrentSession) *ui { - return newUIWithState(mode, sess) +func newUIWithSession(mode string, sess appstate.CurrentSession, paths ...statePaths) *ui { + selected := defaultStatePaths("") + if len(paths) > 0 { + selected = paths[0] + } + return newUIWithState(mode, sess, selected) } -func newUIWithState(mode string, sess appstate.CurrentSession) *ui { +func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) *ui { th := material.NewTheme() th.Palette.Bg = bgColor th.Palette.Fg = color.NRGBA{R: 31, G: 29, B: 27, A: 255} @@ -242,8 +251,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession) *ui { state: appstate.State{}, selectedHistoryIndex: -1, lifecycleMode: "local", - defaultSaveAsPath: defaultSaveAsPath(), - recentVaultsPath: defaultRecentVaultsPath(), + defaultSaveAsPath: paths.DefaultSaveAsPath, + recentVaultsPath: paths.RecentVaultsPath, } u.state.Session = sess u.phoneSplit.Value = 0.46 @@ -270,20 +279,29 @@ func (u *ui) filter() { } } -func defaultSaveAsPath() string { - cacheDir, err := os.UserCacheDir() - if err != nil || strings.TrimSpace(cacheDir) == "" { - cacheDir = os.TempDir() +func defaultStatePaths(stateDir string) statePaths { + baseDir := strings.TrimSpace(stateDir) + if baseDir == "" { + configDir, err := os.UserConfigDir() + if err != nil || strings.TrimSpace(configDir) == "" { + configDir = os.TempDir() + } + baseDir = filepath.Join(configDir, "keepassgo") + } + return statePaths{ + DefaultSaveAsPath: filepath.Join(baseDir, "vault.kdbx"), + RecentVaultsPath: filepath.Join(baseDir, "recent-vaults.json"), } - return filepath.Join(cacheDir, "keepassgo", "vault.kdbx") } -func defaultRecentVaultsPath() string { - configDir, err := os.UserConfigDir() - if err != nil || strings.TrimSpace(configDir) == "" { - configDir = os.TempDir() +func resolveFlagOrEnv(flagValue, envName, fallback string) string { + if value := strings.TrimSpace(flagValue); value != "" { + return value } - return filepath.Join(configDir, "keepassgo", "recent-vaults.json") + if value := strings.TrimSpace(os.Getenv(envName)); value != "" { + return value + } + return fallback } func (u *ui) selectedAttachmentItems() []attachmentItem { @@ -1858,12 +1876,16 @@ func fill(c color.NRGBA) layout.Widget { } func main() { - mode := flag.String("mode", "desktop", "window mode: desktop or phone") + mode := flag.String("mode", "", "window mode: desktop or phone") + stateDir := flag.String("state-dir", "", "directory for KeePassGO state such as recent-vault history and default save targets") flag.Parse() + resolvedMode := resolveFlagOrEnv(*mode, "KEEPASSGO_MODE", "desktop") + resolvedStateDir := resolveFlagOrEnv(*stateDir, "KEEPASSGO_STATE_DIR", "") + width := unit.Dp(1180) height := unit.Dp(760) - if strings.EqualFold(*mode, "phone") { + if strings.EqualFold(resolvedMode, "phone") { // Pixel 10 uses a 20:9 display; use a 412x915 dp viewport as a desktop-friendly preview. width = unit.Dp(412) height = unit.Dp(915) @@ -1875,7 +1897,7 @@ func main() { app.Title(productName), app.Size(width, height), ) - if err := run(w, strings.ToLower(*mode)); err != nil { + if err := run(w, strings.ToLower(resolvedMode), defaultStatePaths(resolvedStateDir)); err != nil { panic(err) } os.Exit(0) @@ -1883,9 +1905,9 @@ func main() { app.Main() } -func run(w *app.Window, mode string) error { +func run(w *app.Window, mode string, paths statePaths) error { var ops op.Ops - ui := newUI(mode) + ui := newUI(mode, paths) for { e := w.Event() switch e := e.(type) { diff --git a/main_test.go b/main_test.go index 82651f7..15771d9 100644 --- a/main_test.go +++ b/main_test.go @@ -1798,6 +1798,34 @@ func TestUILoadsRecentVaultsFromPersistedConfig(t *testing.T) { } } +func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) { + t.Parallel() + + base := filepath.Join(t.TempDir(), "keepassgo-state") + paths := defaultStatePaths(base) + + if got := paths.DefaultSaveAsPath; got != filepath.Join(base, "vault.kdbx") { + t.Fatalf("DefaultSaveAsPath = %q, want %q", got, filepath.Join(base, "vault.kdbx")) + } + if got := paths.RecentVaultsPath; got != filepath.Join(base, "recent-vaults.json") { + t.Fatalf("RecentVaultsPath = %q, want %q", got, filepath.Join(base, "recent-vaults.json")) + } +} + +func TestResolveFlagOrEnvPrefersFlagThenEnvThenFallback(t *testing.T) { + t.Setenv("KEEPASSGO_TEST_VALUE", "from-env") + + if got := resolveFlagOrEnv("from-flag", "KEEPASSGO_TEST_VALUE", "fallback"); got != "from-flag" { + t.Fatalf("resolveFlagOrEnv(flag) = %q, want %q", got, "from-flag") + } + if got := resolveFlagOrEnv("", "KEEPASSGO_TEST_VALUE", "fallback"); got != "from-env" { + t.Fatalf("resolveFlagOrEnv(env) = %q, want %q", got, "from-env") + } + if got := resolveFlagOrEnv("", "KEEPASSGO_TEST_MISSING", "fallback"); got != "fallback" { + t.Fatalf("resolveFlagOrEnv(fallback) = %q, want %q", got, "fallback") + } +} + func TestEnterOnLocalLifecycleScreenDefaultsToOpenVault(t *testing.T) { t.Parallel()