Files
dotfiles/local/bin/dfm
Ismo Vuorinen 959e7c418e chore: remove 15 unused config directories and stale references
Drop config folders (aerospace, aqua, asdf, direnv, flipperdevices,
ghostty, htop, misc, nano, task, tealdeer, tms, wtf, yamlfmt,
yamllint) along with starship.toml, nbrc, and aerospace scripts/docs.

Clean up references in dfm, _dfm completions, bashrc, exports, and
exports-lakka to match.
2026-02-04 08:05:37 +02:00

675 lines
18 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
#
# 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
# shellcheck disable=SC1090
source "$file.fish"
else
echo "Fish shell file not found: $file.fish"
exit 1
fi
;;
sh | bash | zsh)
# shellcheck disable=SC1090
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 <command>"
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 <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 && 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 <command>"
MENU=(
"aliases:<shell> (bash, zsh, fish) Show aliases"
"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)"
;;
"fish")
fish -ic "alias" | sed "s|$HOME|~|"
;;
*)
echo "$SCRIPT helpers aliases <shell> (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 ;;
"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 <command>"
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 <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_builder "$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)
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 <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_builder "$USAGE_PREFIX" "${MENU[@]}" ;;
esac
}
section_scripts()
{
USAGE_PREFIX="$SCRIPT scripts <command>"
# 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 <command>"
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 <section> <command>"
echo $" Empty <command> prints <section> 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 "$@"