diff options
| -rw-r--r-- | CLAUDE.md | 6 | ||||
| -rw-r--r-- | FISH_INTEGRATION.md | 89 | ||||
| -rw-r--r-- | README.md | 11 | ||||
| -rw-r--r-- | completions/foostore.fish | 33 | ||||
| -rw-r--r-- | completions/ge.fish | 53 | ||||
| -rwxr-xr-x | install-fish.sh | 23 | ||||
| -rw-r--r-- | internal/cli/cli.go | 4 | ||||
| -rw-r--r-- | internal/cli/cli_dispatch.go | 4 | ||||
| -rw-r--r-- | internal/cli/cli_fish.go | 156 |
9 files changed, 172 insertions, 207 deletions
@@ -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) @@ -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") +} |
