From c1848f66e8586aca7db4d6ee10e04102b87f083c Mon Sep 17 00:00:00 2001 From: "Oliver Surke (Gitea)" Date: Mon, 11 May 2026 16:16:22 +0200 Subject: [PATCH] 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 --- README.md | 31 ++++++++++ bin/ssr | 3 +- bin/ssr-completions | 138 +++++++++++++++++++++++++++++++++++++++++++ bin/ssr-install | 5 ++ completions/_ssr | 67 +++++++++++++++++++++ completions/ssr.bash | 69 ++++++++++++++++++++++ completions/ssr.fish | 71 ++++++++++++++++++++++ 7 files changed, 383 insertions(+), 1 deletion(-) create mode 100755 bin/ssr-completions create mode 100644 completions/_ssr create mode 100644 completions/ssr.bash create mode 100644 completions/ssr.fish diff --git a/README.md b/README.md index 14d2edd..cc1cef9 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ ssr restore [Brewfile] Restore the machine; Brewfile defaults to the cloud cop ssr update [--dry-run] Pull latest ssr code, refresh launchd if needed ssr schedule on|off Enable/disable the daily launchd job ssr config show|edit Show or edit the active config +ssr completions install Install zsh/bash/fish tab-completion ssr status Show config, cloud target, last sync, schedule state ssr version Print version ``` @@ -124,6 +125,31 @@ Config lives in `~/.config/ssr/ssr.conf` (override with `--config` or `$SSR_CONF --- +## Shell completions + +`ssr install` offers to install completions automatically. To do it manually later: + +```bash +ssr completions install # detects $SHELL +ssr completions install --shell all # zsh + bash + fish +ssr completions status # show what's wired up +ssr completions uninstall --shell all +``` + +Targets (user-scoped, no sudo): + +| Shell | Path | +|-------|------| +| zsh | `~/.zsh/completions/_ssr` | +| bash | `~/.local/share/bash-completion/completions/ssr` | +| fish | `~/.config/fish/completions/ssr.fish` | + +For zsh, ensure `~/.zsh/completions` is on `$fpath` (the installer prints the snippet for `~/.zshrc` if it isn't already). For bash, you need [`bash-completion@2`](https://github.com/scop/bash-completion) sourced from your shell rc — Homebrew install path is also printed. Fish needs no extra setup. + +After installation, open a fresh shell or `exec $SHELL` and `ssr ` lists the available commands. + +--- + ## Scheduled sync ```bash @@ -184,6 +210,7 @@ ssr update --dry-run # show what would change, don't pull │ ├── ssr-restore # Rebuild a Mac from the backup │ ├── ssr-schedule # launchd on/off/status │ ├── ssr-config # show/edit/path +│ ├── ssr-completions # install zsh/bash/fish completions │ └── ssr-status # state summary ├── lib/ │ ├── common.sh # Logging, validation, config loader @@ -194,6 +221,10 @@ ssr update --dry-run # show what would change, don't pull ├── config/ │ ├── ssr.conf.example # Reference user config │ └── macos-defaults.sh # Default macOS preferences +├── completions/ +│ ├── _ssr # zsh +│ ├── ssr.bash # bash +│ └── ssr.fish # fish ├── launchd/ │ └── de.surke.ssr.sync.plist # Template for the scheduled agent ├── VERSION diff --git a/bin/ssr b/bin/ssr index ed70934..5f8b18f 100755 --- a/bin/ssr +++ b/bin/ssr @@ -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" "$@" diff --git a/bin/ssr-completions b/bin/ssr-completions new file mode 100755 index 0000000..1bdd394 --- /dev/null +++ b/bin/ssr-completions @@ -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 < [--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 diff --git a/bin/ssr-install b/bin/ssr-install index 83167bb..d048cda 100755 --- a/bin/ssr-install +++ b/bin/ssr-install @@ -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" diff --git a/completions/_ssr b/completions/_ssr new file mode 100644 index 0000000..608b082 --- /dev/null +++ b/completions/_ssr @@ -0,0 +1,67 @@ +#compdef ssr +# zsh completion for ssr — System Sync & Restore. +# Install: copy or symlink into a directory on $fpath, then `autoload -Uz _ssr`. + +_ssr() { + local context state line + typeset -A opt_args + + local -a commands=( + 'install:First-time install (link binary, seed config, schedule sync)' + 'sync:Run a backup → cloud' + 'restore:Restore a machine (defaults to cloud Brewfile)' + 'update:Pull latest ssr code, refresh launchd if needed' + 'schedule:Manage the scheduled launchd sync job' + 'config:Show or edit the active config file' + 'completions:Install or remove shell completions' + 'status:Show last sync time, cloud target, schedule state' + 'version:Print version' + 'help:Show top-level help' + ) + + _arguments -C \ + '(-h --help)'{-h,--help}'[Show help]' \ + '(-c --config)'{-c,--config}'[Use a specific config file]:path:_files' \ + '1: :->cmd' \ + '*::arg:->args' + + case "$state" in + cmd) + _describe -t commands 'ssr command' commands + ;; + args) + case "${line[1]}" in + restore) + _arguments \ + '(-h --help)'{-h,--help}'[Show help]' \ + '1:Brewfile:_files' + ;; + update) + _arguments \ + '(-h --help)'{-h,--help}'[Show help]' \ + '(-n --dry-run)'{-n,--dry-run}'[Preview without pulling]' + ;; + schedule) + _values 'schedule action' \ + 'on[Enable scheduled sync]' \ + 'off[Disable scheduled sync]' \ + 'status[Show schedule state]' + ;; + config) + _values 'config action' \ + 'show[Print active config]' \ + 'edit[Open config in $EDITOR]' \ + 'path[Print config path]' + ;; + completions) + _values 'completions action' \ + 'install[Install shell completions]' \ + 'uninstall[Remove shell completions]' \ + 'status[Show installed completions]' + ;; + esac + ;; + esac +} + +_ssr "$@" diff --git a/completions/ssr.bash b/completions/ssr.bash new file mode 100644 index 0000000..e6f98d4 --- /dev/null +++ b/completions/ssr.bash @@ -0,0 +1,69 @@ +# bash completion for ssr — System Sync & Restore. +# Source from your ~/.bashrc or place under ~/.local/share/bash-completion/completions/ssr + +_ssr() { + local cur prev words cword + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion || return + else + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + words=("${COMP_WORDS[@]}") + cword=$COMP_CWORD + fi + + local commands="install sync restore update schedule config completions status version help" + local global_flags="-c --config -h --help" + + # Detect command (first non-flag word after `ssr`). + local cmd="" + local i + for ((i = 1; i < cword; i++)); do + case "${words[i]}" in + -c|--config) ((i++)); continue ;; + -*) continue ;; + *) cmd="${words[i]}"; break ;; + esac + done + + # Handle --config . + if [[ "$prev" == "-c" || "$prev" == "--config" ]]; then + COMPREPLY=( $(compgen -f -- "$cur") ) + return + fi + + # Top-level command selection. + if [[ -z "$cmd" ]]; then + if [[ "$cur" == -* ]]; then + COMPREPLY=( $(compgen -W "$global_flags" -- "$cur") ) + else + COMPREPLY=( $(compgen -W "$commands" -- "$cur") ) + fi + return + fi + + # Per-command argument completion. + case "$cmd" in + restore) + COMPREPLY=( $(compgen -f -- "$cur") ) + ;; + update) + COMPREPLY=( $(compgen -W "--dry-run -n -h --help" -- "$cur") ) + ;; + schedule) + COMPREPLY=( $(compgen -W "on off status" -- "$cur") ) + ;; + config) + COMPREPLY=( $(compgen -W "show edit path" -- "$cur") ) + ;; + completions) + COMPREPLY=( $(compgen -W "install uninstall status" -- "$cur") ) + ;; + *) + COMPREPLY=() + ;; + esac +} + +complete -F _ssr ssr diff --git a/completions/ssr.fish b/completions/ssr.fish new file mode 100644 index 0000000..b9ac255 --- /dev/null +++ b/completions/ssr.fish @@ -0,0 +1,71 @@ +# fish completion for ssr — System Sync & Restore. +# Install: drop into ~/.config/fish/completions/ssr.fish + +set -l ssr_cmds install sync restore update schedule config completions status version help + +# True when no subcommand has been picked yet (only flags / option-args seen so far). +function __ssr_needs_subcmd + set -l tokens (commandline -opc) + set -l skip_next 0 + for tok in $tokens[2..-1] + if test $skip_next -eq 1 + set skip_next 0 + continue + end + switch $tok + case -c --config + set skip_next 1 + case '-*' + # ignore other flags + case '*' + return 1 + end + end + return 0 +end + +# True when the chosen subcommand equals $argv[1]. +function __ssr_using_subcmd + set -l target $argv[1] + set -l tokens (commandline -opc) + set -l skip_next 0 + for tok in $tokens[2..-1] + if test $skip_next -eq 1 + set skip_next 0 + continue + end + switch $tok + case -c --config + set skip_next 1 + case '-*' + case '*' + test "$tok" = "$target"; and return 0 + return 1 + end + end + return 1 +end + +# Global flags +complete -c ssr -s c -l config -d 'Config file path' -r -F +complete -c ssr -s h -l help -d 'Show help' + +# Subcommands +complete -c ssr -n __ssr_needs_subcmd -a install -d 'First-time install' +complete -c ssr -n __ssr_needs_subcmd -a sync -d 'Run a backup → cloud' +complete -c ssr -n __ssr_needs_subcmd -a restore -d 'Restore a machine' +complete -c ssr -n __ssr_needs_subcmd -a update -d 'Pull latest ssr code' +complete -c ssr -n __ssr_needs_subcmd -a schedule -d 'Manage launchd schedule' +complete -c ssr -n __ssr_needs_subcmd -a config -d 'Show/edit config' +complete -c ssr -n __ssr_needs_subcmd -a completions -d 'Install shell completions' +complete -c ssr -n __ssr_needs_subcmd -a status -d 'Show state summary' +complete -c ssr -n __ssr_needs_subcmd -a version -d 'Print version' +complete -c ssr -n __ssr_needs_subcmd -a help -d 'Show top-level help' + +# Per-subcommand args +complete -c ssr -n '__ssr_using_subcmd restore' -F +complete -c ssr -n '__ssr_using_subcmd update' -s n -l dry-run -d 'Preview without pulling' + +complete -c ssr -n '__ssr_using_subcmd schedule' -a 'on off status' +complete -c ssr -n '__ssr_using_subcmd config' -a 'show edit path' +complete -c ssr -n '__ssr_using_subcmd completions' -a 'install uninstall status'