#!/usr/bin/env bash # # Dotfiles manager and install helper # (c) Ismo Vuorinen 2022 # Licensed under MIT, see LICENSE # # 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") if ! declare -f msg > /dev/null; then # Function to print messages if VERBOSE is enabled # $1 - message (string) msg() { [ "$VERBOSE" -eq 1 ] && echo "$1" return 0 } fi if ! declare -f msg_err > /dev/null; then # Function to print error messages and exit # $1 - error message (string) msg_err() { echo "(!) ERROR: $1" >&2 exit 1 } fi if ! declare -f msg_done > /dev/null; then # Function to print done message # $1 - message (string) msg_done() { echo "✓ $1" return 0 } fi if ! declare -f array_diff > /dev/null; then # Function to compare two arrays and return the difference # Example: array_diff DIFFERENCE ARRAY1 ARRAY2 # $1 - variable to store the difference # $2 - first array # $3 - second array # Output to $1 the difference between $2 and $3 # Source: https://stackoverflow.com/a/42399479/594940 array_diff() { eval local ARR1=\(\"\${$2[@]}\"\) eval local ARR2=\(\"\${$3[@]}\"\) local IFS=$'\n' mapfile -t "$1" < <(comm -23 <(echo "${ARR1[*]}" | sort) <(echo "${ARR2[*]}" | sort)) } fi VERSION_NVM="v0.39.5" # Loads configs for better installation experience source "$DOTFILES/config/shared.sh" source "${DOTFILES}/local/bin/msgr" # -- Menu builder -- # menu_section() { LINE=$(printf '%-18s [ %-15s ]\n' "$1" "$2") echo -e " $(__log_marker) $LINE" } menu_item() { LINE=$(printf '%-15s %-30s\n' "$1" "$2") echo -e "$(__log_indent)$(__log_marker) $LINE" } # Takes a bash array ("cow:moo", "dinosaur:roar") and loops # through the keys to build menu section listing. menu_usage_header() { MENU_CMD="$1" shift MENU_ARRAY=("$@") KEYS="" for item in "${MENU_ARRAY[@]}"; do CMD=$(echo "${item}" | awk -F ":" '{print $1}') KEYS+="${CMD} | " done # "???" removes 3 last characters, being " | " from the end menu_section "$MENU_CMD" "${KEYS%???}" } # Takes the usage command "$0 dotfiles" and a # bash array ("cow:moo" "dinosaur:roar") and loops # through in building a menu for dfm command usage listing. menu_usage() { MENU_CMD="$1" shift MENU_ARRAY=("$@") msg "$MENU_CMD" for item in "${MENU_ARRAY[@]}"; do CMD=$(echo "${item}" | awk -F ":" '{print $1}') DESC=$(echo "${item}" | awk -F ":" '{print $2}') menu_item "$CMD" "$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" "dotenv:Install dotenv-linter" "fonts:Install programming fonts" "gh:Install GitHub CLI Extensions" "go:Install Go Packages" "imagick:Install ImageMagick CLI" "starship:Install starship.rs" "macos:Setup nice macOS defaults" "neofetch:Install neofetch" "nvm:Install Node Version Manager (nvm)" "nvm-latest:Install latest lts node using nvm" "npm:Install NPM Packages" "ntfy:Install ntfy" "ohmybash:Install oh-my-bash" "pip:Install pip/python packages" "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 ohmyposh $0 install asdf $0 install composer $0 install fzf $0 install go $0 install cheat-databases $0 install imagick $0 install nvm $0 install npm # $0 install ntfy $0 install z msgr msg "Reloading configurations again..." source "$DOTFILES/config/shared.sh" msgr yay "All done!" ;; asdf) msg "Installing asdf..." bash "$DOTFILES/scripts/install-asdf.sh both" \ && msg_yay "asdf installed!" ;; cargo) msg "Installing cargo packages..." bash "$DOTFILES/scripts/install-cargo-packages.sh" \ && msg_yay "cargo packages installed!" ;; cheat-databases) msg "Installing cheat databases..." for database in "$DOTFILES"/scripts/install-cheat-*; do bash "$database" \ && msg_yay "Cheat: $database run" done ;; composer) msg "Installing composer..." bash "$DOTFILES/scripts/install-composer.sh" \ && msg_yay "composer installed!" ;; fonts) msg "Installing fonts..." bash "$DOTFILES/scripts/install-fonts.sh" \ && msg_yay "Installed fonts!" ;; fzf) msg "Installing fzf..." bash "$DOTFILES/scripts/install-fzf.sh" \ && msg_yay "fzf installed!" ;; gh) msg "Installing GitHub CLI Extensions..." bash "$DOTFILES/scripts/install-gh-extensions.sh" \ && msg_yay "github cli extensions installed!" ;; go) msg "Installing Go Packages..." bash "$DOTFILES/scripts/install-go-packages.sh" \ && msg_yay "go packages installed!" ;; imagick) msg "Installing ImageMagick CLI..." curl -L https://imagemagick.org/archive/binaries/magick > "$XDG_BIN_HOME/magick" \ && chmod +x "$XDG_BIN_HOME/magick" \ && msg_yay "imagick installed!" ;; macos) msg "Setting up macOS defaults..." bash "$DOTFILES/scripts/set-macos-defaults.sh" \ && msg_yay "Brewfile defined apps has been installed!" ;; neofetch) msg "Installing neofetch..." bash "$DOTFILES/scripts/install-neofetch.sh" \ && msg_yay "neofetch installed!" ;; nvm) msg "Installing nvm..." NVM_VERSION=$(x-gh-get-latest-version nvm-sh/nvm) msg "Latest nvm version: $NVM_VERSION" NVM_INSTALL="https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" NVM_CURL="curl -o- \"$NVM_INSTALL\" | bash" PROFILE=/dev/null bash -c "$NVM_CURL" $0 install nvm-latest msg_yay "nvm installed!" ;; nvm-latest) msg "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" msg_yay "latest lts node installed!" ;; npm) msg "NPM Packages install started..." bash "$DOTFILES/scripts/install-npm-packages.sh" \ && msg_yay "NPM Packages have been installed!" ;; ntfy) msg "ntfy install started..." bash "$DOTFILES/scripts/install-ntfy.sh" \ && msg_yay "ntfy installed!" ;; ohmyposh) msg "oh-my-posh install started..." bash "$DOTFILES/scripts/install-ohmyposh.sh" \ && msg_yay "oh-my-posh installed!" ;; pip) msg "pip install started..." bash "$DOTFILES/scripts/install-pip-packages.sh" ;; z) msg "Installing z..." bash "$DOTFILES/scripts/install-z.sh" \ && msg_yay "z has been installed!" ;; *) menu_usage "$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 && msg_yay "Done!" ;; update) brew update && brew outdated && brew upgrade && brew cleanup msg_yay "Done!" ;; updatebundle) # Updates .dotfiles/homebrew/Brewfile with descriptions brew bundle dump \ --force \ --file="$BREWFILE" \ --cleanup \ --tap \ --formula \ --cask \ --describe && msg_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 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 msg_yay "No untracked packages found!" exit 0 fi echo "Untracked:" for f in "${BREW_LIST_UNTRACKED[@]}"; do echo " $f" done ;; clean) brew bundle cleanup --file="$BREWFILE" && msg_yay "Done!" ;; autoupdate) brew autoupdate delete brew autoupdate start 43200 --upgrade --cleanup --immediate ;; *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;; esac } ! x-have brew && menu_section "$USAGE_PREFIX" "brew not available on this system" } section_asdf() { USAGE_PREFIX="$SCRIPT asdf " MENU=( "install:Install asdf" "current:Show asdf current versions" "global:Show asdf global versions" "installed:Show asdf installed versions" "local:Show asdf local versions" "versions:Show asdf versions" "where:Show asdf where" "which:Show asdf which" ) case "$1" in install) msg "Installing asdf..." bash "$DOTFILES/scripts/install-asdf.sh both" \ && msg_yay "asdf installed!" ;; current) asdf current ;; global) asdf global ;; installed) asdf list ;; local) asdf local ;; versions) asdf list all ;; where) asdf where ;; which) asdf which ;; *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;; esac } section_helpers() { USAGE_PREFIX="$SCRIPT helpers " MENU=( "aliases: (bash, zsh) Show aliases for bash or zsh" "colors:Show colors" "env:Show environment variables" "functions:Show functions" "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)" ;; *) echo "$SCRIPT helpers aliases (bash, zsh)" ;; esac ;; "colors") for i in {0..255}; do echo -en "\e[38;5;${i}m${i} "; done ;; "env") env | sort ;; "functions") declare -F ;; "nvim") cat "$DOTFILES/docs/nvim-keybindings.md" ;; "tmux") cat "$DOTFILES/docs/tmux-keybindings.md" ;; "wezterm") cat "$DOTFILES/docs/wezterm-keybindings.md" ;; *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;; esac } section_docs() { USAGE_PREFIX="$SCRIPT docs " MENU=( "all:Update all keybindings documentations" "tmux:Update tmux keybindings documentation" "nvim:Update nvim keybindings documentation" "wezterm:Update wezterm keybindings documentation" ) case "$1" in all) $0 docs tmux $0 docs nvim $0 docs wezterm ;; 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_usage "$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) msg_ok "Running all formatters" $0 dotfiles yamlfmt $0 dotfiles shfmt msg_done "...done!" ;; reset_all) msg_ok "Running all reset commands" $0 dotfiles reset_nvim ;; reset_nvim) rm -rf \ ~/.local/share/nvim \ ~/.local/state/nvim \ ~/.cache/nvim \ ~/.config/nvim msg_ok "Deleted old nvim files (share, state and cache + config)" ln -s "$DOTFILES/config/nvim" ~/.config/nvim msg_ok "Linked nvim and astronvim" x-have npm && $0 install npm msg_ok "Installed packages" msg_done "nvim reset!" ;; yamlfmt) # format yaml files x-have yamlfmt && yamlfmt -conf "$DOTFILES/.yamlfmt" ! x-have yamlfmt && msg_err "yamlfmt not found" ;; shfmt) # If system doesn't have fd or shfmt installed, exit ! x-have fd && msg_err "fd not found, install with asdf" ! x-have shfmt && msg_err "shfmt not found, install with asdf" # 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 '**/bin/asdf/**' -E '**/tldr/*' \ -x shfmt \ --language-dialect bash \ --func-next-line --list --write \ --indent 2 --case-indent --space-redirects \ --binary-next-line {} \; msg_yay "dotfiles have been shfmt formatted!" ;; *) menu_usage "$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_usage "$USAGE_PREFIX" "${MENU[@]}" ;; esac } # Secret menu for visual tests section_tests() { USAGE_PREFIX="$SCRIPT tests " MENU=( "msg:List all log functions from shared.sh" "params:List all parameters" ) case "$1" in params) echo "All parameters:" for i in "$@"; do echo " $i" done ;; msg) msg "msg" msg_done "msg_done" msg_done_suffix "msg_done_suffix" msg_err "msg_err" msg_nested "msg_nested" msg_nested_done "msg_nested_done" msg_ok "msg_ok" msg_prompt "msg_prompt" msg_prompt_done "msg_prompt_done" msg_run "msg_run" "second_param" msg_run_done "msg_run_done" "second_param" msg_warn "msg_warn" msg_yay "msg_yay" msg_yay_done "msg_yay_done" ;; *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;; esac } usage() { echo "" msg_prompt "Usage: $SCRIPT
" echo $" Empty prints
help." echo "" section_install echo "" section_asdf echo "" section_brew echo "" section_check echo "" section_dotfiles echo "" section_docs 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 "$@" ;; asdf) section_asdf "$@" ;; brew) section_brew "$@" ;; check) section_check "$@" ;; dotfiles) section_dotfiles "$@" ;; helpers) section_helpers "$@" ;; docs) section_docs "$@" ;; tests) section_tests "$@" ;; *) usage && exit 0 ;; esac } main "$@"