summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-19 16:31:25 +0300
committerPaul Buetow <paul@buetow.org>2026-04-19 16:31:25 +0300
commit5e259cafc87c96bbe6ab6c4a498099c1ffef0fac (patch)
tree87eac0697f0f93a4d2f2f9faf62b1f08ae10f62e
parent7292a5db4e96ffeb30697ce3308d47777bf7c2f5 (diff)
feat: replace fish integration files with built-in fish subcommand (task u6)
Add `foostore fish` subcommand that prints the complete fish shell integration script (tab completions for foostore + the ge wrapper function) to stdout so users can source it with `foostore fish | source`. Remove install-fish.sh, FISH_INTEGRATION.md, and completions/ directory. Update CLAUDE.md and README.md to document the new workflow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--CLAUDE.md6
-rw-r--r--FISH_INTEGRATION.md89
-rw-r--r--README.md11
-rw-r--r--completions/foostore.fish33
-rw-r--r--completions/ge.fish53
-rwxr-xr-xinstall-fish.sh23
-rw-r--r--internal/cli/cli.go4
-rw-r--r--internal/cli/cli_dispatch.go4
-rw-r--r--internal/cli/cli_fish.go156
9 files changed, 172 insertions, 207 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 6c106b8..4ed756b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -32,7 +32,11 @@ Table-driven unit tests exist for all internal packages.
## Fish shell integration
```bash
-./install-fish.sh # installs completions/foostore.fish and completions/ge.fish
+# Print the fish integration script and source it in the current session:
+foostore fish | source
+
+# Or add to ~/.config/fish/config.fish for permanent activation:
+foostore fish | source # add this line to the file
```
## Configuration
diff --git a/FISH_INTEGRATION.md b/FISH_INTEGRATION.md
deleted file mode 100644
index c1e7aee..0000000
--- a/FISH_INTEGRATION.md
+++ /dev/null
@@ -1,89 +0,0 @@
-# Fish Shell Integration for foostore
-
-## Installation
-
-### Automatic Installation
-
-```bash
-cd /home/paul/git/foostore
-./install-fish.sh
-```
-
-### Manual Installation
-
-1. Copy the completion file for `foostore`:
-```bash
-cp completions/foostore.fish ~/.config/fish/completions/
-```
-
-2. Copy the wrapper function for `ge`:
-```bash
-cp completions/ge.fish ~/.config/fish/functions/
-```
-
-3. Reload fish shell:
-```bash
-exec fish
-```
-
-## Usage
-
-### `foostore` command
-
-The `foostore` command now has full tab completion:
-- Tab complete all subcommands (ls, search, cat, paste, etc.)
-- Tab complete file paths for `import`
-- Tab complete the `force` flag for import
-
-### `ge` wrapper
-
-The `ge` wrapper provides shortcuts:
-
-```bash
-# Interactive mode (no arguments)
-ge
-
-# Search shortcut (if not a known command, treats as search)
-ge mypassword
-# Same as: foostore search mypassword
-
-# Explicit commands still work
-ge cat mypassword
-ge import file.txt backup/
-ge import file.txt backup/ force
-```
-
-In interactive mode, empty `Enter` opens the fuzzy picker with direct action keys:
-- `Enter` select
-- `Ctrl-T` cat
-- `Ctrl-Y` paste
-- `Ctrl-O` open
-- `Ctrl-E` edit
-
-The picker preview remains metadata-only for safety (no decrypted secret preview).
-
-### Dynamic Entry Completion
-
-For better security, entry completion only works when the `PIN` environment variable is set:
-
-```bash
-# Set PIN for session (entries will autocomplete)
-set -x PIN yourpin
-
-# Use foostore with autocomplete
-ge <TAB>
-
-# Unset PIN when done
-set -e PIN
-```
-
-Without `PIN` set, commands will still autocomplete, but entry names won't (to avoid prompting for PIN during tab completion).
-
-## Features
-
-- ✓ Dynamic command completion (fetched from `foostore commands`)
-- ✓ Smart search fallback in `ge` wrapper
-- ✓ Entry name completion (when PIN is set)
-- ✓ File path completion for import/export
-- ✓ Force flag completion
-- ✓ No hardcoded command lists (stays in sync with foostore updates)
diff --git a/README.md b/README.md
index 501018a..2fa5de1 100644
--- a/README.md
+++ b/README.md
@@ -26,12 +26,11 @@ Tab completion and a `ge` shortcut wrapper are provided for the [fish shell](htt
### Install
-```bash
-./install-fish.sh
-exec fish
-```
+Source the integration script once to activate it for the current session, or add the line to `~/.config/fish/config.fish` for permanent activation:
-This copies `completions/foostore.fish` to `~/.config/fish/completions/` and `completions/ge.fish` to `~/.config/fish/functions/`.
+```fish
+foostore fish | source
+```
### Usage
@@ -58,8 +57,6 @@ ge <TAB>
set -e PIN # clear when done
```
-See `FISH_INTEGRATION.md` for more details.
-
## Interactive Picker UX
In interactive shell mode (`foostore` with no arguments), pressing `Enter` on an empty line opens an enhanced fuzzy picker.
diff --git a/completions/foostore.fish b/completions/foostore.fish
deleted file mode 100644
index 88056f1..0000000
--- a/completions/foostore.fish
+++ /dev/null
@@ -1,33 +0,0 @@
-# Fish completion for foostore
-# Install to ~/.config/fish/completions/foostore.fish
-
-# Dynamically load commands from foostore
-function __fish_foostore_commands
- foostore commands 2>/dev/null
-end
-
-# Get list of entries for completion
-function __fish_foostore_entries
- # Only run if PIN is set to avoid interactive prompt
- if set -q PIN
- foostore ls 2>/dev/null | string replace -r ';.*$' '' | string trim
- end
-end
-
-# Complete subcommands
-complete -c foostore -f -n "__fish_use_subcommand" -a "(__fish_foostore_commands)"
-
-# Complete search terms for commands that need them
-complete -c foostore -f -n "__fish_seen_subcommand_from search cat paste export pathexport open edit rm" -a "(__fish_foostore_entries)"
-
-# Complete file paths for import
-complete -c foostore -n "__fish_seen_subcommand_from import" -F
-
-# Complete directory paths for import destination
-complete -c foostore -n "__fish_seen_subcommand_from import; and __fish_is_nth_token 3" -F -a "(__fish_complete_directories)"
-
-# Force flag for import
-complete -c foostore -n "__fish_seen_subcommand_from import; and __fish_is_nth_token 4" -f -a "force"
-
-# Complete directory paths for import_r
-complete -c foostore -n "__fish_seen_subcommand_from import_r" -F -a "(__fish_complete_directories)"
diff --git a/completions/ge.fish b/completions/ge.fish
deleted file mode 100644
index ae723b1..0000000
--- a/completions/ge.fish
+++ /dev/null
@@ -1,53 +0,0 @@
-# Fish wrapper and completion for ge (foostore shortcut)
-# Install to ~/.config/fish/functions/ge.fish
-
-function ge --description 'foostore wrapper with shortcuts'
- # If no arguments, run interactive mode
- if test (count $argv) -eq 0
- foostore shell
- return $status
- end
-
- set -l cmd $argv[1]
-
- # Check if first argument is a known command
- if contains $cmd (foostore commands 2>/dev/null)
- # It's a command, pass through to foostore
- foostore $argv
- else
- # Not a command, treat as search term
- foostore search $argv
- end
-end
-
-# Dynamically load commands from foostore
-function __fish_ge_commands
- foostore commands 2>/dev/null
-end
-
-# Get list of entries for completion
-function __fish_ge_entries
- # Only run if PIN is set to avoid interactive prompt
- if set -q PIN
- foostore ls 2>/dev/null | string replace -r ';.*$' '' | string trim
- end
-end
-
-# Complete subcommands or search terms
-complete -c ge -f -n "__fish_use_subcommand" -a "(__fish_ge_commands)"
-complete -c ge -f -n "__fish_use_subcommand" -a "(__fish_ge_entries)"
-
-# Complete search terms for commands that need them
-complete -c ge -f -n "__fish_seen_subcommand_from search cat paste export pathexport open edit rm" -a "(__fish_ge_entries)"
-
-# Complete file paths for import
-complete -c ge -n "__fish_seen_subcommand_from import" -F
-
-# Complete directory paths for import destination
-complete -c ge -n "__fish_seen_subcommand_from import; and __fish_is_nth_token 3" -F -a "(__fish_complete_directories)"
-
-# Force flag for import
-complete -c ge -n "__fish_seen_subcommand_from import; and __fish_is_nth_token 4" -f -a "force"
-
-# Complete directory paths for import_r
-complete -c ge -n "__fish_seen_subcommand_from import_r" -F -a "(__fish_complete_directories)"
diff --git a/install-fish.sh b/install-fish.sh
deleted file mode 100755
index 5ca8901..0000000
--- a/install-fish.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-echo "Installing foostore fish shell integration..."
-
-# Create directories if they don't exist
-mkdir -p ~/.config/fish/completions
-mkdir -p ~/.config/fish/functions
-
-# Copy completion files
-echo "Installing foostore completion..."
-cp completions/foostore.fish ~/.config/fish/completions/
-
-echo "Installing ge wrapper function..."
-cp completions/ge.fish ~/.config/fish/functions/
-
-echo ""
-echo "✓ Fish integration installed successfully!"
-echo ""
-echo "Reload your fish shell with: exec fish"
-echo ""
-echo "See FISH_INTEGRATION.md for usage instructions."
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 674d120..332d88f 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -9,6 +9,7 @@
// - cli_backend.go — backend factory (buildBackend, buildGeheimBackend, buildKeepassBackend, ...)
// - cli_dispatch.go — shell loop (shellLoop) and command dispatcher (dispatch, dispatchSimple, dispatchSearch)
// - cli_commands.go — concrete command handlers (cmdAdd, cmdImport, …) and action-function factories
+// - cli_fish.go — fish subcommand: generates fish shell integration script (completions + ge wrapper)
// - cli_paths.go — shared path utilities (readPasswordFile, resolveHomeDir, expandHome)
// - migrate_kdbx.go — thin CLI handler for migrate-kdbx; delegates logic to internal/migrate
package cli
@@ -33,7 +34,7 @@ import (
var CommandList = []string{
"ls", "search", "cat", "paste", "get", "add", "export", "pathexport",
"open", "edit", "import", "import_r", "rm", "sync", "status", "commit",
- "reset", "fullcommit", "shred", "migrate-kdbx", "version", "commands", "help", "shell",
+ "reset", "fullcommit", "shred", "migrate-kdbx", "fish", "version", "commands", "help", "shell",
"exit", "last",
}
@@ -215,6 +216,7 @@ rm SEARCHTERM
sync|status|commit|reset|fullcommit
shred
migrate-kdbx [--db PATH] [--pass-file PATH] [--binary-out PATH] [--dry-run]
+fish
version
commands
help
diff --git a/internal/cli/cli_dispatch.go b/internal/cli/cli_dispatch.go
index ef23cd5..d7f73f3 100644
--- a/internal/cli/cli_dispatch.go
+++ b/internal/cli/cli_dispatch.go
@@ -191,6 +191,10 @@ func (c *CLI) dispatchSimple(ctx context.Context, argv []string, cmd string) (in
fmt.Println(name)
}
return 0, "", true
+ case "fish":
+ // Print the fish shell integration script to stdout so users can source it:
+ // foostore fish | source
+ return c.cmdFish(os.Stdout), "", true
case "help":
printHelp()
return 0, "", true
diff --git a/internal/cli/cli_fish.go b/internal/cli/cli_fish.go
new file mode 100644
index 0000000..b2d3616
--- /dev/null
+++ b/internal/cli/cli_fish.go
@@ -0,0 +1,156 @@
+// Package cli — fish shell integration subcommand.
+//
+// This file implements the "fish" subcommand, which prints the complete fish
+// shell integration script (tab completion for foostore + the ge wrapper
+// function) to stdout so users can source it directly:
+//
+// foostore fish | source
+//
+// The script is generated at runtime using the resolved binary path so that
+// completion helpers call the correct executable even when foostore is installed
+// under a non-default name or path.
+package cli
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// cmdFish prints the fish shell integration script to stdout and returns an
+// exit code. The binary path is resolved from os.Executable() so that the
+// generated script calls the correct foostore binary regardless of where it is
+// installed.
+func (c *CLI) cmdFish(stdout io.Writer) int {
+ binaryPath, err := os.Executable()
+ if err != nil {
+ binaryPath = "foostore"
+ }
+ script := fishIntegrationScript(binaryPath)
+ if _, err := io.WriteString(stdout, script); err != nil {
+ return 1
+ }
+ return 0
+}
+
+// fishIntegrationScript returns the complete fish shell integration script for
+// the given binary path. The script combines the foostore completion rules and
+// the ge wrapper function so users need only source a single output:
+//
+// foostore fish | source
+//
+// The binary name is extracted from binaryPath (basename without path) and used
+// wherever "foostore" appears in complete directives, making the script
+// correct even when the binary is renamed.
+func fishIntegrationScript(binaryPath string) string {
+ bin := filepath.Base(binaryPath)
+ var b strings.Builder
+
+ writeFishHeader(&b, bin)
+ writeFishCompletionFunctions(&b, bin, binaryPath)
+ writeFishCompleteDirectives(&b, bin)
+ writeFishGeFunction(&b, binaryPath)
+ writeFishGeCompleteDirectives(&b)
+
+ return b.String()
+}
+
+// writeFishHeader writes the preamble comment explaining how to source the script.
+func writeFishHeader(b *strings.Builder, bin string) {
+ fmt.Fprintf(b, "# Fish shell integration for %s\n", bin)
+ fmt.Fprintf(b, "# Source with: %s fish | source\n", bin)
+ fmt.Fprintf(b, "# Or add to ~/.config/fish/config.fish:\n")
+ fmt.Fprintf(b, "# %s fish | source\n\n", bin)
+}
+
+// writeFishCompletionFunctions writes the helper functions used by complete directives
+// for dynamic command and entry completion.
+func writeFishCompletionFunctions(b *strings.Builder, bin, binaryPath string) {
+ // Dynamic command list helper — calls foostore commands at completion time.
+ fmt.Fprintf(b, "# Dynamically load commands from %s\n", bin)
+ fmt.Fprintf(b, "function __fish_%s_commands\n", bin)
+ fmt.Fprintf(b, " %s commands 2>/dev/null\n", binaryPath)
+ fmt.Fprintf(b, "end\n\n")
+
+ // Entry list helper — only runs when $PIN is set to avoid interactive prompts.
+ fmt.Fprintf(b, "# Get list of entries for completion (only when PIN is set)\n")
+ fmt.Fprintf(b, "function __fish_%s_entries\n", bin)
+ fmt.Fprintf(b, " if set -q PIN\n")
+ fmt.Fprintf(b, " %s ls 2>/dev/null | string replace -r ';.*$' '' | string trim\n", binaryPath)
+ fmt.Fprintf(b, " end\n")
+ fmt.Fprintf(b, "end\n\n")
+}
+
+// writeFishCompleteDirectives writes the complete directives for the foostore command.
+func writeFishCompleteDirectives(b *strings.Builder, bin string) {
+ fmt.Fprintf(b, "# Complete subcommands for %s\n", bin)
+ fmt.Fprintf(b, "complete -c %s -f -n '__fish_use_subcommand' -a '(__fish_%s_commands)'\n\n", bin, bin)
+
+ fmt.Fprintf(b, "# Complete entry names for commands that accept a search term\n")
+ fmt.Fprintf(b, "complete -c %s -f -n '__fish_seen_subcommand_from search cat paste export pathexport open edit rm' -a '(__fish_%s_entries)'\n\n", bin, bin)
+
+ fmt.Fprintf(b, "# Complete file paths for import\n")
+ fmt.Fprintf(b, "complete -c %s -n '__fish_seen_subcommand_from import' -F\n\n", bin)
+
+ fmt.Fprintf(b, "# Complete directory paths for import destination (third token)\n")
+ fmt.Fprintf(b, "complete -c %s -n '__fish_seen_subcommand_from import; and __fish_is_nth_token 3' -F -a '(__fish_complete_directories)'\n\n", bin)
+
+ fmt.Fprintf(b, "# Complete force flag for import (fourth token)\n")
+ fmt.Fprintf(b, "complete -c %s -n '__fish_seen_subcommand_from import; and __fish_is_nth_token 4' -f -a 'force'\n\n", bin)
+
+ fmt.Fprintf(b, "# Complete directory paths for import_r\n")
+ fmt.Fprintf(b, "complete -c %s -n '__fish_seen_subcommand_from import_r' -F -a '(__fish_complete_directories)'\n\n", bin)
+}
+
+// writeFishGeFunction writes the ge wrapper function definition.
+// The ge function provides shortcuts:
+// - No arguments → interactive shell mode
+// - First argument is a known command → pass through to foostore
+// - First argument is not a command → treat as search term
+func writeFishGeFunction(b *strings.Builder, binaryPath string) {
+ b.WriteString("# ge — foostore wrapper with shortcuts\n")
+ b.WriteString("# Usage:\n")
+ b.WriteString("# ge → interactive shell mode\n")
+ b.WriteString("# ge mypassword → same as: foostore search mypassword\n")
+ b.WriteString("# ge cat mypass → passes through to foostore\n")
+ b.WriteString("function ge --description 'foostore wrapper with shortcuts'\n")
+ b.WriteString(" # No arguments: enter interactive shell mode\n")
+ b.WriteString(" if test (count $argv) -eq 0\n")
+ fmt.Fprintf(b, " %s shell\n", binaryPath)
+ b.WriteString(" return $status\n")
+ b.WriteString(" end\n\n")
+ b.WriteString(" set -l cmd $argv[1]\n\n")
+ b.WriteString(" # If first argument is a known foostore command, pass through\n")
+ fmt.Fprintf(b, " if contains $cmd (%s commands 2>/dev/null)\n", binaryPath)
+ fmt.Fprintf(b, " %s $argv\n", binaryPath)
+ b.WriteString(" else\n")
+ b.WriteString(" # Not a command: treat as a search term\n")
+ fmt.Fprintf(b, " %s search $argv\n", binaryPath)
+ b.WriteString(" end\n")
+ b.WriteString("end\n\n")
+}
+
+// writeFishGeCompleteDirectives writes completion directives for the ge wrapper.
+// These mirror the foostore directives but apply to the "ge" command name.
+func writeFishGeCompleteDirectives(b *strings.Builder) {
+ b.WriteString("# ge completion: subcommands and bare search terms\n")
+ b.WriteString("complete -c ge -f -n '__fish_use_subcommand' -a '(__fish_foostore_commands)'\n")
+ b.WriteString("complete -c ge -f -n '__fish_use_subcommand' -a '(__fish_foostore_entries)'\n\n")
+
+ b.WriteString("# ge completion: entry names for search-type commands\n")
+ b.WriteString("complete -c ge -f -n '__fish_seen_subcommand_from search cat paste export pathexport open edit rm' -a '(__fish_foostore_entries)'\n\n")
+
+ b.WriteString("# ge completion: file paths for import\n")
+ b.WriteString("complete -c ge -n '__fish_seen_subcommand_from import' -F\n\n")
+
+ b.WriteString("# ge completion: directory paths for import destination (third token)\n")
+ b.WriteString("complete -c ge -n '__fish_seen_subcommand_from import; and __fish_is_nth_token 3' -F -a '(__fish_complete_directories)'\n\n")
+
+ b.WriteString("# ge completion: force flag for import (fourth token)\n")
+ b.WriteString("complete -c ge -n '__fish_seen_subcommand_from import; and __fish_is_nth_token 4' -f -a 'force'\n\n")
+
+ b.WriteString("# ge completion: directory paths for import_r\n")
+ b.WriteString("complete -c ge -n '__fish_seen_subcommand_from import_r' -F -a '(__fish_complete_directories)'\n")
+}