summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-06-10 21:37:23 +0200
committerThomas Vanbesien <tvanbesi@proton.me>2026-06-10 22:25:10 +0200
commit0f84e347c76c48a10cf15c90b97219455bb6f57a (patch)
treef60df275751522b65f7c266584298ce8b2ef4519
parent9a5790ec7bc284e77dfb0a7c76c43509be1dae9a (diff)
downloaddotfiles-0f84e347c76c48a10cf15c90b97219455bb6f57a.tar.gz
dotfiles-0f84e347c76c48a10cf15c90b97219455bb6f57a.zip
refactor(dotfiles): completely rewrite the dotfiles launcher
-rwxr-xr-x.local/bin/dotfiles342
-rw-r--r--.local/share/dotfiles/packages/README.md48
-rw-r--r--.local/share/dotfiles/packages/base.txt49
3 files changed, 189 insertions, 250 deletions
diff --git a/.local/bin/dotfiles b/.local/bin/dotfiles
index 7382a23..9e6bff3 100755
--- a/.local/bin/dotfiles
+++ b/.local/bin/dotfiles
@@ -1,190 +1,226 @@
#!/usr/bin/bash
set -euo pipefail
-prompt_for_install() {
- local label=${1:?label argument missing}
- echo "$label"
- read -rn 1 -p "Install? (y/n)" input
- echo
- [[ $input == y ]]
-}
+declare NVIM_SRC_DIR="$HOME/.local/share/dotfiles/deps/neovim"
+declare NVIM_BUILD_DIR="$HOME/.local/share/nvim"
+declare -a PACKAGES_YAY=(7zip atool base-devel bash bash-completion
+ bash-language-server bat browserpass browserpass-firefox bzip2 cmake cpio
+ ctags docker docker-buildx fd firefox foot fzf git glib2 gzip lhasa
+ lua-language-server lzop marksman ninja pass pigz python-black ranger
+ ripgrep shfmt stylua tar texinfo tig tk tmux tree-sitter-cli unrar unzip
+ vim-language-server wl-clipboard xz yay zip)
+declare -A PACKAGES_PYTHON=(
+ ["basedpyright"]=""
+ ["mdformat"]="mdformat-gfm,mdformat-frontmatter,mdformat-wikilink"
+ ["ruff"]=""
+)
-clipboard_hint() {
- local cmd=${1:?cmd argument missing}
- wl-copy "$cmd"
- echo -e "Copied to clipboard:\n\n$cmd\n"
-}
+################################################################################
+# Arch Linux & AUR packages (yay) ##############################################
+################################################################################
-# sync_packs cmd packages [sync-flag…]
-# cmd → pacman or yay
-# packages → nameref to an array containing a list of packages to sync
-# sync_flag → additional flags to pass to cmd
-sync_packages() {
- case "${1:?cmd argument missing}" in
- pacman | yay) cmd=("$1") ;;
- *)
- echo "usage: sync_packages pacman|yay …"
- exit 1
- ;;
- esac
- local -n packages="${2:?packages argument missing}"
- shift 2
- local sync_args=("$@")
- readarray -t packages_to_install < <(
- comm -13 \
- <(sort <("${cmd[@]}" --query --quiet)) \
- <(sort <(printf '%s\n' "${packages[@]}"))
- )
- readarray -t packages_to_upgrade < <(
- comm -12 \
- <(sort <("${cmd[@]}" --query --upgrades --quiet)) \
- <(sort <(printf '%s\n' "${packages[@]}"))
- )
- local packages_to_sync=("${packages_to_install[@]}" "${packages_to_upgrade[@]}")
- if ((${#packages_to_sync[@]} == 0)); then return; fi
- if prompt_for_install "Out-of-sync packages: ${packages_to_sync[*]}"; then
- if [[ ${cmd[0]} == pacman ]]; then cmd=(sudo pacman); fi
- "${cmd[@]}" --sync "${sync_args[@]}" "${packages_to_sync[@]}"
- fi
+# From the provided package names, print those that are not installed or that
+# have an available upgrade (in yay)
+_get_yay_packages_to_sync() {
+ local packs=("$@")
+ comm -13 \
+ <(sort <(yay --query --quiet)) \
+ <(sort <(printf '%s\n' "${packs[@]}"))
+ comm -12 \
+ <(sort <(yay --query --upgrades --quiet)) \
+ <(sort <(printf '%s\n' "${packs[@]}"))
}
-declare pipx_json
-pipx_json="$(pipx list --json)"
-
-check_pipx_package_installed() {
- jq --exit-status ".venvs | has(\"${1:?}\")" <<<"$pipx_json" >/dev/null 2>&1
+# Sync the provided packages with yay
+# Check if packages actually need to be installed/upgraded before running yay,
+# this is to avoid a password prompt when all packages are up-to-date
+_yay() {
+ local -a packs=("$@") to_sync
+ mapfile -t to_sync < <(_get_yay_packages_to_sync "${packs[@]}")
+ ((${#to_sync[@]} > 0)) || return 0
+ yay --sync --noconfirm "${to_sync[@]}"
}
-check_pipx_injected_package() {
- jq --exit-status ".venvs.${1:?}.metadata.injected_packages | has(\"${2:?}\")" <<<"$pipx_json" >/dev/null 2>&1
+################################################################################
+# Python packages (pipx) #######################################################
+################################################################################
+
+# Print uninstalled (in pipx) packages from the provided list
+_get_uninstalled_pipx_packages() {
+ local json pack
+ json="$(pipx list --json)"
+ for pack; do
+ jq --exit-status --arg p "$pack" \
+ '.venvs | has($p)' \
+ <<<"$json" >/dev/null || echo "$pack"
+ done
}
-get_pipx_package_missing_injected_packages() {
- local base_pack="${1:?missing base_pack argument}"
+# Print missing packages to inject (in pipx) for the given package
+# The first argument is the package to inject into and the remaining arguments
+# are the packages to inject
+_get_uninjected_pipx_packages() {
+ local json to_inject pack="$1"
shift
- local -a packs_to_inject=("$@")
- for pack in "${packs_to_inject[@]}"; do
- if ! check_pipx_injected_package "$base_pack" "$pack"; then echo "$pack"; fi
+ json="$(pipx list --json)"
+ for to_inject; do
+ jq --exit-status --arg p "$pack" --arg t "$to_inject" \
+ '.venvs[$p].metadata.injected_packages | has($t)' \
+ <<<"$json" >/dev/null || echo "$to_inject"
done
}
-sync_python_packages() {
- # map packages to the depencies to inject in their environment
- declare -A deps=(
- [mdformat]="mdformat-gfm,mdformat-frontmatter,mdformat-wikilink"
- [ruff]=""
- [basedpyright]=""
- )
- local missing_packages=()
- for dep in "${!deps[@]}"; do
- if ! check_pipx_package_installed "$dep"; then missing_packages+=("$dep"); fi
- done
- if [[ ${#missing_packages[@]} -gt 0 ]]; then
- if prompt_for_install "Missing pip packages: ${missing_packages[*]}"; then
- pipx install "${missing_packages[@]}"
- fi
- fi
- for dep in "${!deps[@]}"; do
- IFS=',' read -ra packs_to_inject < <(echo "${deps[$dep]}")
- readarray -t missing_injected_packages < <(get_pipx_package_missing_injected_packages "$dep" "${packs_to_inject[@]}")
- if [[ ${#missing_injected_packages[@]} -eq 0 ]]; then return; fi
- if prompt_for_install "Missing $dep extensions: ${missing_injected_packages[*]}"; then
- pipx inject "$dep" "${missing_injected_packages[@]}"
- fi
+# Sync the provided packages with pipx
+# This function takes one nameref argument which must point to an associative
+# array that maps packages to the packages to inject into them
+_pipx() {
+ local -n packs="$1"
+ local -a to_install to_inject tmp
+ local pack
+ mapfile -t to_install < <(_get_uninstalled_pipx_packages "${!packs[@]}")
+ ((${#to_install[@]} == 0)) || pipx install --quiet "${to_install[@]}"
+ for pack in "${!packs[@]}"; do
+ IFS=',' read -ra tmp <<<"${packs[$pack]}"
+ mapfile -t to_inject \
+ < <(_get_uninjected_pipx_packages "$pack" "${tmp[@]}")
+ ((${#to_inject[@]} > 0)) || continue
+ pipx inject --quiet "$pack" "${to_inject[@]}"
done
+ pipx upgrade --quiet --include-injected "${!packs[@]}"
}
-sync_claude() {
- if [[ ! -x ~/.local/bin/claude ]]; then
- curl -fsSL https://claude.ai/install.sh | bash
- fi
+################################################################################
+# Claude Code ##################################################################
+################################################################################
+
+# Install/update Claude Code
+_sync_claude() {
+ if command -v claude >/dev/null; then
+ claude update
+ else curl -fsSL https://claude.ai/install.sh | bash; fi
}
-build_and_install_nvim() {
- local src_dir="$HOME/.local/share/dotfiles/deps/neovim"
- local build_dir="$HOME/.local/share/nvim"
- rm -rf "$src_dir/build/" "$src_dir/.deps/"
+################################################################################
+# Neovim #######################################################################
+################################################################################
+
+# Build and install Neovim
+_deploy_nvim() {
+ echo "Building and installing Neovim…"
+ rm -rf "$NVIM_SRC_DIR/build/" "$NVIM_SRC_DIR/.deps/"
make \
- --directory="$src_dir" \
+ --directory="$NVIM_SRC_DIR" \
CMAKE_BUILD_TYPE=RelWithDebInfo \
- CMAKE_INSTALL_PREFIX="$build_dir"
- make --directory="$src_dir" install
- ln --symbolic --force "$build_dir/bin/nvim" "$HOME/.local/bin/nvim"
- mkdir --parents "$HOME/.local/share/icons/hicolor/128x128/apps"
- ln --symbolic --force "$src_dir/runtime/nvim.png" "$HOME/.local/share/icons/hicolor/128x128/apps/nvim.png"
- mkdir --parents "$HOME/.local/share/man/man1"
- ln --symbolic --force "$build_dir/share/man/man1/nvim.1" "$HOME/.local/share/man/man1/nvim.1"
+ CMAKE_INSTALL_PREFIX="$NVIM_BUILD_DIR" \
+ install
+ local f src dst files=("bin/nvim"
+ "share/icons/hicolor/128x128/apps/nvim.png" "share/man/man1/nvim.1")
+ for f in "${files[@]}"; do
+ src="$NVIM_BUILD_DIR/$f" dst="$HOME/.local/$f"
+ mkdir --parents "$(dirname "$dst")"
+ ln --symbolic --force "$src" "$dst"
+ done
}
-sync_nvim() {
- local nvim_dir="$HOME/.local/share/dotfiles/deps/neovim"
- local git_args=(--git-dir "$nvim_dir/.git" --work-tree "$nvim_dir")
- git "${git_args[@]}" fetch --tags --force >/dev/null
+# Fetches tags from the nvim git submodule
+# If a newer version is available, then rebuild and install
+_sync_nvim() {
+ local git_args=(--git-dir "$NVIM_SRC_DIR/.git" --work-tree "$NVIM_SRC_DIR")
local current_version latest_version
+ git "${git_args[@]}" fetch --tags --force >/dev/null
if ! command -v nvim >/dev/null; then
- if prompt_for_install "Missing nvim"; then build_and_install_nvim; fi
+ _deploy_nvim
return
fi
- current_version="$(command nvim --version | head -1 | grep -E --only-matching "v[0-9]+\.[0-9]+\.[0-9]+")"
- latest_version="$(git "${git_args[@]}" tag --list "v*" --sort=-version:refname | head -1)"
- if [[ $current_version == "$latest_version" ]]; then return; fi
- if prompt_for_install "New nvim version $current_version → $latest_version"; then
- git "${git_args[@]}" checkout "$latest_version"
- build_and_install_nvim
- fi
+ current_version="$(command nvim --version | head -1 |
+ grep -E --only-matching "v[0-9]+\.[0-9]+\.[0-9]+")"
+ latest_version="$(git "${git_args[@]}" tag --list "v*" \
+ --sort=-version:refname | head -1)"
+ [[ $current_version != "$latest_version" ]] ||
+ { echo "Already at latest version $latest_version" && return; }
+ git "${git_args[@]}" checkout "$latest_version"
+ _deploy_nvim
}
-reinstall_nvim() {
- if prompt_for_install "Rebuild nvim from source"; then
- build_and_install_nvim
+################################################################################
+# VPN & Secret Service #########################################################
+################################################################################
+
+# Install pass-secret-service-bin (Secret Service provider)
+# Shows a hint if another package owns the dbus file
+# org.freedesktop.secrets.service
+_sync_secret_service() {
+ local ss_owner \
+ ss_file=/usr/share/dbus-1/services/org.freedesktop.secrets.service
+ ss_owner=$(yay --query --owns --quiet "$ss_file" 2>/dev/null) || true
+ [[ $ss_owner == "pass-secret-service-bin" ]] && return 0
+ if [[ -n $ss_owner ]]; then
+ echo -e "$ss_owner owns $ss_file (should be pass-secret-service-bin)" \
+ "\nPlease uninstall $ss_owner and try again"
+ _clipboard_hint "yay --remove $ss_owner"
+ return 1
fi
+ _yay pass-secret-service-bin
}
-sync_secret_service() {
- local secret_service_file=/usr/share/dbus-1/services/org.freedesktop.secrets.service
- local secret_service_owner
- secret_service_owner=$(pacman --query --owns --quiet "$secret_service_file" 2>/dev/null) || true
- if [[ -n $secret_service_owner && $secret_service_owner != "pass-secret-service-bin" ]]; then
- echo -e "$secret_service_owner owns $secret_service_file (should be pass-secret-service)" \
- "\nPlease uninstall $secret_service_owner, sync, then log back in" >&2
- exit 1
- fi
- # shellcheck disable=SC2034
- local deps=(pass-secret-service-bin)
- sync_packages yay deps
+# proton-vpn-cli has gnome-keyring as a hard dependency because Proton VPN
+# officially supports only gnome-keyring as a Secret Service provider. We have
+# to force proton-vpn-cli to install without gnome-keyring. This is fine,
+# because Proton VPN will use pass-secret-service-bin (our Secret Service
+# provider)
+_sync_protonvpn() {
+ [[ -n $(_get_yay_packages_to_sync proton-vpn-cli) ]] || return 0
+ yay --sync --noconfirm --assume-installed gnome-keyring proton-vpn-cli
+}
+
+# Install Proton VPN and a Secret Service provider it depends on
+_sync_vpn() {
+ _sync_secret_service
+ _sync_protonvpn
+}
+
+################################################################################
+# main() #######################################################################
+################################################################################
+
+_usage() {
+ echo -e "usage:\n\tdotfiles {sync|on|off}"
+ return 1
+}
+
+# Hint a command passed as argument and copy it to clipboard if possible
+_clipboard_hint() {
+ local cmd="$1"
+ echo -e "Run the following command\n\n\t$cmd\n"
+ command -v wl-copy >/dev/null || return 0
+ wl-copy "$cmd" && echo "(Copied to clipboard)"
}
-# python-proton-keyring-linux (a dependency of proton-vpn-…) has gnome-keyring
-# as a hard dependency even though one can actually use any provider of
-# org.freedesktop.secrets (like pass-secret-service in this project).
-# So we just install it assuming gnome-keyring is installed, and it properly
-# picks up our secret service provider on the DBus.
-sync_proton_vpn() {
- # shellcheck disable=SC2034
- local proton_vpn_packages=(proton-vpn-cli)
- # --needed because one of the two packages might already be synced
- sync_packages pacman proton_vpn_packages --needed --assume-installed gnome-keyring
+_sync_all() {
+ echo "Syncing basic dependencies…"
+ command -v yay >/dev/null ||
+ { echo "Please install yay and try again" && return 1; }
+ _yay python-pipx jq
+ echo "Syncing Arch Linux & AUR packages…"
+ _yay "${PACKAGES_YAY[@]}"
+ echo "Syncing Python packages…"
+ _pipx PACKAGES_PYTHON
+ echo "Syncing Claude Code…"
+ _sync_claude
+ echo "Syncing Neovim…"
+ _sync_nvim
+ echo "Syncing Proton VPN…"
+ _sync_vpn
+}
+
+main() {
+ local cmd=${1:-}
+ case $cmd in
+ sync) _sync_all ;;
+ on) _clipboard_hint "source $HOME/.local/share/dotfiles/dotfiles_on.env" ;;
+ off) _clipboard_hint "source $HOME/.local/share/dotfiles/dotfiles_off.env" ;;
+ *) echo "Invalid command: $cmd" && _usage ;;
+ esac
}
-case ${1:-} in
-sync)
- # shellcheck disable=SC2034
- readarray -t deps <"$HOME/.local/share/dotfiles/packages/base.txt"
- sync_packages pacman deps
- # shellcheck disable=SC2034
- declare aur_deps=(vim-language-server)
- sync_packages yay aur_deps
- sync_secret_service
- sync_proton_vpn
- sync_nvim
- sync_python_packages
- sync_claude
- ;;
-reinstall-nvim) reinstall_nvim ;;
-on) clipboard_hint "source $HOME/.local/share/dotfiles/dotfiles_on.env" ;;
-off) clipboard_hint "source $HOME/.local/share/dotfiles/dotfiles_off.env" ;;
-*)
- echo "usage: dotfiles {sync|reinstall-nvim|on|off}"
- exit 1
- ;;
-esac
+if [[ ${BASH_SOURCE[0]} == "$0" ]]; then main "$@"; fi
diff --git a/.local/share/dotfiles/packages/README.md b/.local/share/dotfiles/packages/README.md
deleted file mode 100644
index 6aa04be..0000000
--- a/.local/share/dotfiles/packages/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# packages
-
-## base.txt
-
-- `foot` (terminal emulator)
-- `tmux` (terminal multiplexer)
-- `bash` (shell)
-- `bash-completion` (collection of bash completions)
-- `fzf` (fuzzy finder)
-- `fd` (fast file finder (fzf-lua files provider))
-- `tig` (Git terminal interface)
-- `ripgrep` (fast grep (fzf-lua live_grep provider))
-- `ranger` (file explorer)
-- `glib2` (GIO library (used to trash in ranger))
-- `bat` (syntax highlighter)
-- `texinfo` (GNU info documentation reader)
-- `python-pipx` (Python apps environment)
-- `wl-clipboard` (Wayland clipboard)
-- `jq` (JSON processor)
-- `pass` (password manager)
-- `firefox` (web browser)
-- `browserpass` (pass native host for browsers)
-- `browserpass-firefox` (firefox extension for browserpass)
-- `yay` (AUR helper (for pass-secret-service-bin))
-- `shfmt` (shell program formatter)
-- `stylua` (Lua formatter)
-- `tree-sitter-cli` (used by nvim (increment parsing))
-- `base-devel` `cmake` `ninja` `git` (nvim build dependencies)
-- `tk` (gitk and git gui)
-- `docker`, `pigz`, `docker-buildx` (docker and its dependencies)
-- `ctags` (ctags generator)
-- `python-black` (python opinionated formatter)
-- language servers (used by `nvim)`
- - `bash-language-server`
- - `lua-language-server`
- - `marksman`
-- tools for archives
- - `atool`
- - `bzip2`
- - `cpio`
- - `gzip`
- - `lhasa`
- - `xz`
- - `lzop` `7zip`
- - `tar`
- - `unrar`
- - `zip`
- - `unzip`
diff --git a/.local/share/dotfiles/packages/base.txt b/.local/share/dotfiles/packages/base.txt
deleted file mode 100644
index 6b6b709..0000000
--- a/.local/share/dotfiles/packages/base.txt
+++ /dev/null
@@ -1,49 +0,0 @@
-7zip
-atool
-base-devel
-bash
-bash-completion
-bash-language-server
-bat
-browserpass
-browserpass-firefox
-bzip2
-cmake
-cpio
-ctags
-docker
-docker-buildx
-fd
-firefox
-foot
-fzf
-git
-glib2
-gzip
-jq
-lhasa
-lua-language-server
-lzop
-marksman
-ninja
-pass
-pigz
-proton-vpn-cli
-python-black
-python-pipx
-ranger
-ripgrep
-shfmt
-stylua
-tar
-texinfo
-tig
-tk
-tmux
-tree-sitter-cli
-unrar
-unzip
-wl-clipboard
-xz
-yay
-zip