#!/usr/bin/env bash # # Dotfiles manager and install helper # (c) Ismo Vuorinen 2022 # Licensed under MIT, see LICENSE # # vim: ft=bash ts=2 sw=2 et # shellcheck source-path=$HOME/.dotfiles/local/bin # # Helper variables, override with ENVs like `VERBOSE=1 dfm help` : "${VERBOSE:=0}" : "${DOTFILES:=$HOME/.dotfiles}" : "${BREWFILE:=$DOTFILES/config/homebrew/Brewfile}" : "${HOSTFILES:=$DOTFILES/hosts}" SCRIPT=$(basename "$0") # Detect the current shell CURRENT_SHELL=$(ps -p $$ -ocomm= | awk -F/ '{print $NF}') # Function to source files based on the shell source_file() { local file=$1 case "$CURRENT_SHELL" in fish) if [[ -f "$file.fish" ]]; then source "$file.fish" else echo "Fish shell file not found: $file.fish" exit 1 fi ;; sh | bash | zsh) source "$file" ;; *) echo "Unsupported shell: $CURRENT_SHELL" exit 1 ;; esac } # Modify the source commands to use the new function source_file "$DOTFILES/config/shared.sh" source_file "${DOTFILES}/local/bin/msgr" # Menu builder menu_builder() { local title=$1 local commands=("${@:2}") local width=60 printf "\n%s\n" "$(printf '%.s─' $(seq 1 $width))" printf "%-${width}s\n" " $title" printf "%s\n" "$(printf '%.s─' $(seq 1 $width))" for cmd in "${commands[@]}"; do local name=${cmd%%:*} local desc=${cmd#*:} printf " %-20s %s\n" "$name" "$desc" done } section_install() { USAGE_PREFIX="$SCRIPT install " MENU=( "all:Installs everything in the correct order" "cargo:Install rust/cargo packages" "cheat-databases:Install cheat external cheatsheet databases" "composer:Install composer" "fonts:Install programming fonts" "gh:Install GitHub CLI Extensions" "go:Install Go Packages" "imagick:Install ImageMagick CLI" "macos:Setup nice macOS defaults" "npm-packages:Install NPM Packages" "ntfy:Install ntfy" "nvm-latest:Install latest lts node using nvm" "nvm:Install Node Version Manager (nvm)" "z:Install z" ) case "$1" in all) msgr msg "Starting to install all and reloading configurations..." $0 install macos $0 install fonts $0 brew install $0 install cargo $0 install go $0 install composer $0 install cheat-databases $0 install nvm $0 install npm-packages $0 install z msgr msg "Reloading configurations again..." source "$DOTFILES/config/shared.sh" msgr yay "All done!" ;; cargo) msgr run "Installing cargo packages..." bash "$DOTFILES/scripts/install-cargo-packages.sh" \ && msgr yay "cargo packages installed!" ;; cheat-databases) msgr run "Installing cheat databases..." for database in "$DOTFILES"/scripts/install-cheat-*; do bash "$database" \ && msgr run_done "Cheat: $database run" done ;; composer) msgr run "Installing composer..." bash "$DOTFILES/scripts/install-composer.sh" \ && msgr run_done "composer installed!" ;; fonts) msgr run "Installing fonts..." bash "$DOTFILES/scripts/install-fonts.sh" \ && msgr yay "Installed fonts!" ;; gh) msgr run "Installing GitHub CLI Extensions..." bash "$DOTFILES/scripts/install-gh-extensions.sh" \ && msgr yay "github cli extensions installed!" ;; go) msgr run "Installing Go Packages..." bash "$DOTFILES/scripts/install-go-packages.sh" \ && msgr yay "go packages installed!" ;; imagick) msgr run "Downloading and installing ImageMagick CLI..." curl -L https://imagemagick.org/archive/binaries/magick > "$XDG_BIN_HOME/magick" \ && chmod +x "$XDG_BIN_HOME/magick" \ && msgr yay "imagick downloaded and installed!" ;; macos) msgr run "Setting up macOS defaults..." bash "$DOTFILES/scripts/install-macos-defaults.sh" \ && msgr yay "macOS defaults set!" ;; nvm) msgr run "Installing nvm..." local NVM_VERSION NVM_VERSION=$(x-gh-get-latest-version nvm-sh/nvm) msgr ok "Latest nvm version: $NVM_VERSION" local NVM_INSTALL="https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" local NVM_CURL="curl -o- \"$NVM_INSTALL\" | bash" PROFILE=/dev/null bash -c "$NVM_CURL" $0 install nvm-latest msgr yay "nvm installed!" ;; nvm-latest) msgr run "Installing latest lts node..." if [ -n "$NVM_DIR" ]; then [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" fi nvm install --lts --latest-npm --default git checkout "$DOTFILES/base/zshrc" git checkout "$DOTFILES/base/bashrc" msgr yay "latest lts node installed!" ;; npm-packages) msgr run "NPM Packages install started..." bash "$DOTFILES/scripts/install-npm-packages.sh" \ && msgr yay "NPM Packages have been installed!" ;; z) msgr run "Installing z..." bash "$DOTFILES/scripts/install-z.sh" \ && msgr yay "z has been installed!" ;; *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;; esac } section_brew() { USAGE_PREFIX="$SCRIPT brew " MENU=( "install:Installs items defined in Brewfile" "update:Updates and upgrades brew packages" "updatebundle:Updates Brewfile with descriptions" "autoupdate:Setups brew auto-update and runs it immediately" "leaves:List brew leaves (installed on request)" "clean:Clean up brew packages" "untracked:List untracked brew packages" ) x-have brew && { case "$1" in install) brew bundle install --file="$BREWFILE" --force --quiet && msgr yay "Done!" ;; update) brew update && brew outdated && brew upgrade && brew cleanup msgr yay "Done!" ;; updatebundle) # Updates .dotfiles/homebrew/Brewfile with descriptions brew bundle dump \ --force \ --file="$BREWFILE" \ --cleanup \ --tap \ --formula \ --cask \ --describe && msgr yay "Done!" ;; leaves) brew leaves --installed-on-request ;; untracked) declare -a BREW_LIST_ALL while IFS= read -r line; do BREW_LIST_ALL+=("$line") done < <(brew list --formula --installed-on-request -1 --full-name) while IFS= read -r c; do BREW_LIST_ALL+=("$c") done < <(brew list --cask -1 --full-name) # Remove entries that are installed as dependencies declare -a BREW_LIST_DEPENDENCIES while IFS= read -r l; do BREW_LIST_DEPENDENCIES+=("$l") done < <(brew list -1 --installed-as-dependency) declare -a BREW_LIST_BUNDLED while IFS= read -r b; do BREW_LIST_BUNDLED+=("$b") done < <(brew bundle list --all --file="$BREWFILE") declare -a BREW_LIST_TRACKED_WITHOUT_DEPS for f in "${BREW_LIST_ALL[@]}"; do # shellcheck disable=SC2199 if [[ " ${BREW_LIST_DEPENDENCIES[@]} " != *" ${f} "* ]]; then BREW_LIST_TRACKED_WITHOUT_DEPS+=("$f") fi done array_diff BREW_LIST_UNTRACKED BREW_LIST_TRACKED_WITHOUT_DEPS BREW_LIST_BUNDLED # If there are no untracked packages, exit if [ ${#BREW_LIST_UNTRACKED[@]} -eq 0 ]; then msgr yay "No untracked packages found!" exit 0 fi echo "Untracked:" for f in "${BREW_LIST_UNTRACKED[@]}"; do echo " $f" done ;; autoupdate) brew autoupdate delete brew autoupdate start 43200 --upgrade --cleanup --immediate ;; clean) brew bundle cleanup --file="$BREWFILE" && msgr yay "Done!" ;; *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;; esac } ! x-have brew && menu_builder "$USAGE_PREFIX" "brew not available on this system" } section_helpers() { USAGE_PREFIX="$SCRIPT helpers " MENU=( "aliases: (bash, zsh, fish) Show aliases" "colors:Show colors" "env:Show environment variables" "functions:Show functions" "aerospace:Show aerospace keybindings" "nvim:Show nvim keybindings" 'path:Show $PATH dir by dir' "tmux:Show tmux keybindings" "wezterm:Show wezterm keybindings" ) CMD="$1" shift SECTION="$1" shift case "$CMD" in path) # shellcheck disable=2001 for i in $(echo "$PATH" | sed 's/:/ /g'); do echo "$i"; done ;; aliases) case "$SECTION" in "zsh") zsh -ixc : 2>&1 | grep -E '> alias' | sed "s|$HOME|~|" | grep -v "(eval)" ;; "bash") bash -ixc : 2>&1 | grep -E '> alias' | sed "s|$HOME|~|" | grep -v "(eval)" ;; "fish") fish -ic "alias" | sed "s|$HOME|~|" ;; *) echo "$SCRIPT helpers aliases (bash, zsh, fish)" ;; esac ;; "colors") max=255 start=0 while [ "$start" -le "$max" ]; do for i in $(seq "$start" $((start + 9))); do if [ "$i" -le "$max" ]; then # Outputs colored number # printf " \e[38;5;%sm%4s\e[0m" "$i" "$i" # Outputs colored block with number inside # printf " \e[48;5;%sm\e[38;5;15m%5s \e[0m" "$i" "$i" # Outputs colored block and color number # printf " \e[48;5;%sm \e[0m %3d" "$i" "$i" # Outputs color number and colored block printf "%3d \e[48;5;%sm \e[0m " "$i" "$i" fi done printf "\n" start=$((start + 10)) done ;; "env") env | sort ;; "functions") declare -F ;; "aerospace") cat "$DOTFILES/docs/aerospace-keybindings.md" ;; "nvim") cat "$DOTFILES/docs/nvim-keybindings.md" ;; "tmux") cat "$DOTFILES/docs/tmux-keybindings.md" ;; "wezterm") cat "$DOTFILES/docs/wezterm-keybindings.md" ;; *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;; esac } section_apt() { USAGE_PREFIX="$SCRIPT apt " MENU=( "upkeep:Run update, upgrade, autoremove and clean" 'install:Install packages from $DOTFILES/tools/apt.txt' "update:Update apt packages" "upgrade:Upgrade apt packages" "autoremove:Remove unused apt packages" "clean:Clean apt cache" ) x-have apt && { case "$1" in upkeep) sudo apt update \ && sudo apt upgrade -y \ && sudo apt autoremove -y \ && sudo apt clean ;; install) # if apt.txt is not found, exit [ ! -f "$DOTFILES/tools/apt.txt" ] && msgr err "apt.txt not found" && exit 0 # Load apt.txt, remove comments (even if trailing comment) and empty lines. # # Ignoring "Quote this to prevent word splitting." # shellcheck disable=SC2046 sudo apt install \ -y $( grep -vE '^\s*#' "$DOTFILES/tools/apt.txt" \ | sed -e 's/#.*//' \ | tr '\n' ' ' ) # If there's a apt.txt file under hosts/$hostname/apt.txt, # run install on those lines too. HOSTNAME=$(hostname -s) HOST_APT="$DOTFILES/hosts/$HOSTNAME/apt.txt" [[ -f $HOST_APT ]] && { # shellcheck disable=SC2046 sudo apt install -y $( grep -vE '^\s*#' "$HOST_APT" \ | sed -e 's/#.*//' \ | tr '\n' ' ' ) } # Try this for an alternative way to install packages # xargs -a <(awk '! /^ *(#|$)/' "$packagelist") -r -- sudo apt-get install -y ;; update) sudo apt update ;; upgrade) sudo apt upgrade -y ;; autoremove) sudo apt autoremove -y ;; clean) sudo apt clean ;; *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;; esac } ! x-have apt && menu_builder "$USAGE_PREFIX" "apt not available on this system" } section_docs() { USAGE_PREFIX="$SCRIPT docs " MENU=( "all:Update all keybindings documentations" "aerospace:Update aerospace keybindings documentation" "tmux:Update tmux keybindings documentation" "nvim:Update nvim keybindings documentation" "wezterm:Update wezterm keybindings documentation" ) case "$1" in all) $0 docs aerospace $0 docs tmux $0 docs nvim $0 docs wezterm ;; aerospace) bash "$DOTFILES/scripts/create-aerospace-keymaps.php" ;; tmux) bash "$DOTFILES/local/bin/x-dfm-docs-xterm-keybindings" ;; nvim) bash "$DOTFILES/scripts/create-nvim-keymaps.sh" ;; wezterm) bash "$DOTFILES/scripts/create-wezterm-keymaps.sh" ;; *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;; esac } section_dotfiles() { USAGE_PREFIX="$SCRIPT dotfiles " MENU=( "fmt:Run all formatters" "yamlfmt:Run yamlfmt to all dotfiles, which are in our control" "shfmt:Run shfmt to all dotfiles" "reset_all:Reset everything, runs all configured reset actions" "reset_nvim:Resets nvim. Deletes caches, removes nvim folders and relinks nvim folders" ) case "$1" in fmt) msgr run "Running all formatters" $0 dotfiles yamlfmt $0 dotfiles shfmt msgr run_done "...done!" ;; reset_all) msgr ok "Running all reset commands" $0 dotfiles reset_nvim ;; reset_nvim) msgr run "Cleaning nvim state, cache and config" rm -rf \ ~/.local/share/nvim \ ~/.local/state/nvim \ ~/.cache/nvim \ ~/.config/nvim msgr ok "Deleted old nvim files (share, state and cache + config)" ln -s "$DOTFILES/config/nvim" ~/.config/nvim msgr ok "Linked nvim and astronvim" x-have npm && $0 install npm msgr ok "Installed packages" msgr run_done "nvim reset!" ;; yamlfmt) # format yaml files x-have yamlfmt && yamlfmt -conf "$DOTFILES/.yamlfmt" ! x-have yamlfmt && msgr err "yamlfmt not found" ;; shfmt) # If system doesn't have fd or shfmt installed, exit ! x-have fd && msgr err "fd not found, install it to continue" ! x-have shfmt && msgr err "shfmt not found, install it to continue" # Format shell scripts according to following rules. fd --full-path "$DOTFILES" -tx \ --hidden \ -E '*.pl' -E '*.php' -E '*.py' -E '*.zsh' -E 'plugins' -E 'fzf' -E 'dotbot' \ -E 'test' -E '**/tldr/*' \ -x shfmt \ --language-dialect bash \ --func-next-line --list --write \ --indent 2 --case-indent --space-redirects \ --binary-next-line {} \; msgr yay "dotfiles have been shfmt formatted!" ;; *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;; esac } section_check() { USAGE_PREFIX="$SCRIPT check " X_HOSTNAME=$(hostname) X_ARCH=$(uname) MENU=( "arch :Empty returns current. Exit code 0=match to current, 1=no match." "host :Empty returns current. Exit code 0=match to current, 1=no match." ) case "$1" in a | arch) [[ $2 == "" ]] && echo "$X_ARCH" && exit 0 [[ $X_ARCH == "$2" ]] && exit 0 || exit 1 ;; h | host | hostname) [[ $2 == "" ]] && echo "$X_HOSTNAME" && exit 0 [[ $X_HOSTNAME == "$2" ]] && exit 0 || exit 1 ;; *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;; esac } section_scripts() { USAGE_PREFIX="$SCRIPT scripts " # Get description from a file get_script_description() { local file local desc file="$1" desc=$(sed -n '/@description/s/.*@description *\(.*\)/\1/p' "$file" | head -1) echo "${desc:-No description available}" } # Collect scripts and their descriptions declare -A SCRIPT_MENU for script in "$DOTFILES/scripts/install-"*.sh; do if [ -f "$script" ]; then name=$(basename "$script" .sh | sed 's/install-//') desc=$(get_script_description "$script") SCRIPT_MENU[$name]="$desc" fi done case "$1" in "") # Show the menu local menu_items=() for name in "${!SCRIPT_MENU[@]}"; do menu_items+=("$name:${SCRIPT_MENU[$name]}") done menu_builder "$USAGE_PREFIX" "${menu_items[@]}" ;; *) # Run the chosen script script_path="$DOTFILES/scripts/install-$1.sh" if [ -f "$script_path" ]; then bash "$script_path" else msgr err "Script not found: $1" fi ;; esac } # Secret menu for visual tests section_tests() { USAGE_PREFIX="$SCRIPT tests " MENU=( "msgr:List all available msgr message types" "params:List all parameters" ) case "$1" in params) echo "All parameters:" for i in "$@"; do echo " $i" done ;; msg) # shellcheck disable=SC1010 msgr done "msgr done" msgr done_suffix "msgr done_suffix" msgr err "msgr err" msgr nested "msgr nested" msgr nested_done "msgr nested_done" msgr ok "msgr ok" msgr prompt "msgr prompt" msgr prompt_done "msgr prompt_done" msgr run "msgr run" "second_param" msgr run_done "msgr run_done" "second_param" msgr warn "msgr warn" msgr yay "msgr yay" msgr yay_done "msgr yay_done" ;; *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;; esac } usage() { echo "" msgr prompt "Usage: $SCRIPT
" echo $" Empty prints
help." echo "" section_install echo "" section_apt echo "" section_brew echo "" section_check echo "" section_dotfiles echo "" section_docs echo "" section_scripts echo "" section_helpers } main() { SECTION="$1" shift # The main loop. The first keyword after $0 triggers section, or help. case "$SECTION" in install) section_install "$@" ;; apt) section_apt "$@" ;; brew) section_brew "$@" ;; check) section_check "$@" ;; dotfiles) section_dotfiles "$@" ;; helpers) section_helpers "$@" ;; docs) section_docs "$@" ;; scripts) section_scripts "$@" ;; tests) section_tests "$@" ;; *) usage && exit 0 ;; esac } main "$@"