summaryrefslogtreecommitdiff
path: root/internal/store/store.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-02 11:03:15 +0200
committerPaul Buetow <paul@buetow.org>2026-03-02 11:03:15 +0200
commit630aa260cf92a3fb2f36952bed3be449ea14468c (patch)
tree60c83e3ca5c088d58d2ed4f6849794fb5432ebbc /internal/store/store.go
parentd7a20676e0f6f243b9ad15ccf2bc82ec641bc97b (diff)
picker: extract fzf UI logic from store package (task 400)
Diffstat (limited to 'internal/store/store.go')
-rw-r--r--internal/store/store.go143
1 files changed, 30 insertions, 113 deletions
diff --git a/internal/store/store.go b/internal/store/store.go
index e1fb4e8..527ece8 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -6,7 +6,6 @@ package store
import (
"bufio"
- "bytes"
"context"
"crypto/sha256"
"encoding/hex"
@@ -17,10 +16,10 @@ import (
"path/filepath"
"regexp"
"sort"
- "strconv"
"strings"
"codeberg.org/snonux/foostore/internal/config"
+ "codeberg.org/snonux/foostore/internal/picker"
)
// Action describes what to do with each matching secret during a Search call.
@@ -244,13 +243,6 @@ func (s *Store) actionExport(ctx context.Context, idx *Index, fullPath bool) err
return d.Export(ctx, s.cfg.ExportDir, destFile)
}
-type pickerEntry struct {
- rowID int
- description string
- kind string
- hashSuffix string
-}
-
// Fzf launches fzf and returns only the selected description for compatibility
// with callers that do not care about picker action keys.
func (s *Store) Fzf(ctx context.Context) (string, error) {
@@ -276,7 +268,7 @@ func (s *Store) FzfInteractive(ctx context.Context) (PickerResult, error) {
}
sort.Sort(indexes)
- entries := make([]pickerEntry, 0, len(indexes))
+ entries := make([]picker.Entry, 0, len(indexes))
for i, idx := range indexes {
kind := "TEXT"
if idx.IsBinary() {
@@ -286,132 +278,57 @@ func (s *Store) FzfInteractive(ctx context.Context) (PickerResult, error) {
if len(idx.Hash) >= 63 {
hashSuffix = idx.Hash[53:63]
}
- entries = append(entries, pickerEntry{
- rowID: i + 1,
- description: idx.Description,
- kind: kind,
- hashSuffix: hashSuffix,
+ entries = append(entries, picker.Entry{
+ RowID: i + 1,
+ Description: idx.Description,
+ Kind: kind,
+ HashSuffix: hashSuffix,
})
}
return runFzfInteractive(ctx, entries)
}
-func runFzfInteractive(ctx context.Context, entries []pickerEntry) (PickerResult, error) {
- if len(entries) == 0 {
- return PickerResult{}, nil
- }
- if _, err := exec.LookPath("fzf"); err != nil {
- return PickerResult{}, fmt.Errorf("fzf not found in PATH")
+func runFzfInteractive(ctx context.Context, entries []picker.Entry) (PickerResult, error) {
+ selection, err := picker.Run(ctx, entries)
+ if err != nil {
+ return PickerResult{}, err
}
- input, idToDescription := buildFzfInput(entries)
-
- cmd := exec.CommandContext(ctx, "fzf", buildFzfArgs(len(entries))...)
- cmd.Stdin = strings.NewReader(input)
- var out bytes.Buffer
- cmd.Stdout = &out
- cmd.Stderr = os.Stderr
-
- if err := cmd.Run(); err != nil {
- // Any non-zero exit from fzf (e.g., Escape/no match) is treated as cancel.
+ action, ok := parsePickerAction(selection.Key)
+ if !ok {
+ return PickerResult{}, nil
+ }
+ if selection.Description == "" {
return PickerResult{}, nil
}
- return parsePickerResult(out.String(), idToDescription), nil
-}
-
-func buildFzfInput(entries []pickerEntry) (string, map[string]string) {
- var b strings.Builder
- idToDescription := make(map[string]string, len(entries))
- for _, e := range entries {
- id := strconv.Itoa(e.rowID)
- idToDescription[id] = e.description
- fmt.Fprintf(
- &b,
- "%s\t%s\t%s\t%s\n",
- id,
- sanitizePickerField(e.description),
- sanitizePickerField(e.kind),
- sanitizePickerField(e.hashSuffix),
- )
- }
- return b.String(), idToDescription
+ return PickerResult{
+ Description: selection.Description,
+ Action: action,
+ }, nil
}
func buildFzfArgs(entryCount int) []string {
- header := "enter select | ctrl-t/alt-t cat | ctrl-y/alt-y paste | ctrl-o/alt-o open | ctrl-e/alt-e edit | esc cancel"
- status := fmt.Sprintf("foostore interactive picker | %d entries | metadata preview only", entryCount)
- args := []string{
- "--height=80%",
- "--layout=reverse",
- "--border",
- "--ansi",
- "--delimiter=\t",
- "--with-nth=2,3,4",
- "--prompt=secret> ",
- "--expect=enter,ctrl-t,ctrl-y,ctrl-o,ctrl-e,alt-t,alt-y,alt-o,alt-e",
- "--bind=ctrl-t:ignore,ctrl-y:ignore,ctrl-o:ignore,ctrl-e:ignore,alt-t:ignore,alt-y:ignore,alt-o:ignore,alt-e:ignore",
- "--header=" + header + "\n" + status,
- "--preview-window=down,6,wrap,border-top",
- "--preview=printf 'entry: %s\\nkind: %s\\nhash suffix: %s\\n' {2} {3} {4}",
- "--color=" + pickerColorTheme(os.Getenv("FOOSTORE_TUI_THEME")),
- }
- if extra := strings.TrimSpace(os.Getenv("FOOSTORE_FZF_OPTS")); extra != "" {
- args = append(args, strings.Fields(extra)...)
- }
- return args
+ return picker.BuildArgs(
+ entryCount,
+ os.Getenv("FOOSTORE_TUI_THEME"),
+ os.Getenv("FOOSTORE_FZF_OPTS"),
+ )
}
func pickerColorTheme(theme string) string {
- switch strings.ToLower(strings.TrimSpace(theme)) {
- case "", "bold":
- return "fg:#f8fafc,bg:#0b1220,hl:#f59e0b,fg+:#ffffff,bg+:#1d4ed8,hl+:#fde047,info:#22d3ee,prompt:#f43f5e,pointer:#10b981,marker:#a78bfa,spinner:#fb7185,header:#38bdf8,border:#334155,separator:#0ea5e9,query:#e2e8f0,label:#f472b6"
- case "clean":
- return "fg:#e5e7eb,bg:#111827,hl:#93c5fd,fg+:#f9fafb,bg+:#1f2937,hl+:#93c5fd,info:#a7f3d0,prompt:#fbbf24,pointer:#34d399,marker:#34d399,spinner:#fbbf24,header:#a7f3d0,border:#374151"
- case "neon":
- return "fg:#d1fae5,bg:#020617,hl:#f0abfc,fg+:#ffffff,bg+:#0f172a,hl+:#f9a8d4,info:#67e8f9,prompt:#22d3ee,pointer:#22c55e,marker:#f472b6,spinner:#a78bfa,header:#38bdf8,border:#1d4ed8,separator:#22d3ee,query:#bbf7d0,label:#f0abfc"
- case "mono":
- return "fg:#e5e5e5,bg:#111111,hl:#ffffff,fg+:#ffffff,bg+:#222222,hl+:#ffffff,info:#d4d4d4,prompt:#ffffff,pointer:#ffffff,marker:#ffffff,spinner:#ffffff,header:#d4d4d4,border:#444444"
- default:
- return pickerColorTheme("bold")
- }
-}
-
-func sanitizePickerField(s string) string {
- s = strings.ReplaceAll(s, "\t", " ")
- s = strings.ReplaceAll(s, "\n", " ")
- return strings.TrimSpace(s)
+ return picker.ColorTheme(theme)
}
func parsePickerResult(output string, idToDescription map[string]string) PickerResult {
- lines := strings.Split(strings.TrimRight(output, "\n"), "\n")
- if len(lines) < 2 {
+ selection := picker.ParseSelection(output, idToDescription)
+ action, ok := parsePickerAction(selection.Key)
+ if !ok || selection.Description == "" {
return PickerResult{}
}
-
- action, ok := parsePickerAction(lines[0])
- if !ok {
- return PickerResult{}
- }
-
- row := strings.TrimSpace(lines[1])
- if row == "" {
- return PickerResult{}
- }
-
- id := row
- if parts := strings.SplitN(row, "\t", 2); len(parts) > 0 {
- id = strings.TrimSpace(parts[0])
- }
-
- description, ok := idToDescription[id]
- if !ok || description == "" {
- return PickerResult{}
- }
-
return PickerResult{
- Description: description,
+ Description: selection.Description,
Action: action,
}
}