diff options
Diffstat (limited to 'internal/store/store_picker.go')
| -rw-r--r-- | internal/store/store_picker.go | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/internal/store/store_picker.go b/internal/store/store_picker.go new file mode 100644 index 0000000..1e8b6b7 --- /dev/null +++ b/internal/store/store_picker.go @@ -0,0 +1,160 @@ +// store_picker.go handles fzf/picker integration for the interactive secret browser. +// It wraps the internal/picker package to provide fuzzy-selection with action key +// bindings (cat, paste, open, edit) and maps the raw fzf output back to typed +// PickerResult values consumed by the CLI layer. +package store + +import ( + "context" + "os" + "sort" + "strings" + + "codeberg.org/snonux/foostore/internal/picker" +) + +// PickerAction describes the action requested from the interactive fzf picker. +type PickerAction string + +const ( + PickerSelect PickerAction = "select" + PickerCat PickerAction = "cat" + PickerPaste PickerAction = "paste" + PickerOpen PickerAction = "open" + PickerEdit PickerAction = "edit" +) + +// PickerResult is the selected description plus the desired action from fzf. +// Description is empty when the picker was cancelled. +type PickerResult struct { + Description string + Action PickerAction +} + +// 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) { + result, err := s.FzfInteractive(ctx) + if err != nil { + return "", err + } + return result.Description, nil +} + +// FzfInteractive launches fzf with helper bars, preview metadata, and action +// key bindings, then returns both the selected description and action. +func (s *Store) FzfInteractive(ctx context.Context) (PickerResult, error) { + var indexes IndexSlice + if err := s.WalkIndexes(ctx, "", func(idx *Index) error { + indexes = append(indexes, idx) + return nil + }); err != nil { + return PickerResult{}, err + } + if len(indexes) == 0 { + return PickerResult{}, nil + } + + sort.Sort(indexes) + entries := buildPickerEntries(indexes) + + return runFzfInteractive(ctx, entries) +} + +// buildPickerEntries converts a sorted IndexSlice into picker.Entry values, +// annotating each entry with its kind (TEXT/BINARY) and a short hash suffix +// for visual disambiguation in the fzf list. +func buildPickerEntries(indexes 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 delegates to picker.Run, then maps the raw fzf selection +// (key line + description) to a typed PickerResult. An empty or cancelled +// selection returns a zero PickerResult without error. +func runFzfInteractive(ctx context.Context, entries []picker.Entry) (PickerResult, error) { + selection, err := picker.Run(ctx, entries) + if err != nil { + return PickerResult{}, err + } + + action, ok := parsePickerAction(selection.Key) + if !ok { + return PickerResult{}, nil + } + if selection.Description == "" { + return PickerResult{}, nil + } + + return PickerResult{ + Description: selection.Description, + Action: action, + }, nil +} + +// buildFzfArgs returns the fzf argument slice for the given entry count, +// reading FOOSTORE_TUI_THEME and FOOSTORE_FZF_OPTS from the environment. +// Delegates to picker.BuildArgs so that the fzf argument format stays in one place. +func buildFzfArgs(entryCount int) []string { + return picker.BuildArgs( + entryCount, + os.Getenv("FOOSTORE_TUI_THEME"), + os.Getenv("FOOSTORE_FZF_OPTS"), + ) +} + +// pickerColorTheme returns the fzf --color string for the named TUI theme. +// Delegates to picker.ColorTheme; unknown names fall back to the bold preset. +func pickerColorTheme(theme string) string { + return picker.ColorTheme(theme) +} + +// parsePickerResult decodes a raw fzf --expect output string into a PickerResult. +// idToDescription maps row-ID strings back to their human-readable descriptions. +// Returns a zero PickerResult when the key is unknown or the selection is empty. +func parsePickerResult(output string, idToDescription map[string]string) PickerResult { + selection := picker.ParseSelection(output, idToDescription) + action, ok := parsePickerAction(selection.Key) + if !ok || selection.Description == "" { + return PickerResult{} + } + return PickerResult{ + Description: selection.Description, + Action: action, + } +} + +// parsePickerAction maps an fzf key line (e.g. "ctrl-t", "enter") to a typed +// PickerAction. Returns (action, true) on a recognised key, ("", false) otherwise. +func parsePickerAction(keyLine string) (PickerAction, bool) { + switch strings.TrimSpace(keyLine) { + case "", "enter": + return PickerSelect, true + case "ctrl-t", "alt-t": + return PickerCat, true + case "ctrl-y", "alt-y": + return PickerPaste, true + case "ctrl-o", "alt-o": + return PickerOpen, true + case "ctrl-e", "alt-e": + return PickerEdit, true + default: + return "", false + } +} |
