| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | set -euo pipefail |
| 7 | |
| 8 | DOTFILES="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 9 | CONFIG_SRC="$DOTFILES/config" |
| 10 | CONFIG_DST="${XDG_CONFIG_HOME:-$HOME/.config}" |
| 11 | BACKUP_ROOT="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/backups" |
| 12 | BACKUP_DIR="$BACKUP_ROOT/$(date +%Y%m%d-%H%M%S)" |
| 13 | FONT_DIR="$HOME/.local/share/fonts/JetBrainsMonoNerd" |
| 14 | FONT_URL="https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.tar.xz" |
| 15 | |
| 16 | DRY=0 YES=0 |
| 17 | DO_PKGS=0 DO_FONTS=0 DO_LINK=0 DO_CHECK=0 DO_UNINSTALL=0 |
| 18 | MODULES=() |
| 19 | |
| 20 | |
| 21 | if [[ -t 1 ]]; then |
| 22 | B=$'\e[1m' D=$'\e[2m' R=$'\e[31m' G=$'\e[32m' Y=$'\e[33m' M=$'\e[35m' C=$'\e[36m' N=$'\e[0m' |
| 23 | else |
| 24 | B= D= R= G= Y= M= C= N= |
| 25 | fi |
| 26 | info() { printf '%s::%s %s\n' "$C$B" "$N" "$*"; } |
| 27 | ok() { printf ' %sโ%s %s\n' "$G" "$N" "$*"; } |
| 28 | warn() { printf ' %s!%s %s\n' "$Y" "$N" "$*"; } |
| 29 | err() { printf ' %sโ%s %s\n' "$R" "$N" "$*" >&2; } |
| 30 | run() { if (( DRY )); then printf ' %s[dry-run]%s %s\n' "$D" "$N" "$*"; else "$@"; fi; } |
| 31 | has() { command -v "$1" >/dev/null 2>&1; } |
| 32 | ask() { (( YES )) && return 0; read -rp " $1 [y/N] " a; [[ $a == [yY]* ]]; } |
| 33 | |
| 34 | banner() { |
| 35 | printf '%s' "$M$B" |
| 36 | cat <<'ART' |
| 37 | __ __ ____ __ |
| 38 | ____/ /___ / /_/ __(_) /__ _____ |
| 39 | / __ / __ \/ __/ /_/ / / _ \/ ___/ |
| 40 | / /_/ / /_/ / /_/ __/ / / __(__ ) |
| 41 | \__,_/\____/\__/_/ /_/_/\___/____/ sway ยท waybar ยท catppuccin |
| 42 | ART |
| 43 | printf '%s\n' "$N" |
| 44 | } |
| 45 | |
| 46 | usage() { |
| 47 | cat <<USAGE |
| 48 | ${B}Usage:${N} ./install.sh [options] [module ...] |
| 49 | |
| 50 | ${B}Options${N} |
| 51 | -a, --all Full setup: packages + fonts + symlinks |
| 52 | -p, --packages Install dependencies (apt, pacman or dnf) |
| 53 | -f, --fonts Download JetBrainsMono Nerd Font into ~/.local/share/fonts |
| 54 | -l, --link Symlink configs into ~/.config (default when no option is given) |
| 55 | -c, --check Show which required programs are missing |
| 56 | -u, --uninstall Remove the symlinks and restore your latest backup |
| 57 | -n, --dry-run Print what would happen, change nothing |
| 58 | -y, --yes Don't ask for confirmation |
| 59 | -h, --help Show this help |
| 60 | |
| 61 | ${B}Modules${N} (default: all) |
| 62 | $(available_modules | tr '\n' ' ') |
| 63 | |
| 64 | ${B}Examples${N} |
| 65 | ./install.sh --all # fresh machine |
| 66 | ./install.sh # just link the configs |
| 67 | ./install.sh waybar mako # only link these two |
| 68 | ./install.sh -n -a # preview everything |
| 69 | USAGE |
| 70 | } |
| 71 | |
| 72 | available_modules() { find "$CONFIG_SRC" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort; } |
| 73 | |
| 74 | |
| 75 | detect_pm() { |
| 76 | if has apt-get; then echo apt |
| 77 | elif has pacman; then echo pacman |
| 78 | elif has dnf; then echo dnf |
| 79 | else echo unknown; fi |
| 80 | } |
| 81 | |
| 82 | install_packages() { |
| 83 | local pm list pkgs=() |
| 84 | pm=$(detect_pm) |
| 85 | list="$DOTFILES/packages/$pm.txt" |
| 86 | info "Installing packages with ${B}$pm${N}" |
| 87 | [[ -f $list ]] || { err "No package list for '$pm'. Install the programs from packages/apt.txt by hand."; return 1; } |
| 88 | mapfile -t pkgs < <(grep -vE '^\s*(#|$)' "$list") |
| 89 | |
| 90 | |
| 91 | local available=() missing=() |
| 92 | for p in "${pkgs[@]}"; do |
| 93 | case $pm in |
| 94 | apt) apt-cache show "$p" >/dev/null 2>&1 && available+=("$p") || missing+=("$p") ;; |
| 95 | pacman) pacman -Si "$p" >/dev/null 2>&1 && available+=("$p") || missing+=("$p") ;; |
| 96 | dnf) available+=("$p") ;; |
| 97 | esac |
| 98 | done |
| 99 | (( ${#missing[@]} )) && warn "Not in your repos, skipped: ${missing[*]}" |
| 100 | printf ' %s%s%s\n' "$D" "${available[*]}" "$N" | fold -s -w 100 |
| 101 | |
| 102 | ask "Install ${#available[@]} packages?" || { warn "Skipped packages"; return 0; } |
| 103 | case $pm in |
| 104 | apt) run sudo apt-get update && run sudo apt-get install -y "${available[@]}" ;; |
| 105 | pacman) run sudo pacman -S --needed --noconfirm "${available[@]}" ;; |
| 106 | dnf) run sudo dnf install -y --skip-unavailable "${available[@]}" ;; |
| 107 | esac |
| 108 | ok "Packages installed" |
| 109 | } |
| 110 | |
| 111 | install_fonts() { |
| 112 | info "Installing JetBrainsMono Nerd Font" |
| 113 | if fc-list 2>/dev/null | grep -qi "JetBrainsMono Nerd"; then |
| 114 | ok "Already installed"; return 0 |
| 115 | fi |
| 116 | has curl || { err "curl is required"; return 1; } |
| 117 | run mkdir -p "$FONT_DIR" |
| 118 | if (( DRY )); then |
| 119 | run curl -fL "$FONT_URL" "| tar -xJ -C $FONT_DIR" |
| 120 | else |
| 121 | curl -fL --progress-bar "$FONT_URL" | tar -xJ -C "$FONT_DIR" |
| 122 | fi |
| 123 | run fc-cache -f "$FONT_DIR" >/dev/null |
| 124 | ok "Font installed to ${FONT_DIR/#$HOME/\~}" |
| 125 | } |
| 126 | |
| 127 | |
| 128 | link_module() { |
| 129 | local name=$1 src="$CONFIG_SRC/$1" dst="$CONFIG_DST/$1" |
| 130 | [[ -d $src ]] || { err "Unknown module: $name"; return 1; } |
| 131 | |
| 132 | if [[ -L $dst && $(readlink -f "$dst") == "$src" ]]; then |
| 133 | ok "$name ${D}(already linked)${N}"; return 0 |
| 134 | fi |
| 135 | if [[ -e $dst || -L $dst ]]; then |
| 136 | run mkdir -p "$BACKUP_DIR" |
| 137 | run mv "$dst" "$BACKUP_DIR/$name" |
| 138 | warn "$name: old config moved to ${BACKUP_DIR/#$HOME/\~}/$name" |
| 139 | fi |
| 140 | run ln -s "$src" "$dst" |
| 141 | ok "$name โ ${dst/#$HOME/\~}" |
| 142 | } |
| 143 | |
| 144 | link_all() { |
| 145 | info "Linking configs into ${CONFIG_DST/#$HOME/\~}" |
| 146 | run mkdir -p "$CONFIG_DST" |
| 147 | for m in "${MODULES[@]}"; do link_module "$m"; done |
| 148 | run chmod +x "$CONFIG_SRC"/sway/scripts/*.sh "$CONFIG_SRC"/waybar/scripts/*.sh |
| 149 | |
| 150 | info "Creating folders" |
| 151 | local pics |
| 152 | pics=$(xdg-user-dir PICTURES 2>/dev/null || echo "$HOME/Pictures") |
| 153 | run mkdir -p "$pics/Screenshots" "$pics/Wallpapers" |
| 154 | ok "${pics/#$HOME/\~}/Screenshots, ${pics/#$HOME/\~}/Wallpapers" |
| 155 | |
| 156 | if [[ -n ${SWAYSOCK:-} ]] && has swaymsg; then |
| 157 | info "Reloading sway" |
| 158 | run swaymsg reload >/dev/null && ok "Reloaded" |
| 159 | fi |
| 160 | } |
| 161 | |
| 162 | uninstall() { |
| 163 | info "Removing symlinks" |
| 164 | for m in "${MODULES[@]}"; do |
| 165 | local dst="$CONFIG_DST/$m" |
| 166 | if [[ -L $dst && $(readlink -f "$dst") == "$CONFIG_SRC/$m" ]]; then |
| 167 | run rm "$dst"; ok "removed $m" |
| 168 | local latest |
| 169 | latest=$(find "$BACKUP_ROOT" -mindepth 2 -maxdepth 2 -name "$m" 2>/dev/null | sort | tail -n1) |
| 170 | if [[ -n $latest ]]; then |
| 171 | run mv "$latest" "$dst"; ok "restored $m from ${latest/#$HOME/\~}" |
| 172 | fi |
| 173 | fi |
| 174 | done |
| 175 | } |
| 176 | |
| 177 | |
| 178 | check() { |
| 179 | info "Checking programs" |
| 180 | local req=(sway swaybg swayidle swaylock waybar wofi foot mako makoctl grim slurp |
| 181 | wl-copy wl-paste jq notify-send brightnessctl playerctl wpctl curl) |
| 182 | local opt=(cliphist swappy wtype magick gammastep wlogout kitty nm-applet |
| 183 | blueman-applet pavucontrol btop lxpolkit) |
| 184 | local miss=0 |
| 185 | for c in "${req[@]}"; do |
| 186 | if has "$c"; then ok "$c"; else err "$c ${D}(required)${N}"; miss=1; fi |
| 187 | done |
| 188 | for c in "${opt[@]}"; do |
| 189 | if has "$c"; then ok "$c"; else warn "$c ${D}(optional)${N}"; fi |
| 190 | done |
| 191 | if fc-list 2>/dev/null | grep -qi "JetBrainsMono Nerd"; then ok "JetBrainsMono Nerd Font" |
| 192 | else warn "JetBrainsMono Nerd Font ${D}(run ./install.sh --fonts)${N}"; fi |
| 193 | (( miss )) && warn "Run ${B}./install.sh --packages${N} to install what's missing." |
| 194 | return 0 |
| 195 | } |
| 196 | |
| 197 | |
| 198 | while (( $# )); do |
| 199 | case $1 in |
| 200 | -a|--all) DO_PKGS=1 DO_FONTS=1 DO_LINK=1 ;; |
| 201 | -p|--packages) DO_PKGS=1 ;; |
| 202 | -f|--fonts) DO_FONTS=1 ;; |
| 203 | -l|--link) DO_LINK=1 ;; |
| 204 | -c|--check) DO_CHECK=1 ;; |
| 205 | -u|--uninstall) DO_UNINSTALL=1 ;; |
| 206 | -n|--dry-run) DRY=1 ;; |
| 207 | -y|--yes) YES=1 ;; |
| 208 | -h|--help) banner; usage; exit 0 ;; |
| 209 | -*) err "Unknown option: $1"; usage; exit 1 ;; |
| 210 | *) MODULES+=("$1") ;; |
| 211 | esac |
| 212 | shift |
| 213 | done |
| 214 | |
| 215 | (( DO_PKGS + DO_FONTS + DO_LINK + DO_CHECK + DO_UNINSTALL )) || DO_LINK=1 |
| 216 | (( ${#MODULES[@]} )) || mapfile -t MODULES < <(available_modules) |
| 217 | |
| 218 | banner |
| 219 | (( DRY )) && warn "Dry run โ nothing will be changed" |
| 220 | (( EUID == 0 )) && { err "Don't run this as root; it will ask for sudo when needed."; exit 1; } |
| 221 | |
| 222 | if (( DO_UNINSTALL )); then uninstall; exit 0; fi |
| 223 | (( DO_PKGS )) && install_packages |
| 224 | (( DO_FONTS )) && install_fonts |
| 225 | (( DO_LINK )) && link_all |
| 226 | (( DO_CHECK )) && check |
| 227 | |
| 228 | echo |
| 229 | info "${G}Done!${N}" |
| 230 | if (( DO_LINK )); then |
| 231 | info "Log out and pick ${B}Sway${N} at the login screen (or type ${B}sway${N} in a TTY)." |
| 232 | info "Press ${B}Super + /${N} inside sway for the keybinding cheat sheet." |
| 233 | fi |