Files
andwati 77cc3a1c98 Switch Slack to Flathub instead of the packagecloud apt repo
Adds a shared ensure_flatpak helper in lib.sh (installs flatpak + adds the
Flathub remote) and reuses it from both the slack module and desnap.sh's
unmapped-snap fallback instead of duplicating the setup.
2026-07-15 17:04:38 +03:00

254 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODULES_DIR="$SCRIPT_DIR/modules"
export DOTFILES_DIR="${DOTFILES_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
# shellcheck source=lib.sh
source "$SCRIPT_DIR/lib.sh"
trap 'rm -f "$(_apt_marker)"' EXIT
ALL_MODULES=(
00-base mirrors perms cli-tools
nvm node pnpm deno bun
pyenv pipx pipenv poetry uv
vscode cursor ghcli docker go rust zig
chrome discord slack firefox postman jetbrains-toolbox mpv spotify telegram signal protonvpn onedrive
gnome wakatime
)
declare -A MODULE_DESC=(
[00-base]="core apt packages (curl, git, build-essential, jq, stow, ...)"
[mirrors]="benchmark + switch apt to the fastest fresh Ubuntu mirror"
[perms]="fix ~/.ssh and ~/.gnupg permissions"
[cli-tools]="eza, ripgrep + shell aliases"
[nvm]="Node Version Manager"
[node]="Node.js LTS (via nvm)"
[pnpm]="pnpm package manager"
[deno]="Deno runtime"
[bun]="Bun runtime"
[pyenv]="Python version manager"
[pipx]="pipx (isolated python CLI installs)"
[pipenv]="pipenv (via pipx)"
[poetry]="Poetry python packaging"
[uv]="uv python package/venv manager"
[vscode]="VS Code + your extensions/settings (mirrors to Cursor too)"
[cursor]="Cursor editor (official AppImage)"
[ghcli]="GitHub CLI (gh)"
[docker]="Docker Engine + compose plugin"
[go]="Go toolchain"
[rust]="Rust (rustup)"
[zig]="Zig + zls (language server)"
[chrome]="Google Chrome"
[discord]="Discord"
[slack]="Slack (Flathub)"
[firefox]="Firefox (Mozilla apt repo, not snap)"
[postman]="Postman"
[jetbrains-toolbox]="JetBrains Toolbox"
[mpv]="mpv media player"
[spotify]="Spotify"
[telegram]="Telegram Desktop"
[signal]="Signal Desktop"
[protonvpn]="ProtonVPN (official GNOME app)"
[onedrive]="OneDrive client (abraunegg, official OBS repo)"
[gnome]="GNOME Tweaks, Extension Manager, AppIndicator support"
[wakatime]="Wakatime API key setup"
)
SUMMARY_OK=()
SUMMARY_FAILED=()
usage() {
cat <<EOF
Usage: $(basename "$0") [module ...]
$(basename "$0") --list
$(basename "$0") --except mod1,mod2
$(basename "$0") --dry-run [module ...]
$(basename "$0") --all | --no-tui
With no arguments on an interactive terminal, shows a checklist (whiptail)
to pick which modules to install. Pass --all/--no-tui to skip the checklist
and install everything, or name modules explicitly.
A module failing does not abort the run: its own changes are rolled back,
the failure is recorded, and the next module runs. Exit status is nonzero
if anything failed. Every module is idempotent — safe to re-run.
Modules:
$(printf ' %-20s %s\n' $(for m in "${ALL_MODULES[@]}"; do printf '%s\n%s\n' "$m" "${MODULE_DESC[$m]:-}"; done))
EOF
}
list_modules() { printf '%s\n' "${ALL_MODULES[@]}"; }
ensure_whiptail() {
if has_cmd whiptail; then return 0; fi
log "installing whiptail for the selection menu"
apt_update_once
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y whiptail
}
tui_select_modules() {
if ! ensure_whiptail || ! has_cmd whiptail; then
warn "whiptail unavailable, falling back to a plain prompt"
plain_select_modules
return
fi
local -a items=()
local m
for m in "${ALL_MODULES[@]}"; do
items+=("$m" "${MODULE_DESC[$m]:-}" "ON")
done
local choices
choices=$(whiptail --title "dotfiles installer" --checklist \
"Space to toggle, Enter to confirm, Esc to cancel" 24 78 16 \
"${items[@]}" 3>&1 1>&2 2>&3) || { warn "selection cancelled"; return 1; }
eval "local -a selected=($choices)"
printf '%s\n' "${selected[@]}"
}
plain_select_modules() {
echo "Select modules (space-separated numbers, or 'all'):" >&2
local i=1 m
for m in "${ALL_MODULES[@]}"; do
printf ' %2d) %-20s %s\n' "$i" "$m" "${MODULE_DESC[$m]:-}" >&2
i=$((i + 1))
done
read -rp "> " reply
if [[ "$reply" == "all" || -z "$reply" ]]; then
printf '%s\n' "${ALL_MODULES[@]}"
return
fi
local -a out=()
local n
for n in $reply; do
[[ "$n" =~ ^[0-9]+$ ]] || continue
out+=("${ALL_MODULES[$((n - 1))]}")
done
printf '%s\n' "${out[@]}"
}
run_module() {
local slug="$1"
local file="$MODULES_DIR/${slug}.sh"
local fn="install_${slug//-/_}"
if [[ ! -f "$file" ]]; then
err "unknown module: $slug"
SUMMARY_FAILED+=("$slug")
return
fi
if [[ "${DRY_RUN:-0}" == "1" ]]; then
log "[dry-run] would install: $slug — ${MODULE_DESC[$slug]:-}"
SUMMARY_OK+=("$slug (dry-run)")
return
fi
log "installing $slug"
# Run in a subshell so a failing command genuinely aborts the module
# (errexit) instead of limping on to the next line. Note: `if ( ... );
# then` or `( ... ) || ...` would silently defeat -e here — bash suspends
# errexit for the *whole* tested command, including inside a nested
# subshell, even if that subshell re-sets -e itself. The subshell must run
# as a genuinely unconditional statement, under a `set +e` bracket so its
# own nonzero exit doesn't also kill the outer script, with $? captured
# right after and -e restored immediately.
set +e
(
set -Eeuo pipefail
trap 'rollback_run' ERR
# shellcheck disable=SC1090
source "$file"
rollback_reset
"$fn"
)
local status=$?
set -e
if [[ $status -eq 0 ]]; then
ok "$slug done"
SUMMARY_OK+=("$slug")
else
err "$slug failed — its changes were rolled back"
SUMMARY_FAILED+=("$slug")
fi
}
print_summary() {
echo
log "summary"
if [[ ${#SUMMARY_OK[@]} -gt 0 ]]; then printf ' ok: %s\n' "${SUMMARY_OK[*]}"; fi
if [[ ${#SUMMARY_FAILED[@]} -gt 0 ]]; then printf ' failed: %s\n' "${SUMMARY_FAILED[*]}"; fi
return 0
}
main() {
local -a explicit=() except=()
local dry_run=0 force_all=0
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--list) list_modules; exit 0 ;;
--dry-run) dry_run=1; shift ;;
--all|--no-tui) force_all=1; shift ;;
--except)
IFS=',' read -ra except <<< "${2:-}"
shift 2
;;
-*)
err "unknown flag: $1"; usage; exit 1 ;;
*) explicit+=("$1"); shift ;;
esac
done
export DRY_RUN=$dry_run
local -a targets=()
local m s skipped
if [[ ${#except[@]} -gt 0 ]]; then
for m in "${ALL_MODULES[@]}"; do
skipped=0
for s in "${except[@]}"; do
if [[ "$m" == "$s" ]]; then skipped=1; fi
done
[[ $skipped -eq 1 ]] || targets+=("$m")
done
elif [[ ${#explicit[@]} -gt 0 ]]; then
targets=("${explicit[@]}")
elif [[ $force_all -eq 0 && -t 0 && -t 1 ]]; then
local tui_out
if ! tui_out=$(tui_select_modules); then
err "no modules selected"
exit 1
fi
mapfile -t targets <<< "$tui_out"
else
targets=("${ALL_MODULES[@]}")
fi
if [[ ${#targets[@]} -eq 0 ]]; then
warn "nothing selected"
exit 0
fi
local -a ordered=(00-base)
for m in "${targets[@]}"; do
[[ "$m" == "00-base" ]] || ordered+=("$m")
done
for m in "${ordered[@]}"; do
run_module "$m"
done
print_summary
[[ ${#SUMMARY_FAILED[@]} -eq 0 ]]
}
main "$@"