Files
dotfiles/install-stow.sh
2026-03-19 08:02:02 +01:00

150 lines
3.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# install-stow.sh: Stow dotfile packages into $HOME with per-package prompts.
# Packages that are already fully stowed are skipped automatically.
# Run after install-deps.sh.
set -euo pipefail
DOTFILES_DIR="$(cd "$(dirname "$0")" && pwd)"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
ok() { echo "$*"; }
info() { echo "$*"; }
warn() { echo "$*"; }
header() { echo ""; echo "==> $*"; }
# Ask yes/no. Returns 0 for yes, 1 for no.
ask_yn() {
local prompt="$1"
while true; do
read -r -p "$prompt [y/N] " answer
case "${answer,,}" in
y|yes) return 0 ;;
n|no|"") return 1 ;;
*) echo " Please answer y or n." ;;
esac
done
}
# ---------------------------------------------------------------------------
# Check if a package is already fully stowed
# (every file in the package has a correct symlink in $HOME)
# ---------------------------------------------------------------------------
is_stowed() {
local pkg="$1"
local pkg_dir="$DOTFILES_DIR/$pkg"
local all_ok=true
while IFS= read -r -d '' src; do
local rel="${src#$pkg_dir/}"
local target="$HOME/$rel"
# Must exist as a symlink pointing at this exact source
if [[ ! -L "$target" ]] || [[ "$(readlink -f "$target")" != "$(readlink -f "$src")" ]]; then
all_ok=false
break
fi
done < <(find "$pkg_dir" -type f -print0 2>/dev/null)
$all_ok
}
# List any real files (non-symlinks) that would be overwritten by stowing
conflicts_for() {
local pkg="$1"
local pkg_dir="$DOTFILES_DIR/$pkg"
local conflicts=()
while IFS= read -r -d '' src; do
local rel="${src#$pkg_dir/}"
local target="$HOME/$rel"
if [[ -e "$target" ]] && [[ ! -L "$target" ]]; then
conflicts+=("$target")
fi
done < <(find "$pkg_dir" -type f -print0 2>/dev/null)
printf '%s\n' "${conflicts[@]}"
}
# ---------------------------------------------------------------------------
# Collect packages (top-level dirs, skip hidden)
# ---------------------------------------------------------------------------
header "Stowing dotfile packages"
PACKAGES=()
for d in "$DOTFILES_DIR"/*/; do
pkg="$(basename "$d")"
[[ "$pkg" == .* ]] && continue
PACKAGES+=("$pkg")
done
STOWED=()
SKIPPED=()
for pkg in "${PACKAGES[@]}"; do
echo ""
echo "Package: $pkg"
if is_stowed "$pkg"; then
ok "already stowed — skipping"
STOWED+=("$pkg (already)")
continue
fi
# Show conflicts, if any
mapfile -t conflicts < <(conflicts_for "$pkg")
if [[ ${#conflicts[@]} -gt 0 ]]; then
warn "Stowing would overwrite these real files:"
for f in "${conflicts[@]}"; do
echo " $f"
done
echo " Back them up or delete them first, then re-run."
fi
if ask_yn " Stow '$pkg'?"; then
stow --dir="$DOTFILES_DIR" --target="$HOME" --restow "$pkg"
ok "stowed"
STOWED+=("$pkg")
else
info "skipped"
SKIPPED+=("$pkg")
fi
done
# ---------------------------------------------------------------------------
# Configure shell (after stowing zshrc)
# ---------------------------------------------------------------------------
header "Shell configuration"
if [[ ! -f "$HOME/.zshrc" ]]; then
touch "$HOME/.zshrc"
fi
if ! grep -q 'eval "$(starship init zsh)"' "$HOME/.zshrc"; then
echo 'eval "$(starship init zsh)"' >> "$HOME/.zshrc"
ok "Added starship init to ~/.zshrc"
else
ok "Starship already configured in ~/.zshrc"
fi
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
header "Summary"
if [[ ${#STOWED[@]} -gt 0 ]]; then
echo " Stowed: ${STOWED[*]}"
fi
if [[ ${#SKIPPED[@]} -gt 0 ]]; then
echo " Skipped: ${SKIPPED[*]}"
fi
echo ""
echo "Done."