#!/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 # --------------------------------------------------------------------------- # Firefox userChrome / userContent # --------------------------------------------------------------------------- header "Firefox" FF_PROFILES_INI="$HOME/.mozilla/firefox/profiles.ini" if [[ ! -f "$FF_PROFILES_INI" ]]; then warn "Firefox profiles.ini not found — skipping" else # Prefer the profile the installed Firefox binary actually uses FF_PROFILE=$(grep -A1 '^\[Install' "$FF_PROFILES_INI" | grep 'Default=' | head -1 | cut -d= -f2) # Fall back to the profile marked Default=1 inside a [Profile] block if [[ -z "$FF_PROFILE" ]]; then FF_PROFILE=$(awk '/^\[Profile/{p=1} p && /^Default=1/{found=1} p && /^Path=/{path=$0} found{print path; exit}' \ "$FF_PROFILES_INI" | cut -d= -f2) fi if [[ -z "$FF_PROFILE" ]]; then warn "Could not determine Firefox profile path — skipping" else FF_PROFILE_DIR="$HOME/.mozilla/firefox/$FF_PROFILE" echo " Profile: $FF_PROFILE_DIR" FF_SRC="$DOTFILES_DIR/firefox" if ask_yn " Stow 'firefox' → $FF_PROFILE_DIR?"; then while IFS= read -r -d '' src; do rel="${src#$FF_SRC/}" # policies.json is system-wide — handled separately below [[ "$rel" == "policies.json" ]] && continue target="$FF_PROFILE_DIR/$rel" target_dir="$(dirname "$target")" mkdir -p "$target_dir" if [[ -e "$target" ]] && [[ ! -L "$target" ]]; then warn "Backing up existing $target → $target.bak" mv "$target" "$target.bak" fi ln -sf "$src" "$target" ok "linked $rel" done < <(find "$FF_SRC" -type f -print0 2>/dev/null) else info "skipped" fi # Install policies.json system-wide so extensions auto-install on first launch FF_DISTRIB=$(dirname "$(readlink -f "$(which firefox)")" 2>/dev/null) # Fallback: locate the real firefox binary directory if [[ ! -d "$FF_DISTRIB" ]] || [[ "$FF_DISTRIB" == "/usr/bin" ]]; then FF_DISTRIB=$(find /usr/lib /usr/lib64 /opt -maxdepth 2 -name 'firefox' -type f 2>/dev/null \ | grep -v '/bin/' | head -1 | xargs dirname 2>/dev/null) fi if [[ -z "$FF_DISTRIB" ]]; then warn "Could not locate Firefox installation directory — skipping policies.json" else FF_POLICIES_DEST="$FF_DISTRIB/distribution/policies.json" FF_POLICIES_SRC="$FF_SRC/policies.json" echo " policies.json → $FF_POLICIES_DEST (requires sudo)" if ask_yn " Install policies.json (extensions auto-install on next Firefox launch)?"; then sudo mkdir -p "$FF_DISTRIB/distribution" sudo ln -sf "$FF_POLICIES_SRC" "$FF_POLICIES_DEST" ok "linked policies.json" else info "skipped" fi fi fi fi # --------------------------------------------------------------------------- # 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."