diff options
Diffstat (limited to '.local/bin')
| -rwxr-xr-x | .local/bin/dotfiles | 342 |
1 files changed, 189 insertions, 153 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 |
