mirror of
https://github.com/andwati/dotfiles.git
synced 2026-10-28 23:42:18 +00:00
Add modular dev environment installer, dotfiles, and desnap script
Adds a TUI-driven, rollback-capable installer (install/install.sh + install/modules/*.sh) covering JS/Python/systems toolchains, dev tooling, browsers, chat apps, GNOME extras, and misc setup (wakatime key, ssh/gpg perms, apt mirror benchmarking), all sourced from each vendor's official install method. bootstrap.sh stows the zsh/git/tmux/vscode dotfiles and runs the installer; desnap.sh purges snapd and replaces default snaps with apt/Flatpak equivalents.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
install_00_base() {
|
||||
if ! grep -qi ubuntu /etc/os-release 2>/dev/null; then
|
||||
warn "this installer targets Ubuntu; continuing anyway"
|
||||
fi
|
||||
apt_install \
|
||||
curl wget git ca-certificates gnupg lsb-release jq \
|
||||
software-properties-common apt-transport-https \
|
||||
build-essential unzip xz-utils stow
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
install_bun() {
|
||||
if has_cmd bun; then ok "bun already installed"; return; fi
|
||||
track_new_dir "$HOME/.bun"
|
||||
NO_MODIFY_PATH=1 bash -c "$(curl -fsSL https://bun.sh/install)"
|
||||
profile_add 'export BUN_INSTALL="$HOME/.bun"'
|
||||
profile_add 'export PATH="$BUN_INSTALL/bin:$PATH"'
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
install_chrome() {
|
||||
if has_cmd google-chrome-stable; then
|
||||
ok "chrome already installed"
|
||||
return
|
||||
fi
|
||||
|
||||
local deb
|
||||
deb=$(mktemp --suffix=.deb)
|
||||
curl -fsSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o "$deb"
|
||||
apt_update_once
|
||||
sudo apt-get install -y "$deb"
|
||||
rm -f "$deb"
|
||||
rollback_push "sudo apt-get remove -y google-chrome-stable"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
install_cli_tools() {
|
||||
apt_install eza ripgrep
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
install_cursor() {
|
||||
if [[ -x /opt/cursor/cursor.AppImage ]]; then
|
||||
ok "cursor already installed"
|
||||
return
|
||||
fi
|
||||
|
||||
apt_install libfuse2
|
||||
|
||||
local appimage
|
||||
appimage=$(mktemp --suffix=.AppImage)
|
||||
curl -fsSL https://downloader.cursor.sh/linux/appImage/x64 -o "$appimage"
|
||||
chmod +x "$appimage"
|
||||
|
||||
track_new_sudo_dir /opt/cursor
|
||||
sudo mkdir -p /opt/cursor
|
||||
sudo mv "$appimage" /opt/cursor/cursor.AppImage
|
||||
sudo ln -sf /opt/cursor/cursor.AppImage /usr/local/bin/cursor
|
||||
rollback_push "sudo rm -f /usr/local/bin/cursor"
|
||||
|
||||
sudo tee /usr/share/applications/cursor.desktop >/dev/null <<'EOF'
|
||||
[Desktop Entry]
|
||||
Name=Cursor
|
||||
Exec=/opt/cursor/cursor.AppImage --no-sandbox %U
|
||||
Icon=cursor
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Development;
|
||||
EOF
|
||||
rollback_push "sudo rm -f /usr/share/applications/cursor.desktop"
|
||||
|
||||
# Reuses vscode.sh's mirror step to symlink settings + install the same
|
||||
# extensions into the newly-installed Cursor.
|
||||
# shellcheck disable=SC1090
|
||||
source "${DOTFILES_DIR:?}/install/modules/vscode.sh"
|
||||
_vscode_mirror_cursor
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
install_deno() {
|
||||
if has_cmd deno; then ok "deno already installed"; return; fi
|
||||
track_new_dir "$HOME/.deno"
|
||||
curl -fsSL https://deno.land/install.sh | sh
|
||||
profile_add 'export DENO_INSTALL="$HOME/.deno"'
|
||||
profile_add 'export PATH="$DENO_INSTALL/bin:$PATH"'
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
install_discord() {
|
||||
if has_cmd discord; then ok "discord already installed"; return; fi
|
||||
local deb
|
||||
deb=$(mktemp --suffix=.deb)
|
||||
curl -fsSL 'https://discord.com/api/download?platform=linux&format=deb' -o "$deb"
|
||||
apt_update_once
|
||||
sudo apt-get install -y "$deb"
|
||||
rm -f "$deb"
|
||||
rollback_push "sudo apt-get remove -y discord"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
install_docker() {
|
||||
if has_cmd docker; then ok "docker already installed"; return; fi
|
||||
apt_install ca-certificates curl
|
||||
|
||||
local codename
|
||||
codename=$(ubuntu_codename)
|
||||
if ! curl -fsSL -o /dev/null "https://download.docker.com/linux/ubuntu/dists/${codename}/Release"; then
|
||||
warn "Docker has no repo for '${codename}' yet, falling back to 'noble' (24.04)"
|
||||
codename=noble
|
||||
fi
|
||||
|
||||
apt_add_repo docker /etc/apt/keyrings/docker.asc \
|
||||
https://download.docker.com/linux/ubuntu/gpg \
|
||||
"deb [arch=$(deb_arch) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu ${codename} stable"
|
||||
apt_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
sudo usermod -aG docker "$USER"
|
||||
rollback_push "sudo gpasswd -d '$USER' docker"
|
||||
warn "log out/in (or run 'newgrp docker') for docker group membership to take effect"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
install_firefox() {
|
||||
if [[ -f /etc/apt/preferences.d/mozilla ]]; then
|
||||
ok "firefox (deb) already installed"
|
||||
return
|
||||
fi
|
||||
|
||||
log "removing snap firefox / transitional apt stub in favor of Mozilla's official apt repo"
|
||||
sudo apt-get remove -y firefox 2>/dev/null || true
|
||||
sudo snap remove firefox 2>/dev/null || true
|
||||
|
||||
apt_add_repo mozilla /etc/apt/keyrings/packages.mozilla.org.asc \
|
||||
https://packages.mozilla.org/apt/repo-signing-key.gpg \
|
||||
"deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main"
|
||||
|
||||
printf 'Package: *\nPin: origin packages.mozilla.org\nPin-Priority: 1000\n' \
|
||||
| sudo tee /etc/apt/preferences.d/mozilla >/dev/null
|
||||
rollback_push "sudo rm -f /etc/apt/preferences.d/mozilla"
|
||||
|
||||
apt_install firefox
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
install_ghcli() {
|
||||
if has_cmd gh; then ok "gh already installed"; return; fi
|
||||
apt_add_repo github-cli /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
"deb [arch=$(deb_arch) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main"
|
||||
apt_install gh
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
install_gnome() {
|
||||
if ! has_cmd gnome-shell; then
|
||||
warn "gnome-shell not found; skipping (not running GNOME?)"
|
||||
return
|
||||
fi
|
||||
|
||||
apt_install gnome-tweaks gnome-shell-extension-manager dconf-editor \
|
||||
gnome-shell-extension-appindicator
|
||||
|
||||
if has_cmd gnome-extensions; then
|
||||
gnome-extensions enable [email protected] 2>/dev/null || true
|
||||
fi
|
||||
|
||||
ok "log out/in for the AppIndicator extension to take full effect (tray icons for Discord/Slack/Signal etc.)"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
install_go() {
|
||||
local latest
|
||||
latest=$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n1)
|
||||
|
||||
if has_cmd go && go version | grep -q "$latest"; then
|
||||
ok "go already at $latest"
|
||||
return
|
||||
fi
|
||||
|
||||
local arch
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch=amd64 ;;
|
||||
aarch64) arch=arm64 ;;
|
||||
*) err "unsupported arch $(uname -m)"; return 1 ;;
|
||||
esac
|
||||
|
||||
local tarball="/tmp/${latest}.linux-${arch}.tar.gz"
|
||||
curl -fsSL "https://go.dev/dl/${latest}.linux-${arch}.tar.gz" -o "$tarball"
|
||||
|
||||
local backup=""
|
||||
if [[ -d /usr/local/go ]]; then
|
||||
backup="/usr/local/go.bak.$$"
|
||||
sudo mv /usr/local/go "$backup"
|
||||
rollback_push "sudo rm -rf /usr/local/go; sudo mv '$backup' /usr/local/go"
|
||||
else
|
||||
rollback_push "sudo rm -rf /usr/local/go"
|
||||
fi
|
||||
|
||||
sudo tar -C /usr/local -xzf "$tarball"
|
||||
rm -f "$tarball"
|
||||
if [[ -n "$backup" ]]; then sudo rm -rf "$backup"; fi
|
||||
|
||||
profile_add 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"'
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
install_jetbrains_toolbox() {
|
||||
if [[ -x /opt/jetbrains-toolbox/jetbrains-toolbox ]]; then
|
||||
ok "jetbrains toolbox already installed"
|
||||
return
|
||||
fi
|
||||
|
||||
local url tarball extract_dir
|
||||
url=$(curl -fsSL 'https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release' \
|
||||
| jq -r '.TBA[0].downloads.linux.link')
|
||||
|
||||
tarball=$(mktemp --suffix=.tar.gz)
|
||||
curl -fsSL "$url" -o "$tarball"
|
||||
extract_dir=$(mktemp -d)
|
||||
tar -xzf "$tarball" -C "$extract_dir" --strip-components=1
|
||||
rm -f "$tarball"
|
||||
|
||||
sudo rm -rf /opt/jetbrains-toolbox
|
||||
track_new_sudo_dir /opt/jetbrains-toolbox
|
||||
sudo mv "$extract_dir" /opt/jetbrains-toolbox
|
||||
sudo ln -sf /opt/jetbrains-toolbox/jetbrains-toolbox /usr/local/bin/jetbrains-toolbox
|
||||
rollback_push "sudo rm -f /usr/local/bin/jetbrains-toolbox"
|
||||
|
||||
ok "run 'jetbrains-toolbox' once to finish setup; it installs its own desktop entry"
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
install_mirrors() {
|
||||
local codename
|
||||
codename=$(ubuntu_codename)
|
||||
|
||||
local candidates
|
||||
candidates=$(curl -fsSL --max-time 5 https://mirrors.ubuntu.com/mirrors.txt) || {
|
||||
err "could not fetch the official Ubuntu mirror list"
|
||||
return 1
|
||||
}
|
||||
|
||||
log "benchmarking mirrors for speed + freshness (this takes a few seconds)"
|
||||
|
||||
local best_url="" best_time="" url resp t last_modified age_hours checked=0
|
||||
while IFS= read -r url; do
|
||||
if [[ -z "$url" || "$url" == \#* ]]; then continue; fi
|
||||
if [[ $checked -ge 12 ]]; then break; fi
|
||||
checked=$((checked + 1))
|
||||
|
||||
resp=$(curl -o /dev/null -sD - --max-time 3 -w '\n%{time_total}' "${url}dists/${codename}/Release" 2>/dev/null) || continue
|
||||
if [[ -z "$resp" ]]; then continue; fi
|
||||
|
||||
t=$(tail -n1 <<< "$resp")
|
||||
if [[ -z "$t" ]]; then continue; fi
|
||||
|
||||
last_modified=$(grep -i '^Last-Modified:' <<< "$resp" | head -n1 | cut -d' ' -f2- | tr -d '\r')
|
||||
if [[ -n "$last_modified" ]]; then
|
||||
age_hours=$(( ($(date +%s) - $(date -d "$last_modified" +%s 2>/dev/null || echo 0)) / 3600 ))
|
||||
if [[ $age_hours -gt 72 ]]; then continue; fi
|
||||
fi
|
||||
|
||||
if [[ -z "$best_time" ]] || awk -v a="$t" -v b="$best_time" 'BEGIN{exit !(a<b)}'; then
|
||||
best_time="$t"
|
||||
best_url="$url"
|
||||
fi
|
||||
done <<< "$candidates"
|
||||
|
||||
if [[ -z "$best_url" ]]; then
|
||||
warn "no mirror passed the speed/freshness check, leaving apt sources untouched"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ok "fastest fresh mirror: $best_url (${best_time}s)"
|
||||
_mirrors_apply "$best_url"
|
||||
}
|
||||
|
||||
# Rewrites whichever apt sources format is in use to point at the chosen
|
||||
# mirror. Ubuntu 24.04+ (and 26.04) default to the deb822
|
||||
# /etc/apt/sources.list.d/ubuntu.sources file; older installs still use the
|
||||
# one-line-per-entry /etc/apt/sources.list. security.ubuntu.com is left
|
||||
# alone on purpose — security updates stay on Canonical's own CDN.
|
||||
_mirrors_apply() {
|
||||
local mirror="${1%/}"
|
||||
local deb822="/etc/apt/sources.list.d/ubuntu.sources"
|
||||
local legacy="/etc/apt/sources.list"
|
||||
local target="" backup=""
|
||||
|
||||
if [[ -f "$deb822" ]]; then
|
||||
target="$deb822"
|
||||
elif [[ -f "$legacy" ]] && grep -q archive.ubuntu.com "$legacy"; then
|
||||
target="$legacy"
|
||||
else
|
||||
warn "no recognized apt sources file found, skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
backup="${target}.bak.$$"
|
||||
sudo cp "$target" "$backup"
|
||||
rollback_push "sudo mv '$backup' '$target'; apt_invalidate"
|
||||
sudo sed -i -E "s#https?://[a-zA-Z0-9.-]*archive\.ubuntu\.com/ubuntu#${mirror}#g" "$target"
|
||||
|
||||
apt_invalidate
|
||||
if ! apt_update_once; then
|
||||
err "apt update failed against $mirror"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sudo rm -f "$backup"
|
||||
ok "apt now points at $mirror"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
install_mpv() {
|
||||
apt_install mpv
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
install_node() {
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
if [[ ! -s "$NVM_DIR/nvm.sh" ]]; then
|
||||
err "nvm not found; run the nvm module first"
|
||||
return 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
\. "$NVM_DIR/nvm.sh"
|
||||
|
||||
nvm install --lts
|
||||
nvm alias default 'lts/*'
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
install_nvm() {
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
|
||||
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
|
||||
ok "nvm already installed"
|
||||
else
|
||||
local version installer
|
||||
version=$(curl -fsSL https://api.github.com/repos/nvm-sh/nvm/releases/latest | jq -r .tag_name)
|
||||
installer=$(mktemp)
|
||||
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${version}/install.sh" -o "$installer"
|
||||
track_new_dir "$NVM_DIR"
|
||||
# PROFILE=/dev/null: we manage PATH/init lines ourselves via profile_add below.
|
||||
PROFILE=/dev/null bash "$installer"
|
||||
rm -f "$installer"
|
||||
fi
|
||||
|
||||
profile_add 'export NVM_DIR="$HOME/.nvm"'
|
||||
profile_add '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"'
|
||||
profile_add '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"'
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
install_onedrive() {
|
||||
if has_cmd onedrive; then
|
||||
ok "onedrive already installed"
|
||||
return
|
||||
fi
|
||||
|
||||
local version repo_base
|
||||
version=$(. /etc/os-release; echo "$VERSION_ID")
|
||||
repo_base="https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_${version}"
|
||||
|
||||
if ! curl -fsSL -o /dev/null "${repo_base}/Release"; then
|
||||
warn "onedrive's OBS repo has no build for Ubuntu ${version} yet, falling back to 24.04"
|
||||
version="24.04"
|
||||
repo_base="https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_${version}"
|
||||
fi
|
||||
|
||||
apt_add_repo onedrive /etc/apt/keyrings/onedrive.gpg \
|
||||
"${repo_base}/Release.key" \
|
||||
"deb ${repo_base}/ ./"
|
||||
apt_install onedrive
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
install_perms() {
|
||||
if [[ -d "$HOME/.ssh" ]]; then
|
||||
chmod 700 "$HOME/.ssh"
|
||||
find "$HOME/.ssh" -maxdepth 1 -type f ! -name '*.pub' -exec chmod 600 {} +
|
||||
find "$HOME/.ssh" -maxdepth 1 -type f -name '*.pub' -exec chmod 644 {} +
|
||||
if [[ -f "$HOME/.ssh/config" ]]; then chmod 600 "$HOME/.ssh/config"; fi
|
||||
if [[ -f "$HOME/.ssh/authorized_keys" ]]; then chmod 600 "$HOME/.ssh/authorized_keys"; fi
|
||||
if [[ -f "$HOME/.ssh/known_hosts" ]]; then chmod 644 "$HOME/.ssh/known_hosts"; fi
|
||||
ok "fixed ~/.ssh permissions"
|
||||
else
|
||||
warn "~/.ssh not found, skipping"
|
||||
fi
|
||||
|
||||
if [[ -d "$HOME/.gnupg" ]]; then
|
||||
chmod 700 "$HOME/.gnupg"
|
||||
find "$HOME/.gnupg" -type d -exec chmod 700 {} +
|
||||
find "$HOME/.gnupg" -type f -exec chmod 600 {} +
|
||||
ok "fixed ~/.gnupg permissions"
|
||||
else
|
||||
warn "~/.gnupg not found, skipping"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
install_pipenv() {
|
||||
if ! has_cmd pipx; then
|
||||
err "pipx not found; run the pipx module first"
|
||||
return 1
|
||||
fi
|
||||
if pipx list --short 2>/dev/null | grep -q '^pipenv '; then
|
||||
ok "pipenv already installed"
|
||||
return
|
||||
fi
|
||||
pipx install pipenv
|
||||
rollback_push "pipx uninstall pipenv"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
install_pipx() {
|
||||
apt_install pipx
|
||||
profile_add 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
install_pnpm() {
|
||||
if has_cmd pnpm; then ok "pnpm already installed"; return; fi
|
||||
track_new_dir "$HOME/.local/share/pnpm"
|
||||
curl -fsSL https://get.pnpm.io/install.sh | sh -
|
||||
profile_add 'export PNPM_HOME="$HOME/.local/share/pnpm"'
|
||||
profile_add 'case ":$PATH:" in *":$PNPM_HOME:"*) ;; *) export PATH="$PNPM_HOME:$PATH" ;; esac'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
install_poetry() {
|
||||
if has_cmd poetry; then ok "poetry already installed"; return; fi
|
||||
track_new_dir "$HOME/.local/share/pypoetry"
|
||||
rollback_push "rm -f '$HOME/.local/bin/poetry'"
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
profile_add 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
install_postman() {
|
||||
if [[ -x /opt/Postman/Postman ]]; then ok "postman already installed"; return; fi
|
||||
|
||||
local tarball
|
||||
tarball=$(mktemp --suffix=.tar.gz)
|
||||
curl -fsSL https://dl.pstmn.io/download/latest/linux_64 -o "$tarball"
|
||||
track_new_sudo_dir /opt/Postman
|
||||
sudo tar -xzf "$tarball" -C /opt
|
||||
rm -f "$tarball"
|
||||
sudo ln -sf /opt/Postman/Postman /usr/local/bin/postman
|
||||
rollback_push "sudo rm -f /usr/local/bin/postman /usr/share/applications/postman.desktop"
|
||||
|
||||
sudo tee /usr/share/applications/postman.desktop >/dev/null <<'EOF'
|
||||
[Desktop Entry]
|
||||
Name=Postman
|
||||
Exec=/opt/Postman/Postman %U
|
||||
Icon=/opt/Postman/app/resources/app/assets/icon.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Development;
|
||||
EOF
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
install_protonvpn() {
|
||||
if dpkg -s proton-vpn-gnome-desktop >/dev/null 2>&1; then
|
||||
ok "protonvpn already installed"
|
||||
return
|
||||
fi
|
||||
|
||||
local list_url deb_name deb
|
||||
list_url="https://repo.protonvpn.com/debian/dists/stable/main/binary-all/"
|
||||
deb_name=$(curl -fsSL "$list_url" | grep -oE 'protonvpn-stable-release_[0-9.]+_all\.deb' | sort -V | tail -n1)
|
||||
|
||||
if [[ -z "$deb_name" ]]; then
|
||||
err "could not find the protonvpn-stable-release package on repo.protonvpn.com"
|
||||
return 1
|
||||
fi
|
||||
|
||||
deb=$(mktemp --suffix=.deb)
|
||||
curl -fsSL "${list_url}${deb_name}" -o "$deb"
|
||||
apt_update_once
|
||||
sudo apt-get install -y "$deb"
|
||||
rm -f "$deb"
|
||||
rollback_push "sudo apt-get remove -y protonvpn-stable-release"
|
||||
|
||||
apt_invalidate
|
||||
apt_install proton-vpn-gnome-desktop
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
install_pyenv() {
|
||||
apt_install make build-essential libssl-dev zlib1g-dev libbz2-dev \
|
||||
libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev \
|
||||
xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
|
||||
|
||||
if [[ -d "$HOME/.pyenv" ]]; then
|
||||
ok "pyenv already installed"
|
||||
else
|
||||
track_new_dir "$HOME/.pyenv"
|
||||
curl -fsSL https://pyenv.run | bash
|
||||
fi
|
||||
|
||||
profile_add 'export PYENV_ROOT="$HOME/.pyenv"'
|
||||
profile_add 'export PATH="$PYENV_ROOT/bin:$PATH"'
|
||||
profile_add 'eval "$(pyenv init - zsh)"'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
install_rust() {
|
||||
if has_cmd rustc; then ok "rust already installed"; return; fi
|
||||
track_new_dir "$HOME/.cargo"
|
||||
track_new_dir "$HOME/.rustup"
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
|
||||
profile_add '. "$HOME/.cargo/env"'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
install_signal() {
|
||||
if has_cmd signal-desktop; then ok "signal already installed"; return; fi
|
||||
apt_add_repo signal-xenial /usr/share/keyrings/signal-desktop-keyring.gpg \
|
||||
https://updates.signal.org/desktop/apt/keys.asc \
|
||||
"deb [arch=amd64 signed-by=/usr/share/keyrings/signal-desktop-keyring.gpg] https://updates.signal.org/desktop/apt xenial main"
|
||||
apt_install signal-desktop
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
install_slack() {
|
||||
if has_cmd slack; then ok "slack already installed"; return; fi
|
||||
apt_add_repo slack /etc/apt/trusted.gpg.d/slack.gpg \
|
||||
https://packagecloud.io/slacktechnologies/slack/gpgkey \
|
||||
"deb https://packagecloud.io/slacktechnologies/slack/debian/ jessie main"
|
||||
apt_install slack-desktop
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
install_spotify() {
|
||||
if has_cmd spotify; then ok "spotify already installed"; return; fi
|
||||
apt_add_repo spotify /etc/apt/trusted.gpg.d/spotify.gpg \
|
||||
https://download.spotify.com/debian/pubkey_6224F9941A8AA6D1.gpg \
|
||||
"deb http://repository.spotify.com stable non-free"
|
||||
apt_install spotify-client
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
install_telegram() {
|
||||
if [[ -x /opt/Telegram/Telegram ]]; then ok "telegram already installed"; return; fi
|
||||
|
||||
local tarball
|
||||
tarball=$(mktemp --suffix=.tar.xz)
|
||||
curl -fsSL https://telegram.org/dl/desktop/linux -o "$tarball"
|
||||
track_new_sudo_dir /opt/Telegram
|
||||
sudo tar -xJf "$tarball" -C /opt
|
||||
rm -f "$tarball"
|
||||
sudo ln -sf /opt/Telegram/Telegram /usr/local/bin/telegram
|
||||
rollback_push "sudo rm -f /usr/local/bin/telegram /usr/share/applications/telegram.desktop"
|
||||
|
||||
sudo tee /usr/share/applications/telegram.desktop >/dev/null <<'EOF'
|
||||
[Desktop Entry]
|
||||
Name=Telegram
|
||||
Exec=/opt/Telegram/Telegram -- %u
|
||||
Icon=/opt/Telegram/telegram.svg
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;InstantMessaging;
|
||||
MimeType=x-scheme-handler/tg;
|
||||
EOF
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
install_uv() {
|
||||
if has_cmd uv; then ok "uv already installed"; return; fi
|
||||
rollback_push "rm -f '$HOME/.local/bin/uv' '$HOME/.local/bin/uvx'"
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
profile_add 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
install_vscode() {
|
||||
if has_cmd code; then
|
||||
ok "vscode already installed"
|
||||
else
|
||||
apt_add_repo vscode /etc/apt/keyrings/packages.microsoft.gpg \
|
||||
https://packages.microsoft.com/keys/microsoft.asc \
|
||||
"deb [arch=$(deb_arch) signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main"
|
||||
apt_install code
|
||||
fi
|
||||
|
||||
_vscode_install_extensions code
|
||||
_vscode_mirror_cursor
|
||||
}
|
||||
|
||||
# Installs every extension id listed in vscode/extensions.txt (exported from
|
||||
# this machine with `code --list-extensions`) into whichever editor binary
|
||||
# is passed — reused for both `code` and `cursor` since Cursor is a VS Code
|
||||
# fork that reads the same extension id format.
|
||||
_vscode_install_extensions() {
|
||||
local bin="$1"
|
||||
local list="${DOTFILES_DIR:?}/vscode/extensions.txt"
|
||||
has_cmd "$bin" || return 0
|
||||
[[ -f "$list" ]] || return 0
|
||||
|
||||
local installed
|
||||
installed=$("$bin" --list-extensions 2>/dev/null || true)
|
||||
|
||||
local ext
|
||||
while IFS= read -r ext; do
|
||||
if [[ -z "$ext" || "$ext" == \#* ]]; then continue; fi
|
||||
if grep -qxF "$ext" <<< "$installed"; then continue; fi
|
||||
log "$bin: installing extension $ext"
|
||||
"$bin" --install-extension "$ext" --force >/dev/null 2>&1 || warn "$bin: failed to install $ext"
|
||||
done < "$list"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Cursor reads ~/.config/Cursor/User/settings.json the same way VS Code
|
||||
# reads ~/.config/Code/User/settings.json. Symlinking it to the (stowed)
|
||||
# Code settings keeps both in sync from one source of truth in this repo.
|
||||
_vscode_mirror_cursor() {
|
||||
local src="$HOME/.config/Code/User/settings.json"
|
||||
local dest_dir="$HOME/.config/Cursor/User"
|
||||
[[ -f "$src" ]] || return 0
|
||||
|
||||
mkdir -p "$dest_dir"
|
||||
if [[ ! -e "$dest_dir/settings.json" ]]; then
|
||||
ln -sf "$src" "$dest_dir/settings.json"
|
||||
ok "mirrored VS Code settings to Cursor"
|
||||
fi
|
||||
|
||||
if has_cmd cursor; then _vscode_install_extensions cursor; fi
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
install_wakatime() {
|
||||
local cfg="$HOME/.wakatime.cfg"
|
||||
|
||||
if grep -q '^api_key' "$cfg" 2>/dev/null; then
|
||||
ok "wakatime already configured in $cfg"
|
||||
return
|
||||
fi
|
||||
|
||||
local key
|
||||
read -rsp "Wakatime API key (leave blank to skip): " key
|
||||
echo
|
||||
if [[ -z "$key" ]]; then
|
||||
warn "no key entered, skipping wakatime setup"
|
||||
return
|
||||
fi
|
||||
|
||||
local fresh=0
|
||||
[[ -f "$cfg" ]] || fresh=1
|
||||
|
||||
if [[ -f "$cfg" ]]; then
|
||||
grep -v '^api_key' "$cfg" > "${cfg}.tmp" 2>/dev/null || true
|
||||
mv "${cfg}.tmp" "$cfg"
|
||||
if grep -q '^\[settings\]' "$cfg"; then
|
||||
sed -i "/^\[settings\]/a api_key = ${key}" "$cfg"
|
||||
else
|
||||
{ echo "[settings]"; echo "api_key = ${key}"; cat "$cfg"; } > "${cfg}.tmp"
|
||||
mv "${cfg}.tmp" "$cfg"
|
||||
fi
|
||||
else
|
||||
printf '[settings]\napi_key = %s\n' "$key" > "$cfg"
|
||||
fi
|
||||
|
||||
chmod 600 "$cfg"
|
||||
if [[ $fresh -eq 1 ]]; then rollback_push "rm -f '$cfg'"; fi
|
||||
ok "wrote $cfg"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
install_zig() {
|
||||
local index latest
|
||||
index=$(curl -fsSL https://ziglang.org/download/index.json)
|
||||
latest=$(jq -r 'keys[]' <<< "$index" | grep -v '^master$' | sort -V | tail -n1)
|
||||
|
||||
if has_cmd zig && zig version 2>/dev/null | grep -qx "$latest"; then
|
||||
ok "zig already at $latest"
|
||||
else
|
||||
local url tarball extract_dir
|
||||
url=$(jq -r --arg v "$latest" '.[$v]["x86_64-linux"].tarball' <<< "$index")
|
||||
tarball=$(mktemp --suffix=.tar.xz)
|
||||
curl -fsSL "$url" -o "$tarball"
|
||||
|
||||
extract_dir=$(mktemp -d)
|
||||
tar -xJf "$tarball" -C "$extract_dir" --strip-components=1
|
||||
rm -f "$tarball"
|
||||
|
||||
sudo rm -rf /opt/zig
|
||||
track_new_sudo_dir /opt/zig
|
||||
sudo mv "$extract_dir" /opt/zig
|
||||
sudo ln -sf /opt/zig/zig /usr/local/bin/zig
|
||||
rollback_push "sudo rm -f /usr/local/bin/zig"
|
||||
fi
|
||||
|
||||
_zig_install_zls
|
||||
}
|
||||
|
||||
# zls (the Zig language server) is versioned against a specific zig build;
|
||||
# zigtools' own select-version API returns the right zls release for
|
||||
# whatever zig version is currently installed.
|
||||
_zig_install_zls() {
|
||||
if ! has_cmd zig; then return 0; fi
|
||||
|
||||
local zig_version resp url
|
||||
zig_version=$(zig version)
|
||||
resp=$(curl -fsSL "https://releases.zigtools.org/v1/zls/select-version?zig_version=${zig_version}&compatibility=only-runtime")
|
||||
url=$(jq -r '.["x86_64-linux"].tarball // empty' <<< "$resp" 2>/dev/null)
|
||||
|
||||
if [[ -z "$url" ]]; then
|
||||
warn "no matching zls build for zig $zig_version, skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -x /opt/zls/zls ]]; then
|
||||
local current
|
||||
current=$(/opt/zls/zls --version 2>/dev/null || echo "")
|
||||
if [[ "$current" == *"$zig_version"* ]]; then
|
||||
ok "zls already matches zig $zig_version"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local tarball extract_dir
|
||||
tarball=$(mktemp --suffix=.tar.xz)
|
||||
curl -fsSL "$url" -o "$tarball"
|
||||
extract_dir=$(mktemp -d)
|
||||
tar -xJf "$tarball" -C "$extract_dir"
|
||||
rm -f "$tarball"
|
||||
|
||||
sudo rm -rf /opt/zls
|
||||
track_new_sudo_dir /opt/zls
|
||||
sudo mv "$extract_dir" /opt/zls
|
||||
sudo ln -sf /opt/zls/zls /usr/local/bin/zls
|
||||
rollback_push "sudo rm -f /usr/local/bin/zls"
|
||||
}
|
||||
Reference in New Issue
Block a user