From 92e8fce0e72855005f5f33d8d93e54a77009b88b Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Sun, 29 Mar 2026 15:28:20 -0700 Subject: [PATCH] Persist recent KeePassGO vault list --- main.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ main_test.go | 19 +++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/main.go b/main.go index 43801c0..6afd5f1 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "errors" "flag" "fmt" @@ -164,6 +165,7 @@ type ui struct { lifecycleMode string keyboardFocus focusID defaultSaveAsPath string + recentVaultsPath string editingEntry bool recentVaults []string } @@ -241,6 +243,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession) *ui { selectedHistoryIndex: -1, lifecycleMode: "local", defaultSaveAsPath: defaultSaveAsPath(), + recentVaultsPath: defaultRecentVaultsPath(), } u.state.Session = sess u.phoneSplit.Value = 0.46 @@ -249,6 +252,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession) *ui { u.copyIcon, _ = widget.NewIcon(icons.ContentContentCopy) u.passwordProfile.SetText("strong") u.keyboardFocus = focusSearch + u.loadRecentVaults() u.filter() return u } @@ -274,6 +278,14 @@ func defaultSaveAsPath() string { return filepath.Join(cacheDir, "keepassgo", "vault.kdbx") } +func defaultRecentVaultsPath() string { + configDir, err := os.UserConfigDir() + if err != nil || strings.TrimSpace(configDir) == "" { + configDir = os.TempDir() + } + return filepath.Join(configDir, "keepassgo", "recent-vaults.json") +} + func (u *ui) selectedAttachmentItems() []attachmentItem { item, ok := u.selectedEntry() if !ok || len(item.Attachments) == 0 { @@ -561,6 +573,52 @@ func (u *ui) noteRecentVault(path string) { if len(u.recentVaultClicks) < len(u.recentVaults) { u.recentVaultClicks = make([]widget.Clickable, len(u.recentVaults)) } + u.saveRecentVaults() +} + +func (u *ui) loadRecentVaults() { + if strings.TrimSpace(u.recentVaultsPath) == "" { + return + } + content, err := os.ReadFile(u.recentVaultsPath) + if err != nil { + return + } + var paths []string + if err := json.Unmarshal(content, &paths); err != nil { + return + } + filtered := make([]string, 0, len(paths)) + seen := map[string]bool{} + for _, path := range paths { + path = strings.TrimSpace(path) + if path == "" || seen[path] { + continue + } + seen[path] = true + filtered = append(filtered, path) + if len(filtered) == 6 { + break + } + } + u.recentVaults = filtered + if len(u.recentVaultClicks) < len(u.recentVaults) { + u.recentVaultClicks = make([]widget.Clickable, len(u.recentVaults)) + } +} + +func (u *ui) saveRecentVaults() { + if strings.TrimSpace(u.recentVaultsPath) == "" { + return + } + if err := os.MkdirAll(filepath.Dir(u.recentVaultsPath), 0o700); err != nil { + return + } + content, err := json.MarshalIndent(u.recentVaults, "", " ") + if err != nil { + return + } + _ = os.WriteFile(u.recentVaultsPath, content, 0o600) } func (u *ui) hiddenVaultRoot() string { diff --git a/main_test.go b/main_test.go index b721843..9228d35 100644 --- a/main_test.go +++ b/main_test.go @@ -1732,6 +1732,7 @@ func TestUINoteRecentVaultDeduplicatesAndOrdersMostRecentFirst(t *testing.T) { t.Parallel() u := newUIWithSession("desktop", &session.Manager{}) + u.recentVaultsPath = filepath.Join(t.TempDir(), "recent-vaults.json") u.noteRecentVault("/tmp/one.kdbx") u.noteRecentVault("/tmp/two.kdbx") u.noteRecentVault("/tmp/one.kdbx") @@ -1741,6 +1742,24 @@ func TestUINoteRecentVaultDeduplicatesAndOrdersMostRecentFirst(t *testing.T) { } } +func TestUILoadsRecentVaultsFromPersistedConfig(t *testing.T) { + t.Parallel() + + configPath := filepath.Join(t.TempDir(), "recent-vaults.json") + first := newUIWithSession("desktop", &session.Manager{}) + first.recentVaultsPath = configPath + first.noteRecentVault("/tmp/one.kdbx") + first.noteRecentVault("/tmp/two.kdbx") + + second := newUIWithSession("desktop", &session.Manager{}) + second.recentVaultsPath = configPath + second.loadRecentVaults() + + if got := second.recentVaults; !slices.Equal(got, []string{"/tmp/two.kdbx", "/tmp/one.kdbx"}) { + t.Fatalf("recentVaults after reload = %v, want [/tmp/two.kdbx /tmp/one.kdbx]", got) + } +} + func TestUICopyActionsWriteExpectedClipboardContentsAndSanitizedFeedback(t *testing.T) { t.Parallel()