nix/bin/dotfiles-link

176 lines
4.1 KiB
Bash
Executable file

#!/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!"