Files
dotfiles/local/bin/dfm

773 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Dotfiles manager and install helper
# (c) Ismo Vuorinen <https://github.com/ivuorinen> 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()
{
# shellcheck disable=SC2317
echo "(!) ERROR: $1" >&2
# shellcheck disable=SC2317
exit 1
}
fi
if ! declare -f msg_done > /dev/null; then
# Function to print done message
# $1 - message (string)
msg_done()
{
# shellcheck disable=SC2317
echo "$1"
# shellcheck disable=SC2317
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()
{
# shellcheck disable=SC1083,SC2086
eval local ARR1=\(\"\${$2[@]}\"\)
# shellcheck disable=SC1083,SC2086
eval local ARR2=\(\"\${$3[@]}\"\)
local IFS=$'\n'
mapfile -t "$1" < <(comm -23 <(echo "${ARR1[*]}" | sort) <(echo "${ARR2[*]}" | sort))
}
fi
# 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 <command>"
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..."
$0 asdf plugins-add \
&& 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 <command>"
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
# 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
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 <command>"
MENU=(
"current:Show asdf current versions"
"global:Show asdf global versions"
"installed:Show asdf installed versions"
"local:Show asdf local versions"
"plugins-add:Add and update direnv and asdf-plugin-manager, and all other plugins"
"plugins-update:Update all asdf plugins"
"reset:Reset asdf plugins"
"versions:Show asdf versions"
"where:Show asdf where"
"which:Show asdf which"
)
case "$1" in
plugins-update)
APM_BIN="$(asdf where asdf-plugin-manager)/bin/asdf-plugin-manager"
msgr run "Updating all asdf plugins"
$APM_BIN update-all
$APM_BIN export > "$ASDF_PLUGIN_MANAGER_PLUGIN_VERSIONS_FILENAME"
msgr run_done "Updated all plugins"
;;
plugins-add)
X_GH_BIN="$DOTFILES/local/bin/x-gh-get-latest-version"
LATEST_APM="$($X_GH_BIN asdf-community/asdf-plugin-manager | sed 's/^v//')"
LATEST_DIRENV="$($X_GH_BIN asdf-community/asdf-direnv)"
PLUGIN_VERSIONS="$DOTFILES/config/asdf/plugin-versions"
APM_BIN="$HOME/.local/bin/asdf/shims/asdf-plugin-manager"
msgr run "Adding and updating direnv and asdf-plugin-manager"
asdf plugin add direnv https://github.com/asdf-community/asdf-direnv.git
asdf global direnv "$LATEST_DIRENV"
asdf install direnv "$LATEST_DIRENV"
asdf plugin add asdf-plugin-manager https://github.com/asdf-community/asdf-plugin-manager.git
asdf global asdf-plugin-manager "$LATEST_APM"
asdf install asdf-plugin-manager "$LATEST_APM"
asdf reshim
msgr run_done "direnv and asdf-plugin-manager added and updated"
msgr run "Reset plugin-versions file to the original using git"
if git ls-files --error-unmatch "$PLUGIN_VERSIONS" > /dev/null 2>&1; then
git checkout -- "$PLUGIN_VERSIONS"
fi
msgr run_done "Reset plugin-versions file"
msgr run "Adding all plugins with asdf-plugin-manager"
"$APM_BIN" add-all
msgr run_done "Added all plugins with asdf-plugin-manager"
msgr run "Install all plugins"
PLUGINS=$($APM_BIN list | awk -F ' ' '{print $1}' | sort)
for P in $PLUGINS; do
msgr run "Installing $P"
asdf install "$P" latest
asdf global "$P" latest
done
msgr run_done "Installed all plugins"
msgr run "Reshimming"
asdf reshim
msgr run_done "Reshimmed"
;;
plugins-remove)
PLUGIN_VERSIONS="$DOTFILES/config/asdf/plugin-versions"
msgr run "Remove installed plugins"
INSTALLED_ASDF_PLUGINS=$(asdf list | grep -vE "direnv|asdf-plugin-manager" | grep -v "^ ")
for P in $INSTALLED_ASDF_PLUGINS; do
asdf plugin remove "$P"
msgr nested_done "Removed $P"
done
msgr run "Reset plugin-versions file to the original using git"
if git ls-files --error-unmatch "$PLUGIN_VERSIONS" > /dev/null 2>&1; then
git checkout -- "$PLUGIN_VERSIONS"
fi
msgr run_done "Reset plugin-versions file"
msgr run_done "Remove plugins done!"
;;
reset)
APM_BIN="$(asdf where asdf-plugin-manager)/bin/asdf-plugin-manager"
msgr run "Get currently installed plugins, remove those that are not defined"
$0 asdf plugins-remove
$0 asdf plugins-add
$0 asdf fixtoolversions
asdf reshim
msgr yay "Reset asdf plugins done!"
;;
fixtoolversions)
ASDF_TOOL_VERSIONS_FILE="$DOTFILES/base/tool-versions"
ASDF_TOOL_FILE_PLUGINS=$(awk '{print $1 " " $2}' "$ASDF_TOOL_VERSIONS_FILE")
APM_BIN="$(asdf where asdf-plugin-manager)/bin/asdf-plugin-manager"
msgr run "Loading $ASDF_TOOL_VERSIONS_FILE and collecting installed"
ASDF_PLUGINS_DEFINED=$($APM_BIN list | awk -F ' ' '{print $1}')
echo "$ASDF_TOOL_FILE_PLUGINS" | \
grep -Fxv -f <(echo "$ASDF_PLUGINS_DEFINED") > tmp && \
mv tmp "$ASDF_TOOL_VERSIONS_FILE"
msgr run_done "Fixed $ASDF_TOOL_VERSIONS_FILE"
;;
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 <command>"
MENU=(
"aliases:<shell> (bash, zsh) Show aliases for bash or zsh"
"colors:Show colors"
"env:Show environment variables"
"functions:Show functions"
"nvim:Show nvim keybindings"
# shellcheck disable=SC2016
'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 <shell> (bash, zsh)"
;;
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
;;
"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 <command>"
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 <command>"
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 <command>"
X_HOSTNAME=$(hostname)
X_ARCH=$(uname)
MENU=(
"arch <arch>:Empty <arch> returns current. Exit code 0=match to current, 1=no match."
"host <host>:Empty <host> 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 <command>"
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 <section> <command>"
echo $" Empty <command> prints <section> 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 "$@"