Add shell auto-completion for zsh, bash, and fish

- completions/_ssr            zsh #compdef completion using _arguments/_values
- completions/ssr.bash        bash complete -F function (works with bash-completion@2)
- completions/ssr.fish        fish completions with __ssr_needs_subcmd predicates
- bin/ssr-completions         install/uninstall/status subcommand
                              - auto-detects $SHELL, --shell zsh|bash|fish|all
                              - symlinks into user-scoped paths (no sudo):
                                ~/.zsh/completions/_ssr
                                ~/.local/share/bash-completion/completions/ssr
                                ~/.config/fish/completions/ssr.fish
                              - prints fpath/bash-completion setup hints if not configured
- bin/ssr                     dispatcher knows `completions`; help line added
- bin/ssr-install             offers completion install after schedule prompt
- README                      Shell completions section, command list, layout entry

Verified:
- zsh -n  on _ssr        OK; autoload registers function
- bash -n on ssr.bash    OK; `complete -p ssr` returns the registration after source
- end-to-end install/status/uninstall against an ephemeral $HOME

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-11 16:16:22 +02:00
parent 69541209dc
commit c1848f66e8
7 changed files with 383 additions and 1 deletions
+2 -1
View File
@@ -32,6 +32,7 @@ Commands:
update [--dry-run] Pull latest ssr code, refresh launchd if needed.
schedule on|off Enable/disable the scheduled launchd sync job.
config show|edit Print or edit the active config file.
completions Install or remove shell completions (zsh/bash/fish).
status Show last sync time, cloud target, and scheduled state.
version Print version.
@@ -61,7 +62,7 @@ done
cmd="$1"; shift
case "$cmd" in
install|sync|restore|update|schedule|config|status)
install|sync|restore|update|schedule|config|completions|status)
target="$SSR_BIN_DIR/ssr-$cmd"
[[ -x "$target" ]] || ssr::die "Subcommand not executable: $target"
exec "$target" "$@"
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
# ssr-completions — install/uninstall shell completions for ssr.
#
# Shell -> target dir (user-scoped, no sudo):
# zsh ~/.zsh/completions/_ssr (added to $fpath via shell hint)
# bash ~/.local/share/bash-completion/completions/ssr
# fish ~/.config/fish/completions/ssr.fish
set -euo pipefail
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=../lib/common.sh
source "$SSR_ROOT/lib/common.sh"
ZSH_SRC="$SSR_ROOT/completions/_ssr"
BASH_SRC="$SSR_ROOT/completions/ssr.bash"
FISH_SRC="$SSR_ROOT/completions/ssr.fish"
ZSH_DST="$HOME/.zsh/completions/_ssr"
BASH_DST="$HOME/.local/share/bash-completion/completions/ssr"
FISH_DST="$HOME/.config/fish/completions/ssr.fish"
usage() {
cat <<EOF
Usage: ssr completions <install|uninstall|status> [--shell zsh|bash|fish|all]
Install or remove shell completions for ssr.
Without --shell, auto-detects from \$SHELL and installs only that one.
Use --shell all to install for every supported shell.
EOF
}
detect_shell() {
case "${SHELL:-}" in
*/zsh) echo zsh ;;
*/bash) echo bash ;;
*/fish) echo fish ;;
*) echo "" ;;
esac
}
install_zsh() {
ssr::ensure_dir "$(dirname "$ZSH_DST")"
ln -sfn "$ZSH_SRC" "$ZSH_DST"
ssr::ok "zsh: $ZSH_DST"
# Hint: shell rc needs fpath entry. Detect if already configured.
if ! grep -qsE "(\.zsh/completions|fpath\+?=.*\.zsh/completions)" "$HOME/.zshrc" 2>/dev/null; then
cat <<'HINT'
Add to your ~/.zshrc:
fpath=("$HOME/.zsh/completions" $fpath)
autoload -Uz compinit && compinit
HINT
fi
}
install_bash() {
ssr::ensure_dir "$(dirname "$BASH_DST")"
ln -sfn "$BASH_SRC" "$BASH_DST"
ssr::ok "bash: $BASH_DST"
if ! command -v bash-completion >/dev/null 2>&1 \
&& [[ ! -f /opt/homebrew/etc/profile.d/bash_completion.sh \
&& ! -f /usr/local/etc/profile.d/bash_completion.sh ]]; then
cat <<'HINT'
Install bash-completion to activate:
brew install bash-completion@2
and add to ~/.bashrc:
[[ -r /opt/homebrew/etc/profile.d/bash_completion.sh ]] && \
source /opt/homebrew/etc/profile.d/bash_completion.sh
HINT
fi
}
install_fish() {
ssr::ensure_dir "$(dirname "$FISH_DST")"
ln -sfn "$FISH_SRC" "$FISH_DST"
ssr::ok "fish: $FISH_DST"
}
uninstall_zsh() { rm -f "$ZSH_DST" && ssr::ok "removed $ZSH_DST"; }
uninstall_bash() { rm -f "$BASH_DST" && ssr::ok "removed $BASH_DST"; }
uninstall_fish() { rm -f "$FISH_DST" && ssr::ok "removed $FISH_DST"; }
status_one() {
local label="$1" dst="$2"
if [[ -L "$dst" || -f "$dst" ]]; then
printf ' %-5s installed: %s\n' "$label" "$dst"
else
printf ' %-5s not installed\n' "$label"
fi
}
action="${1:-status}"; shift || true
shells=""
while [[ $# -gt 0 ]]; do
case "$1" in
--shell) shells="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) ssr::die "Unknown flag: $1" ;;
esac
done
if [[ -z "$shells" ]]; then
shells="$(detect_shell)"
[[ -n "$shells" ]] || ssr::die "Could not detect \$SHELL; pass --shell zsh|bash|fish|all"
fi
[[ "$shells" == "all" ]] && shells="zsh bash fish"
case "$action" in
install)
for s in $shells; do
case "$s" in
zsh) install_zsh ;;
bash) install_bash ;;
fish) install_fish ;;
*) ssr::die "Unknown shell: $s" ;;
esac
done
ssr::log "Open a new shell (or 'exec \$SHELL') to activate."
;;
uninstall)
for s in $shells; do
case "$s" in
zsh) uninstall_zsh ;;
bash) uninstall_bash ;;
fish) uninstall_fish ;;
*) ssr::die "Unknown shell: $s" ;;
esac
done
;;
status)
echo "ssr completions:"
status_one zsh "$ZSH_DST"
status_one bash "$BASH_DST"
status_one fish "$FISH_DST"
;;
*)
usage; exit 1 ;;
esac
+5
View File
@@ -44,4 +44,9 @@ if ssr::confirm "Enable daily scheduled sync (launchd)?" 30; then
"$SSR_ROOT/bin/ssr-schedule" on
fi
# Offer to install shell completions for the detected shell.
if ssr::confirm "Install shell completions for $(basename "${SHELL:-unknown}")?" 30; then
"$SSR_ROOT/bin/ssr-completions" install
fi
ssr::ok "Install complete. Run: ssr status"