#!/usr/bin/env bash # # dotfiles-link - Symlink dotfiles without Nix # # Usage: dotfiles-link [--dry-run] [--force] # # Reads MANIFEST file from dotfiles root and creates symlinks accordingly. # Works on both Linux and macOS (Darwin). # set -euo pipefail # ============================================================================ # Configuration # ============================================================================ DOTFILES_DIR="$(cd "$(dirname "$0")/.." && pwd)" CONFIG_DIR="${DOTFILES_DIR}/config" MANIFEST="${DOTFILES_DIR}/MANIFEST" # Detect OS case "$(uname -s)" in Darwin) OS="darwin" ;; Linux) OS="linux" ;; *) echo "Unsupported OS: $(uname -s)" >&2 exit 1 ;; esac # XDG config directory XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" # ============================================================================ # Functions # ============================================================================ DRY_RUN=true FORCE=false usage() { echo "Usage: $(basename "$0") [--apply] [--force]" echo "" echo "Options:" echo " --apply Actually create symlinks (dry-run is default)" echo " --force Remove existing files/symlinks before linking" echo "" echo "Manifest: ${MANIFEST}" echo "Detected OS: ${OS}" } log() { echo "[dotfiles-link] $*" } # Expand variables in target path expand_path() { local path="$1" # Use eval to expand $HOME and $XDG_CONFIG_HOME eval echo "$path" } create_symlink() { local source="$1" local target="$2" local target_dir target_dir="$(dirname "$target")" # Check if source exists if [[ ! -e $source ]]; then log "SKIP: Source does not exist: $source" return fi # Create parent directory if needed if [[ ! -d $target_dir ]]; then if $DRY_RUN; then log "WOULD CREATE DIR: $target_dir" else log "CREATE DIR: $target_dir" mkdir -p "$target_dir" fi fi # Handle existing target if [[ -e $target || -L $target ]]; then if [[ -L $target ]] && [[ "$(readlink "$target")" == "$source" ]]; then log "OK: $target (already linked)" return fi if $FORCE; then if $DRY_RUN; then log "WOULD REMOVE: $target" else log "REMOVE: $target" rm -rf "$target" fi else log "SKIP: $target exists (use --force to override)" return fi fi # Create symlink if $DRY_RUN; then log "WOULD LINK: $target -> $source" else log "LINK: $target -> $source" ln -s "$source" "$target" fi } process_manifest() { if [[ ! -f $MANIFEST ]]; then log "ERROR: Manifest not found: $MANIFEST" exit 1 fi while IFS= read -r line || [[ -n $line ]]; do # Skip comments and empty lines [[ -z $line || $line =~ ^[[:space:]]*# ]] && continue # Parse: os:source:target local entry_os="${line%%:*}" local rest="${line#*:}" local source="${rest%%:*}" local target="${rest#*:}" # Check if this entry applies to current OS if [[ $entry_os != "common" && $entry_os != "$OS" ]]; then continue fi # Expand variables and create symlink target="$(expand_path "$target")" create_symlink "${CONFIG_DIR}/${source}" "$target" done <"$MANIFEST" } # ============================================================================ # Main # ============================================================================ # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --apply) DRY_RUN=false shift ;; --force) FORCE=true shift ;; -h | --help) usage exit 0 ;; *) echo "Unknown option: $1" usage exit 1 ;; esac done log "Dotfiles directory: ${DOTFILES_DIR}" log "OS: ${OS}" $DRY_RUN && log "DRY RUN MODE - no changes will be made" echo "" process_manifest echo "" log "Done!"