#!/usr/bin/env zsh set -e DISTRO="$(sed -n 's/^NAME="\([^"]\+\)"/\1/p' /etc/os-release)" SCRIPT_FILE="$(readlink -f -- "$0")" SCRIPT_NAME="$(basename -- "$SCRIPT_FILE")" SCRIPT_DIR="$(dirname -- "$SCRIPT_FILE")" SOURCE_DIR="$SCRIPT_DIR" DEST_DIR="$HOME" PRINT_HELP=false FORCE=false REMOVE_EXISTING=false IGNORE_EXISTING=false DIFF=false ERROR=false # Define packages typeset -a PKGS PKGS=( "alacritty" "i3" "i3lock" "i3status" "jq" "lf" "rofi" "startx" "tmux" "zsh" ) # Define paths to symlink typeset -a SYMLINKS SYMLINKS=( ".config/alacritty" ".config/dolphinrc" ".config/dunst" ".config/fish" ".config/foot" ".config/fontconfig" ".config/frogminer" ".config/ghostty" ".config/i3" ".config/i3status" ".config/kde-mimeapps.list" ".config/kglobalshortcutsrc" ".config/klipperrc" ".config/kwinrc" ".config/lf" ".config/mimeapps.list" ".config/opencode" ".config/picom" ".config/pipewire" ".config/rofi" ".config/strawberry" ".config/tmux" ".config/vlc" ".config/wezterm" ".config/wireplumber" ".config/yay" ".config/zed" ".local/bin" ".local/share/dbus-1" ".local/share/easyeffects" ".local/share/fonts" ".local/share/konsole" ".gdbinit" ".vimrc" ".xinit-scripts" ".xinitrc" ".Xresources" ) typeset -a COPIES COPIES=( ".claude/settings.json" ".codex/config.toml" ".config/gtk-3.0/bookmarks" ".config/gtk-3.0/settings.ini" ".config/gtk-4.0/settings.ini" ".gtkrc-2.0" ) typeset -a SYSTEM_INSTALLS SYSTEM_INSTALLS=( "sysctl.d/99-gaming-perf.conf" "sysctl.d/99-network.conf" "tmpfiles.d/99-gaming-perf.conf" "udev/rules.d/99-perf.rules" "ssh/sshd_config.d/sshd_harden.conf" ) typeset -A SYMLINK_MAP SYMLINK_MAP[zsh/rc]=".zshrc" error() { local msg="$@" ERROR=true echo "Error: $msg" >&2 } check_packages_installed() { for pkg in "${PKGS[@]}"; do if ! type "$pkg" >/dev/null; then error "missing $pkg" fi done } remove_symlink() { local link src link="${DEST_DIR}/$1" if test -L "$link"; then src="$(readlink -f -- "$link")" echo "Removing symlink: $link -> $src" rm "$link" elif test -e "$link"; then error "object to be removed is not a symlink:" \ "${link}: $(stat -c '%F' -- "$link")" return 0 fi } remove_all_symlinks() { for link in "${SYMLINKS[@]}"; do remove_symlink "$link" done for src dst in ${(kv)SYMLINK_MAP}; do remove_symlink "$dst" done } create_symlink() { local rel_src rel_dst src dst dst_parent rel_src="$1" rel_dst="$2" if test -z "$rel_src"; then error "missing src argument: $0 $@" return 1 fi if test -z "$rel_dst"; then error "missing dst argument: $0 $@" return 1 fi src="${SCRIPT_DIR}/$rel_src" dst="${DEST_DIR}/$rel_dst" dst_parent="$(dirname -- "$dst")" if ! test -e "$src"; then error "the following source path does not exist: $src" return 1 fi if ! test -d "$dst_parent"; then echo "Creating parent: $dst_parent" mkdir -p "$dst_parent" fi if test -L "$dst"; then if test "$(readlink -f -- "$dst")" = "$src"; then return 0 fi if $FORCE; then remove_symlink "$rel_dst" elif $IGNORE_EXISTING; then return 0 else error "symlink exists but points elsewhere:" \ "$dst -> $(readlink -f -- "$dst")" return 0 fi elif test -e "$dst"; then error "path already exists but is not a symlink:" \ "${dst}: $(stat -c '%F' -- "$dst")" return 0 fi echo "Creating link: $dst -> $src" ln -s "$src" "$dst" } remove_path() { local target target="$1" echo "Trashing item: $target" gio trash "$target" } copy_item() { local rel_src rel_dst src dst dst_parent rel_src="$1" rel_dst="$2" if test -z "$rel_src"; then error "missing src argument: $0 $@" return 1 fi if test -z "$rel_dst"; then error "missing dst argument: $0 $@" return 1 fi src="${SCRIPT_DIR}/$rel_src" dst="${DEST_DIR}/$rel_dst" dst_parent="$(dirname -- "$dst")" if ! test -f "$src"; then error "source is not a regular file: $src" return 1 fi if ! test -d "$dst_parent"; then echo "Creating parent: $dst_parent" mkdir -p "$dst_parent" fi if test -e "$dst"; then if cmp -s "$src" "$dst"; then return 0 fi if $DIFF; then diff -u "$dst" "$src" || true fi if $FORCE; then remove_path "$dst" elif $IGNORE_EXISTING; then return 0 else error "file already exists and differs: ${dst}" return 0 fi fi echo "Copying item: from $rel_src to ${dst_parent}/" cp "$src" "${dst_parent}/" } create_all_symlinks() { for link in "${SYMLINKS[@]}"; do create_symlink "$link" "$link" done for src dst in ${(kv)SYMLINK_MAP}; do create_symlink "$src" "$dst" done } copy_all_items() { for item in "${COPIES[@]}"; do copy_item "$item" "$item" done } install_system_file() { local rel src dst dst_parent rel="$1" if test -z "$rel"; then error "missing path argument: $0 $@" return 1 fi src="${SCRIPT_DIR}/etc/$rel" dst="/etc/$rel" dst_parent="$(dirname -- "$dst")" if ! test -e "$src"; then error "the following source path does not exist: $src" return 1 fi if ! sudo test -d "$dst_parent"; then echo "Creating parent: $dst_parent" sudo mkdir -p "$dst_parent" fi if sudo test -e "$dst"; then if sudo cmp -s "$src" "$dst"; then return 0 fi if $DIFF; then sudo diff -u "$dst" "$src" || true fi if $FORCE; then echo "Overwriting: $dst" elif $IGNORE_EXISTING; then return 0 else error "system file already exists and differs: $dst" return 0 fi fi echo "Installing: etc/$rel -> $dst" sudo install -m 644 "$src" "$dst" } install_all_system_files() { for rel in "${SYSTEM_INSTALLS[@]}"; do install_system_file "$rel" done } check_terminfo() { if ! infocmp tmux-256color > /dev/null; then error "Missing terminfo for tmux-256color. Try installing ncurses-term." return 0 fi } print_help() { echo "Usage: $SCRIPT_NAME [...]" echo "" echo "Options:" echo " -h, --help Print this help message" echo " -f, --force Overwrite any existing links" echo " -i, --ignore-existing Ignore existing symlinks" echo " -r, --remove-existing Remove any existing symlinks" echo " -d, --diff Show unified diff when a managed file differs" } while [ $# -gt 0 ]; do case $1 in -h|--help) PRINT_HELP=true shift ;; -f|--force) FORCE=true shift ;; -r|--remove-existing) REMOVE_EXISTING=true shift ;; -i|--ignore-existing) IGNORE_EXISTING=true shift ;; -d|--diff) DIFF=true shift ;; *) error "unknown option: $1" PRINT_HELP=true shift ;; esac done if $PRINT_HELP; then print_help elif $REMOVE_EXISTING; then remove_all_symlinks else check_terminfo check_packages_installed create_all_symlinks copy_all_items install_all_system_files fi if $ERROR; then exit 1 fi