summaryrefslogtreecommitdiff
path: root/internal/keepass/fzf.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-17 08:45:13 +0300
committerPaul Buetow <paul@buetow.org>2026-04-17 08:45:13 +0300
commit9e2bf4af8b7b3b4ca2980aa6285482e7db0cd151 (patch)
tree96bcd8ad2a64e6dfc9d3bedc9d063eeef1591cf0 /internal/keepass/fzf.go
parentb53e348d89046ea8de5b82c283fb56980b53cdd8 (diff)
feat: add internal/keepass package with full Backend implementation (tasks k4+l4+m4)
Read-write KeePass backend implementing backend.Backend: - WalkIndexes flattens groups into 'Group/Title' virtual entries - LoadData returns formatted Password/User/URL/Notes for text entries or raw bytes for binary attachments - WriteBack hook enables edit round-trip through the kdbx file - Add/Import/ImportRecursive/Remove with atomic tmp+rename save - Binary attachments surface as virtual 'Group/Title/filename' entries; Add to such a path creates/replaces the attachment on the parent entry - AtomicSave exported for reuse by cli/kdbx_store.go - parseContent is the tolerant inverse of formatContent - Helpers EnsureGroup, UpsertEntryByTitle, SetEntryField, SplitDescriptionPath, SanitizeRelativePath exported so cli/kdbx_store.go avoids duplication Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/keepass/fzf.go')
-rw-r--r--internal/keepass/fzf.go94
1 files changed, 94 insertions, 0 deletions
diff --git a/internal/keepass/fzf.go b/internal/keepass/fzf.go
new file mode 100644
index 0000000..a30e769
--- /dev/null
+++ b/internal/keepass/fzf.go
@@ -0,0 +1,94 @@
+package keepass
+
+import (
+ "context"
+ "sort"
+ "strings"
+
+ "codeberg.org/snonux/foostore/internal/picker"
+ "codeberg.org/snonux/foostore/internal/store"
+)
+
+// Fzf launches fzf and returns only the selected description.
+func (b *Backend) Fzf(ctx context.Context) (string, error) {
+ result, err := b.FzfInteractive(ctx)
+ if err != nil {
+ return "", err
+ }
+ return result.Description, nil
+}
+
+// FzfInteractive launches fzf with action key bindings and returns the
+// selected description plus the chosen action.
+func (b *Backend) FzfInteractive(ctx context.Context) (store.PickerResult, error) {
+ var indexes store.IndexSlice
+ if err := b.WalkIndexes(ctx, "", func(idx *store.Index) error {
+ indexes = append(indexes, idx)
+ return nil
+ }); err != nil {
+ return store.PickerResult{}, err
+ }
+ if len(indexes) == 0 {
+ return store.PickerResult{}, nil
+ }
+
+ sort.Sort(indexes)
+ entries := buildPickerEntries(indexes)
+ return runFzfInteractive(ctx, entries)
+}
+
+// buildPickerEntries converts a sorted IndexSlice into picker.Entry rows.
+func buildPickerEntries(indexes store.IndexSlice) []picker.Entry {
+ entries := make([]picker.Entry, 0, len(indexes))
+ for i, idx := range indexes {
+ kind := "TEXT"
+ if idx.IsBinary() {
+ kind = "BINARY"
+ }
+ hashSuffix := ""
+ if len(idx.Hash) >= 63 {
+ hashSuffix = idx.Hash[53:63]
+ }
+ entries = append(entries, picker.Entry{
+ RowID: i + 1,
+ Description: idx.Description,
+ Kind: kind,
+ HashSuffix: hashSuffix,
+ })
+ }
+ return entries
+}
+
+// runFzfInteractive calls picker.Run and maps the chosen key to a PickerAction.
+func runFzfInteractive(ctx context.Context, entries []picker.Entry) (store.PickerResult, error) {
+ selection, err := picker.Run(ctx, entries)
+ if err != nil {
+ return store.PickerResult{}, err
+ }
+ action, ok := parsePickerAction(selection.Key)
+ if !ok || selection.Description == "" {
+ return store.PickerResult{}, nil
+ }
+ return store.PickerResult{
+ Description: selection.Description,
+ Action: action,
+ }, nil
+}
+
+// parsePickerAction maps an fzf key string to a PickerAction.
+func parsePickerAction(keyLine string) (store.PickerAction, bool) {
+ switch strings.TrimSpace(keyLine) {
+ case "", "enter":
+ return store.PickerSelect, true
+ case "ctrl-t", "alt-t":
+ return store.PickerCat, true
+ case "ctrl-y", "alt-y":
+ return store.PickerPaste, true
+ case "ctrl-o", "alt-o":
+ return store.PickerOpen, true
+ case "ctrl-e", "alt-e":
+ return store.PickerEdit, true
+ default:
+ return "", false
+ }
+}