221 lines
6.5 KiB
Bash
Executable File
221 lines
6.5 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
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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."
|