2 Commits
Author SHA1 Message Date
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
andwati 36f0caa66e 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.
2026-07-15 17:02:27 +03:00
74 changed files with 1530 additions and 2853 deletions
+2
View File
@@ -0,0 +1,2 @@
.DS_Store
*.swp
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2023 Ian Andwati Copyright (c) 2026 Ian Andwati
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+126 -39
View File
@@ -1,52 +1,139 @@
# dotfiles # dotfiles
My configuration files that keep me from flipping a table after logging in. My configuration files, plus a modular installer for the tools I use on a
fresh Ubuntu box (targeting 26.04). Everything installs from each vendor's
official source — apt repos with their real signing keys, or their official
install scripts/tarballs. No PPAs of unclear provenance, no snaps unless the
vendor's own instructions use one (Ubuntu's own default snaps are actively
replaced — see `desnap.sh` below).
## Setup ## Quick start
``` On a fresh Ubuntu install:
sudo pacman -S stow # or your distro's equivalent
git clone https://github.com/andwati/dotfiles ~/dev/dotfiles ```sh
~/dev/dotfiles/install.sh git clone <this-repo> ~/dotfiles
cd ~/dotfiles
./bootstrap.sh
``` ```
Each top-level directory is a [GNU Stow](https://www.gnu.org/software/stow/) That will:
package mirroring `$HOME` (e.g. `zsh/.zshrc` -> `~/.zshrc`). `install.sh`
clones oh-my-zsh plus the theme/plugins `.zshrc` expects (powerlevel10k,
zsh-autosuggestions, zsh-syntax-highlighting, zsh-history-substring-search,
zsh-completions, you-should-use), then stows the `zsh`, `gnupg`, `git`,
`ghostty`, `kde`, `nvim`, and `vscode` packages to link everything in, backing
up any conflicting real files it finds first (e.g. oh-my-zsh's own
freshly-installed `.zshrc`).
To add a new package: create a top-level dir shaped like `$HOME` (e.g. 1. Install `stow`, `zsh`, `curl`, `git`.
`git/.gitconfig`), then add a `stow_package <name>` call in `install.sh`. 2. Symlink the dotfiles (`zsh/`, `git/`, `tmux/`, `vscode/`) into `$HOME` with
GNU Stow.
3. Prompt once for your git name/email/signing key, saved to
`~/.gitconfig.local` (gitignored — never committed, so this repo stays
shareable).
4. Set zsh as your default shell.
5. Run `install/install.sh`, which shows a checklist (see below) of every
tool to install.
`gpg-agent.conf` caches your GPG passphrase for 8 hours (24h hard cap) Open a new shell afterwards (or `exec zsh`) to pick up PATH changes.
instead of the 10-minute default, so signed commits don't re-prompt
constantly, and uses `pinentry-qt` for a native dialog instead of a
terminal prompt.
Not installed by the script — grab these separately if a fresh machine needs ## Picking what to install
them: `nvm`, `pyenv`, `bun`, `pnpm`, `deno`, `rustup`, `docker`, `fzf`,
`zoxide`, and a Nerd Font (JetBrains Mono Nerd Font) set as your terminal font.
## Everforest theme Run interactively with no arguments and `install/install.sh` shows a
`whiptail` checklist — space to toggle, enter to confirm. Everything's
checked by default; uncheck what you don't want. If `whiptail` isn't
available it's installed automatically; if that's not possible either it
falls back to a plain numbered prompt.
The desktop is themed with [Everforest](https://github.com/sainnhe/everforest) Non-interactive options:
(dark, medium; background `#2b3339`):
- `kde` — `Everforest` color scheme, wallpaper, and the lock screen wallpaper ```sh
(`kscreenlockerrc`). Select the scheme in System Settings > Colors. ./bootstrap.sh nvm node pnpm docker # only these modules (+ base deps)
- `ghostty` — `everforest-dark` theme with background opacity and blur (needs ./install/install.sh --list # list all module names
KWin's Blur effect on Plasma). ./install/install.sh --except discord,slack,spotify
- `nvim` — LazyVim with `neanias/everforest-nvim` (transparent background) and ./install/install.sh --all # skip the checklist, install everything
LSP for zig, python, typescript, rust, go, c and c++. ./install/install.sh --dry-run # show what would run without doing it
- `vscode` — user `settings.json` (Everforest Dark via the ```
`reesew.everforest-theme` extension).
- `chrome/everforest` — not stowed. Open `chrome://extensions`, enable
Developer mode, and Load unpacked on that folder.
The Plasma Login Manager screen is root-owned and can't be stowed: pick the `--dry-run` works with any of the above (`--dry-run --all`,
wallpaper and sync colors in System Settings > Login Screen after changing the `--dry-run nvm docker`, ...) and also works on `bootstrap.sh` and
scheme. Firefox uses the Everforest Dark Medium theme from addons.mozilla.org. `desnap.sh`.
## Resilience: a failed module doesn't kill the run
Every module runs in its own subshell with `errexit` on. If a command in a
module fails partway through, that module's own changes (packages
installed, repos added, directories created, group memberships granted —
whatever it called `rollback_push` for) are unwound in reverse order, the
failure is recorded, and the **next module still runs**. At the end you get
a summary of what succeeded and what failed, and the script exits nonzero
only if something actually failed. Every module is also idempotent — safe
to re-run individually or all together.
## What gets installed
| Category | Tools |
|---|---|
| System | apt mirror benchmarking, `~/.ssh`/`~/.gnupg` permission fixes, eza/ripgrep |
| JS/TS runtimes | nvm, node (via nvm, LTS), pnpm, deno, bun |
| Python | pyenv, pipx, pipenv, poetry, uv |
| Systems languages | go, rust (rustup), zig + zls |
| Dev tooling | VS Code (+ your extensions/settings, mirrored to Cursor), Cursor, GitHub CLI (`gh`), Docker Engine |
| Apps | Chrome, Firefox, Discord, Slack, Postman, JetBrains Toolbox, mpv, Spotify, Telegram, Signal, ProtonVPN, OneDrive (abraunegg) |
| Desktop | GNOME Tweaks, Extension Manager, AppIndicator support |
| Misc | Wakatime API key setup |
## Layout
```
bootstrap.sh entrypoint: stows dotfiles, then runs install/install.sh
desnap.sh standalone: purges snapd, replaces default snaps with apt/Flatpak
install/
lib.sh shared shell helpers (apt/repo/rollback/profile-line helpers)
install.sh module runner — TUI checklist, dry-run, rollback, summary
modules/*.sh one file per tool, each defines install_<name>()
zsh/ git/ tmux/ vscode/ stow packages — mirror their target layout under $HOME
```
To add a new tool: drop a new `install/modules/<name>.sh` defining
`install_<name>()`, and add `<name>` (+ a description) to `ALL_MODULES` /
`MODULE_DESC` in `install/install.sh`.
## desnap.sh
Purges snapd entirely and pins it out of apt so it can't come back on
upgrade. Before removing anything it works out, per installed snap, whether
there's a real replacement:
- Snaps we already have an official-source module for (Firefox) get that
module.
- Known default-Ubuntu snaps with a plain apt equivalent (Thunderbird,
snap-store → gnome-software) get that apt package.
- GNOME/core runtime snaps (content-only, no user-facing app) are skipped.
- Anything else gets Flatpak + Flathub set up and is listed at the end so
you can `flatpak search`/`install` it yourself, rather than guessing an
app id.
Destructive and asks for confirmation (`-y`/`--yes` to skip the prompt);
supports `--dry-run` to preview the plan first.
```sh
./desnap.sh --dry-run
./desnap.sh
```
## Notes
- `mirrors` benchmarks official Ubuntu mirrors (speed + a freshness check on
`Last-Modified`) and rewrites apt's sources to the fastest one — handles
both the legacy `sources.list` and the 24.04+/26.04 deb822
`ubuntu.sources` format. Runs early so the rest of the install benefits.
`security.ubuntu.com` is left alone on purpose.
- `firefox` removes the transitional apt stub / snap package first and pins
Mozilla's official apt repo, since stock Ubuntu ships Firefox as a snap.
- `docker`, `onedrive` each check whether their upstream repo has packages
for the running Ubuntu codename/version yet and fall back to the most
recent known-supported one if it's too new (relevant right after a fresh
26.04 install).
- `vscode` installs every extension listed in `vscode/extensions.txt`
(exported with `code --list-extensions`) and mirrors `settings.json` to
Cursor via symlink, so both stay in sync from one source of truth.
- `wakatime` prompts once for your API key and writes `~/.wakatime.cfg`
(chmod 600); leave the prompt blank to skip.
- Tool PATH/init lines are appended idempotently to the end of `zsh/.zshrc`
by `profile_add` in `install/lib.sh` — re-running a module never
duplicates a line.
Executable
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
export DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=install/lib.sh
source "$DOTFILES_DIR/install/lib.sh"
DRY_RUN=0
for a in "$@"; do
if [[ "$a" == "--dry-run" ]]; then DRY_RUN=1; fi
done
export DRY_RUN
STOW_PACKAGES=(zsh git tmux vscode)
if [[ "$DRY_RUN" == "1" ]]; then
log "[dry-run] would install base packages: stow zsh curl git"
log "[dry-run] would stow: ${STOW_PACKAGES[*]} -> \$HOME"
else
log "installing base packages"
apt_install stow zsh curl git
log "stowing dotfiles from $DOTFILES_DIR"
stow -v -t "$HOME" -d "$DOTFILES_DIR" "${STOW_PACKAGES[@]}"
fi
if [[ "$DRY_RUN" == "1" ]]; then
log "[dry-run] would prompt for git identity + signing key -> ~/.gitconfig.local"
elif [[ ! -f "$HOME/.gitconfig.local" ]]; then
read -rp "git user.name: " git_name
read -rp "git user.email [[email protected]]: " git_email
git_email="${git_email:-andwatiian@gmail.com}"
read -rp "git signing key (GPG key id) [1F20FED0AA05B0DA]: " git_signkey
git_signkey="${git_signkey:-1F20FED0AA05B0DA}"
{
echo "[user]"
echo " name = $git_name"
echo " email = $git_email"
if [[ -n "$git_signkey" ]]; then echo " signingkey = $git_signkey"; fi
} > "$HOME/.gitconfig.local"
ok "wrote $HOME/.gitconfig.local"
fi
if [[ "$DRY_RUN" == "1" ]]; then
if [[ "$SHELL" != *zsh* ]]; then log "[dry-run] would set default shell to zsh"; fi
elif [[ "$SHELL" != *zsh* ]]; then
log "setting zsh as default shell"
chsh -s "$(command -v zsh)" "$USER"
fi
log "running tool installer"
"$DOTFILES_DIR/install/install.sh" "$@"
ok "all done — open a new shell (or 'exec zsh') to pick up PATH changes"
-1
View File
@@ -1 +0,0 @@
Cached Theme.pak
-29
View File
@@ -1,29 +0,0 @@
{
"manifest_version": 3,
"name": "Everforest Dark",
"version": "1.0",
"description": "Everforest dark theme matching the desktop palette (bg #2b3339).",
"theme": {
"colors": {
"frame": [35, 42, 46],
"frame_inactive": [35, 42, 46],
"frame_incognito": [30, 35, 38],
"frame_incognito_inactive": [30, 35, 38],
"toolbar": [43, 51, 57],
"toolbar_text": [211, 198, 170],
"toolbar_button_icon": [167, 192, 128],
"tab_text": [211, 198, 170],
"tab_background_text": [133, 146, 137],
"bookmark_text": [211, 198, 170],
"button_background": [52, 63, 68],
"omnibox_background": [52, 63, 68],
"omnibox_text": [211, 198, 170],
"background_tab": [35, 42, 46],
"background_tab_inactive": [35, 42, 46],
"ntp_background": [43, 51, 57],
"ntp_text": [211, 198, 170],
"ntp_link": [167, 192, 128],
"ntp_header": [61, 72, 77]
}
}
}
Executable
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
set -euo pipefail
export DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=install/lib.sh
source "$DOTFILES_DIR/install/lib.sh"
DRY_RUN=0
ASSUME_YES=0
for a in "$@"; do
case "$a" in
--dry-run) DRY_RUN=1 ;;
-y|--yes) ASSUME_YES=1 ;;
esac
done
if ! has_cmd snap; then
ok "snapd not installed, nothing to do"
exit 0
fi
mapfile -t snaps < <(snap list 2>/dev/null | awk 'NR>1 {print $1}')
if [[ ${#snaps[@]} -eq 0 ]]; then
log "no snaps installed"
else
log "snaps currently installed: ${snaps[*]}"
fi
# snapd's own bundled core/bare snaps can't be removed while anything else
# depends on them, so they're removed last, in their own pass, below.
core_snaps=(snapd core core18 core20 core22 core24 bare)
# Known default-Ubuntu snaps mapped to their real, official-source
# replacement: an existing dotfiles module (reused so it stays one source of
# truth), a plain apt package, or "skip" for snap-only infra with no
# user-facing equivalent. Anything not listed here that isn't a GNOME/core
# runtime snap falls back to Flatpak/Flathub instead of a guessed package.
declare -A SNAP_REPLACEMENTS=(
[firefox]="module:firefox"
[thunderbird]="apt:thunderbird"
[snap-store]="apt:gnome-software"
[snapd-desktop-integration]="skip"
[gtk-common-themes]="skip"
)
_replacement_for() {
local snap="$1"
if [[ -n "${SNAP_REPLACEMENTS[$snap]:-}" ]]; then
echo "${SNAP_REPLACEMENTS[$snap]}"
return
fi
case "$snap" in
gnome-*|core*|bare) echo "skip" ;;
*) echo "unmapped" ;;
esac
}
declare -a to_remove=() modules_to_run=() apt_pkgs=() unmapped=()
for s in "${snaps[@]}"; do
case " ${core_snaps[*]} " in *" $s "*) continue ;; esac
to_remove+=("$s")
action=$(_replacement_for "$s")
case "$action" in
module:*) modules_to_run+=("${action#module:}") ;;
apt:*) apt_pkgs+=("${action#apt:}") ;;
skip) : ;;
unmapped) unmapped+=("$s") ;;
esac
done
if [[ "$DRY_RUN" == "1" ]]; then
log "[dry-run] would remove: ${to_remove[*]:-<none>} (plus core/base snaps)"
log "[dry-run] would purge snapd and pin /etc/apt/preferences.d/nosnap.pref"
if [[ ${#modules_to_run[@]} -gt 0 ]]; then
log "[dry-run] would reinstall via existing modules: ${modules_to_run[*]}"
fi
if [[ ${#apt_pkgs[@]} -gt 0 ]]; then
log "[dry-run] would reinstall via apt: ${apt_pkgs[*]}"
fi
if [[ ${#unmapped[@]} -gt 0 ]]; then
log "[dry-run] no apt equivalent for: ${unmapped[*]} — would set up Flatpak/Flathub instead"
fi
exit 0
fi
if [[ "$ASSUME_YES" != "1" ]]; then
warn "this removes ALL snap packages, purges snapd, and blocks it from ever being reinstalled by apt."
read -rp "continue? [y/N] " reply
[[ "$reply" =~ ^[Yy]$ ]] || { log "aborted"; exit 1; }
fi
for s in "${to_remove[@]}"; do
log "removing snap: $s"
sudo snap remove --purge "$s" || warn "failed to remove $s, continuing"
done
for s in "${core_snaps[@]}"; do
if snap list "$s" >/dev/null 2>&1; then
log "removing snap: $s"
sudo snap remove --purge "$s" || true
fi
done
log "purging snapd"
sudo apt-get purge -y snapd
sudo rm -rf /var/cache/snapd/ "$HOME/snap"
log "pinning snapd out of apt so it can't come back on upgrade"
sudo tee /etc/apt/preferences.d/nosnap.pref >/dev/null <<'EOF'
Package: snapd
Pin: release a=*
Pin-Priority: -10
EOF
for m in "${modules_to_run[@]}"; do
log "installing $m from its official source (snap replacement)"
# shellcheck disable=SC1090
source "$DOTFILES_DIR/install/modules/$m.sh"
"install_${m//-/_}"
done
if [[ ${#apt_pkgs[@]} -gt 0 ]]; then
log "installing apt replacements: ${apt_pkgs[*]}"
apt_install "${apt_pkgs[@]}"
fi
if [[ ${#unmapped[@]} -gt 0 ]]; then
warn "no apt equivalent known for: ${unmapped[*]} — setting up Flatpak/Flathub so you can still get them"
ensure_flatpak
warn "search/install with: flatpak search <name> && flatpak install flathub <app-id>"
for s in "${unmapped[@]}"; do
warn " - $s"
done
fi
ok "desnapped — snapd is purged and pinned out, replacements installed from apt/official sources"
-3
View File
@@ -1,3 +0,0 @@
theme = everforest-dark
background-opacity = 0.85
background-blur = true
@@ -1,22 +0,0 @@
palette = 0=#475258
palette = 1=#e67e80
palette = 2=#a7c080
palette = 3=#dbbc7f
palette = 4=#7fbbb3
palette = 5=#d699b6
palette = 6=#83c092
palette = 7=#d3c6aa
palette = 8=#859289
palette = 9=#e67e80
palette = 10=#a7c080
palette = 11=#dbbc7f
palette = 12=#7fbbb3
palette = 13=#d699b6
palette = 14=#83c092
palette = 15=#9da9a0
background = #2b3339
foreground = #d3c6aa
cursor-color = #d3c6aa
cursor-text = #2b3339
selection-background = #543a48
selection-foreground = #d3c6aa
+22 -61
View File
@@ -1,63 +1,24 @@
[user] [core]
name = andwati editor = vim
email = [email protected] excludesfile = ~/.gitignore_global
signingkey = 1F20FED0AA05B0DA
[commit]
gpgsign = true
[tag]
gpgSign = true
[init] [init]
defaultBranch = main defaultBranch = main
[color]
ui = true [pull]
[core] rebase = false
filemode = false
autocrlf = input [alias]
[credential "https://android.googlesource.com"] st = status
helper = co = checkout
helper = luci br = branch
useHttpPath = yes lg = log --oneline --graph --decorate --all
[credential "https://aomedia.googlesource.com"]
helper = [commit]
helper = luci gpgsign = true
useHttpPath = yes
[credential "https://beto-core.googlesource.com"] [tag]
helper = gpgSign = true
helper = luci
useHttpPath = yes [include]
[credential "https://boringssl.googlesource.com"] path = ~/.gitconfig.local
helper =
helper = luci
useHttpPath = yes
[credential "https://chromium.googlesource.com"]
helper =
helper = luci
useHttpPath = yes
[credential "https://chrome-internal.googlesource.com"]
helper =
helper = luci
useHttpPath = yes
[credential "https://dawn.googlesource.com"]
helper =
helper = luci
useHttpPath = yes
[credential "https://pdfium.googlesource.com"]
helper =
helper = luci
useHttpPath = yes
[credential "https://quiche.googlesource.com"]
helper =
helper = luci
useHttpPath = yes
[credential "https://skia.googlesource.com"]
helper =
helper = luci
useHttpPath = yes
[credential "https://swiftshader.googlesource.com"]
helper =
helper = luci
useHttpPath = yes
[credential "https://webrtc.googlesource.com"]
helper =
helper = luci
useHttpPath = yes
+4
View File
@@ -0,0 +1,4 @@
.DS_Store
*.swp
node_modules/
.env
-3
View File
@@ -1,3 +0,0 @@
default-cache-ttl 86400
max-cache-ttl 604800
pinentry-program /usr/bin/pinentry-qt
-75
View File
@@ -1,75 +0,0 @@
#!/usr/bin/env bash
# Bootstraps dotfiles on a new machine: installs oh-my-zsh + the custom
# theme/plugins .zshrc expects, then symlinks packages into $HOME via
# GNU Stow (each top-level dir is a package mirroring $HOME, e.g.
# zsh/.zshrc -> ~/.zshrc). Add a new package by adding a new top-level dir.
set -euo pipefail
DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
command -v stow >/dev/null || {
echo "GNU Stow is required: sudo pacman -S stow (or your distro's equivalent)" >&2
exit 1
}
clone_if_missing() {
local url="$1" dst="$2"
[ -d "$dst" ] || git clone --depth=1 "$url" "$dst"
}
if [ ! -d "$HOME/.oh-my-zsh" ]; then
echo "Installing oh-my-zsh..."
RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c \
"$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
fi
clone_if_missing https://github.com/romkatv/powerlevel10k.git "$ZSH_CUSTOM/themes/powerlevel10k"
clone_if_missing https://github.com/zsh-users/zsh-autosuggestions "$ZSH_CUSTOM/plugins/zsh-autosuggestions"
clone_if_missing https://github.com/zsh-users/zsh-syntax-highlighting.git "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting"
clone_if_missing https://github.com/zsh-users/zsh-history-substring-search "$ZSH_CUSTOM/plugins/zsh-history-substring-search"
clone_if_missing https://github.com/zsh-users/zsh-completions "$ZSH_CUSTOM/plugins/zsh-completions"
clone_if_missing https://github.com/MichaelAquilina/zsh-you-should-use "$ZSH_CUSTOM/plugins/you-should-use"
# Stow refuses to link over a real (non-symlink) file, so move any
# conflicting pre-existing file aside first (e.g. the .zshrc oh-my-zsh's
# own installer just wrote above).
stow_package() {
local pkg="$1" f rel target
while IFS= read -r -d '' f; do
rel="${f#"$DOTFILES_DIR/$pkg/"}"
target="$HOME/$rel"
if [ -L "$target" ]; then
rm "$target"
elif [ -e "$target" ]; then
mkdir -p "$(dirname "$target")"
mv "$target" "$target.bak.$(date +%s)"
echo "Backed up existing $target"
fi
done < <(find "$DOTFILES_DIR/$pkg" -type f -print0)
stow --dir="$DOTFILES_DIR" --target="$HOME" --restow "$pkg"
echo "Stowed $pkg"
}
mkdir -p "$HOME/.gnupg" && chmod 700 "$HOME/.gnupg"
stow_package zsh
stow_package gnupg
stow_package git
stow_package ghostty
stow_package kde
stow_package nvim
stow_package vscode
gpgconf --kill gpg-agent 2>/dev/null || true
cat <<'EOF'
Done. This does NOT install (grab these separately if missing):
- nvm, pyenv, bun, pnpm, deno, rustup, docker, fzf, zoxide
- ghostty, neovim (+ fd, lazygit, tree-sitter-cli, wl-clipboard), VS Code
- Colloid Everforest GTK/icon themes (AUR: colloid-everforest-gtk-theme-git,
colloid-everforest-theme-git)
- a Nerd Font (JetBrains Mono Nerd Font) — set it as your terminal's font
Start a new shell to pick everything up: exec zsh
EOF
+253
View File
@@ -0,0 +1,253 @@
#!/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 "$@"
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# Shared helpers sourced by install.sh, bootstrap.sh, desnap.sh, and every
# module in install/modules/.
C_RESET=$'\033[0m'; C_BLUE=$'\033[1;34m'; C_GREEN=$'\033[1;32m'; C_YELLOW=$'\033[1;33m'; C_RED=$'\033[1;31m'
log() { printf '%s==>%s %s\n' "$C_BLUE" "$C_RESET" "$*"; }
ok() { printf '%s ok%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
warn() { printf '%s warn%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; }
err() { printf '%s !!%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; }
has_cmd() { command -v "$1" >/dev/null 2>&1; }
deb_arch() { dpkg --print-architecture; }
ubuntu_codename() { . /etc/os-release; echo "$VERSION_CODENAME"; }
# Each module now runs in its own subshell (see run_module in install.sh),
# so a plain shell variable can't track "already updated" across modules —
# it'd reset every time. Use a marker file scoped to this whole run ($$ is
# stable across subshells) instead; apt_invalidate removes it when a repo
# changes so the next apt_install re-syncs.
_apt_marker() { echo "/tmp/.dotfiles-apt-updated.$$"; }
apt_update_once() {
local marker
marker=$(_apt_marker)
if [[ ! -f "$marker" ]]; then
sudo apt-get update -qq
touch "$marker"
fi
}
apt_invalidate() { rm -f "$(_apt_marker)"; }
# apt_install <pkg...> — installs packages and, for any package that wasn't
# already present, registers a rollback that removes it again. Packages the
# user already had are left alone on rollback.
apt_install() {
apt_update_once
local -a new_pkgs=()
local p
for p in "$@"; do
dpkg -s "$p" >/dev/null 2>&1 || new_pkgs+=("$p")
done
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"
if [[ ${#new_pkgs[@]} -gt 0 ]]; then
rollback_push "sudo DEBIAN_FRONTEND=noninteractive apt-get remove -y ${new_pkgs[*]}"
fi
}
# ensure_flatpak — installs flatpak (+ GNOME Software integration when
# running GNOME) and adds the Flathub remote system-wide. Shared by any
# module that prefers a verified Flathub app id over a vendor's own apt repo.
ensure_flatpak() {
apt_install flatpak
if has_cmd gnome-shell; then
apt_install gnome-software-plugin-flatpak
fi
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
}
# apt_add_repo <list-name> <keyring-path> <key-url> <repo-line>
# Registers a signed apt repo the way every vendor's official install docs do
# it (dearmor the key into /etc/apt/keyrings or similar, write a .list file),
# forces the next apt_install to refresh package lists, and registers a
# rollback that removes the repo + keyring again.
apt_add_repo() {
local list_name="$1" keyring="$2" key_url="$3" repo_line="$4"
sudo install -d -m 0755 "$(dirname "$keyring")"
curl -fsSL "$key_url" | sudo gpg --dearmor --yes -o "$keyring"
sudo chmod a+r "$keyring"
echo "$repo_line" | sudo tee "/etc/apt/sources.list.d/${list_name}.list" >/dev/null
rollback_push "sudo rm -f '/etc/apt/sources.list.d/${list_name}.list' '$keyring'"
apt_invalidate
}
add_line_once() {
local file="$1" line="$2"
mkdir -p "$(dirname "$file")"
touch "$file"
grep -qxF "$line" "$file" || printf '%s\n' "$line" >> "$file"
}
# profile_add <line> — idempotently append a PATH/init line to ~/.zshrc so
# re-running a module never duplicates it. Not rollback-tracked: a stray
# export pointing at a directory that got rolled back is inert, not broken.
profile_add() { add_line_once "$HOME/.zshrc" "$1"; }
# --- transactional rollback -------------------------------------------------
#
# Each module gets a fresh rollback stack (see run_module in install.sh).
# Anything a module does that should be undone if a *later* command in the
# same module fails should call rollback_push with a shell command string.
# On success the stack is simply discarded; on failure it's run in reverse.
ROLLBACK_ACTIONS=()
rollback_reset() { ROLLBACK_ACTIONS=(); }
rollback_push() { ROLLBACK_ACTIONS+=("$1"); }
rollback_run() {
local i
for (( i=${#ROLLBACK_ACTIONS[@]}-1; i>=0; i-- )); do
warn "rollback: ${ROLLBACK_ACTIONS[$i]}"
eval "${ROLLBACK_ACTIONS[$i]}" || true
done
ROLLBACK_ACTIONS=()
}
track_new_dir() { rollback_push "rm -rf '$1'"; }
track_new_sudo_dir() { rollback_push "sudo rm -rf '$1'"; }
+9
View File
@@ -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
}
+7
View File
@@ -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"'
}
+14
View File
@@ -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"
}
+3
View File
@@ -0,0 +1,3 @@
install_cli_tools() {
apt_install eza ripgrep
}
+36
View File
@@ -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
}
+7
View File
@@ -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"'
}
+10
View File
@@ -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"
}
+20
View File
@@ -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"
}
+20
View File
@@ -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
}
+7
View File
@@ -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
}
+15
View File
@@ -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.)"
}
+34
View File
@@ -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"'
}
+24
View File
@@ -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"
}
+79
View File
@@ -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"
}
+3
View File
@@ -0,0 +1,3 @@
install_mpv() {
apt_install mpv
}
+12
View File
@@ -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/*'
}
+20
View File
@@ -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"'
}
+21
View File
@@ -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
}
+22
View File
@@ -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
}
+12
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
install_pipx() {
apt_install pipx
profile_add 'export PATH="$HOME/.local/bin:$PATH"'
}
+7
View File
@@ -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'
}
+7
View File
@@ -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"'
}
+22
View File
@@ -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
}
+25
View File
@@ -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
}
+16
View File
@@ -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)"'
}
+7
View File
@@ -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"'
}
+7
View File
@@ -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
}
+10
View File
@@ -0,0 +1,10 @@
install_slack() {
if flatpak list --app 2>/dev/null | grep -q com.slack.Slack; then
ok "slack already installed"
return
fi
ensure_flatpak
sudo flatpak install -y flathub com.slack.Slack
rollback_push "sudo flatpak uninstall -y com.slack.Slack"
}
+7
View File
@@ -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
}
+23
View File
@@ -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
}
+6
View File
@@ -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"'
}
+54
View File
@@ -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
}
+36
View File
@@ -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"
}
+65
View File
@@ -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"
}
-11
View File
@@ -1,11 +0,0 @@
[Daemon]
Autolock=false
LockOnResume=false
Timeout=0
[Greeter]
WallpaperPlugin=org.kde.image
[Greeter][Wallpaper][org.kde.image][General]
Image=file:///home/ian/.local/share/wallpapers/everforest/1-tree-tops.webp
PreviewImage=file:///home/ian/.local/share/wallpapers/everforest/1-tree-tops.webp
@@ -1,108 +0,0 @@
[ColorEffects:Disabled]
Color=47,56,62
ColorAmount=0.75
ColorEffect=3
ContrastAmount=0.65
ContrastEffect=0
IntensityAmount=0.1
IntensityEffect=0
[ColorEffects:Inactive]
ChangeSelectionColor=true
Color=47,56,62
ColorAmount=0.47500000000000003
ColorEffect=2
ContrastAmount=0.1
ContrastEffect=0
Enable=true
IntensityAmount=0.1
IntensityEffect=0
[Colors:Button]
BackgroundAlternate=64,69,82
BackgroundNormal=55,66,71
DecorationFocus=167,192,128
DecorationHover=167,192,128
ForegroundActive=167,192,128
ForegroundInactive=122,132,120
ForegroundLink=127,187,179
ForegroundNegative=230,126,128
ForegroundNeutral=219,188,127
ForegroundNormal=211,198,170
ForegroundPositive=141,161,1
ForegroundVisited=214,153,182
[Colors:Complementary]
BackgroundNormal=42,46,50
[Colors:Selection]
BackgroundAlternate=147,178,89
BackgroundNormal=167,192,128
DecorationFocus=167,192,128
DecorationHover=167,192,128
ForegroundActive=47,56,62
ForegroundInactive=122,132,120
ForegroundLink=127,187,179
ForegroundNegative=230,126,128
ForegroundNeutral=219,188,127
ForegroundNormal=47,56,62
ForegroundPositive=141,161,1
ForegroundVisited=214,153,182
[Colors:Tooltip]
BackgroundAlternate=47,52,63
BackgroundNormal=55,66,71
DecorationFocus=167,192,128
DecorationHover=167,192,128
ForegroundActive=167,192,128
ForegroundInactive=122,132,120
ForegroundLink=127,187,179
ForegroundNegative=230,126,128
ForegroundNeutral=219,188,127
ForegroundNormal=211,198,170
ForegroundPositive=141,161,1
ForegroundVisited=214,153,182
[Colors:View]
BackgroundAlternate=55,66,71
BackgroundNormal=43,51,57
DecorationFocus=167,192,128
DecorationHover=167,192,128
ForegroundActive=167,192,128
ForegroundInactive=122,132,120
ForegroundLink=127,187,179
ForegroundNegative=230,126,128
ForegroundNeutral=219,188,127
ForegroundNormal=211,198,170
ForegroundPositive=141,161,1
ForegroundVisited=214,153,182
[Colors:Window]
BackgroundAlternate=55,66,71
BackgroundNormal=47,56,62
DecorationFocus=167,192,128
DecorationHover=167,192,128
ForegroundActive=167,192,128
ForegroundInactive=122,132,120
ForegroundLink=127,187,179
ForegroundNegative=230,126,128
ForegroundNeutral=219,188,127
ForegroundNormal=211,198,170
ForegroundPositive=141,161,1
ForegroundVisited=214,153,182
[General]
ColorScheme=Everforest
Name=Everforest
shadeSortColumn=true
[KDE]
contrast=4
[WM]
activeBackground=47,56,62
activeBlend=47,56,62
activeForeground=211,198,170
inactiveBackground=47,56,62
inactiveBlend=47,56,62
inactiveForeground=122,132,120
Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

-8
View File
@@ -1,8 +0,0 @@
tt.*
.tests
doc/tags
debug
.repro
foo.*
*.log
data
-15
View File
@@ -1,15 +0,0 @@
{
"neodev": {
"library": {
"enabled": true,
"plugins": true
}
},
"neoconf": {
"plugins": {
"lua_ls": {
"enabled": true
}
}
}
}
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-2
View File
@@ -1,2 +0,0 @@
-- bootstrap lazy.nvim, LazyVim and your plugins
require("config.lazy")
-39
View File
@@ -1,39 +0,0 @@
{
"LazyVim": { "branch": "main", "commit": "999700997f72227187d49d8b92667183dc7fc809" },
"blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" },
"bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" },
"catppuccin": { "branch": "main", "commit": "edefef779ab08ce1a4a404713e3012b0d202bd35" },
"clangd_extensions.nvim": { "branch": "main", "commit": "78c2ecd659d54972be17aa6ba2deac3c53223b80" },
"conform.nvim": { "branch": "master", "commit": "016802de402556da54c36bd7359b441266b01cdd" },
"crates.nvim": { "branch": "main", "commit": "7039bc1d130e424232a7aa64ceddb170a154ff5b" },
"everforest-nvim": { "branch": "main", "commit": "a0e9edc57379e8feafc6ba207c26dbb12fc1b6d8" },
"flash.nvim": { "branch": "main", "commit": "5f0f270fdc7c5b0c21d903ee85b9cb06f2ac636a" },
"friendly-snippets": { "branch": "main", "commit": "b4d01b0fdf3c9a549961c2f9ffe8dc09be166219" },
"gitsigns.nvim": { "branch": "main", "commit": "851a051e2bd2caba97e23314c68d69f1b1cf5d19" },
"grug-far.nvim": { "branch": "main", "commit": "11595bf747edc270bce2069d1020502ad4ae56cf" },
"lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" },
"lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" },
"lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" },
"mason-lspconfig.nvim": { "branch": "main", "commit": "b3298993d55fa194279b5eb9dbfb3a43da65cadf" },
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
"mini.ai": { "branch": "main", "commit": "c2c4278beb759aa24b9506f56a67186a6cc2209c" },
"mini.icons": { "branch": "main", "commit": "f642e3ba8c12afbc8f32f6d77ef6597eac56786f" },
"mini.pairs": { "branch": "main", "commit": "b1c5a726921b7a8c9321e9a7a208aa0571de5810" },
"noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" },
"nui.nvim": { "branch": "main", "commit": "10fc361835c856ba4233ef5ea135b919bf3dce97" },
"nvim-lint": { "branch": "master", "commit": "3d55c8f67c6ae5c15e1042571e107c7a3d5c5f4e" },
"nvim-lspconfig": { "branch": "master", "commit": "33497f4930d40f28a465d38a763a00fc6902c658" },
"nvim-treesitter": { "branch": "main", "commit": "e289100ff98969e118c702199d88b764ce9e7fdf" },
"nvim-treesitter-textobjects": { "branch": "main", "commit": "5c7b0263797dfd1bd6202f2b219f3b53a80b2187" },
"nvim-ts-autotag": { "branch": "main", "commit": "d7220b55e16ed3c04e88086256def5d282429493" },
"persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" },
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
"rustaceanvim": { "branch": "main", "commit": "a968f5133b8b24f481de12f08cd79420d1ace559" },
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
"todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" },
"tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" },
"trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" },
"ts-comments.nvim": { "branch": "main", "commit": "a59d6092213447450191122c9346f309161504cb" },
"venv-selector.nvim": { "branch": "main", "commit": "cc4bb3975de8835291f9bb45889e96c6b2795fc4" },
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
}
-10
View File
@@ -1,10 +0,0 @@
{
"extras": [
],
"install_version": 8,
"news": {
"NEWS.md": "11866"
},
"version": 8
}
-17
View File
@@ -1,17 +0,0 @@
-- Autocmds are automatically loaded on the VeryLazy event
-- Default autocmds that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/autocmds.lua
--
-- Add any additional autocmds here
-- with `vim.api.nvim_create_autocmd`
--
-- Or remove existing autocmds by their group name (which is prefixed with `lazyvim_` for the defaults)
-- e.g. vim.api.nvim_del_augroup_by_name("lazyvim_wrap_spell")
-- enter terminal mode whenever a terminal buffer is focused
vim.api.nvim_create_autocmd({ "TermOpen", "BufEnter", "WinEnter" }, {
group = vim.api.nvim_create_augroup("term_autoinsert", { clear = true }),
pattern = "term://*",
callback = function()
vim.cmd("startinsert")
end,
})
-3
View File
@@ -1,3 +0,0 @@
-- Keymaps are automatically loaded on the VeryLazy event
-- Default keymaps that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/keymaps.lua
-- Add any additional keymaps here
-60
View File
@@ -1,60 +0,0 @@
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
if vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
{ out, "WarningMsg" },
{ "\nPress any key to exit..." },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup({
spec = {
-- add LazyVim and import its plugins
{ "LazyVim/LazyVim", import = "lazyvim.plugins" },
-- language support (LSP, treesitter, formatters, debuggers)
{ import = "lazyvim.plugins.extras.lang.zig" },
{ import = "lazyvim.plugins.extras.lang.python" },
{ import = "lazyvim.plugins.extras.lang.typescript" },
{ import = "lazyvim.plugins.extras.lang.rust" },
{ import = "lazyvim.plugins.extras.lang.go" },
{ import = "lazyvim.plugins.extras.lang.clangd" },
-- import/override with your plugins
{ import = "plugins" },
},
defaults = {
-- By default, only LazyVim plugins will be lazy-loaded. Your custom plugins will load during startup.
-- If you know what you're doing, you can set this to `true` to have all your custom plugins lazy-loaded by default.
lazy = false,
-- It's recommended to leave version=false for now, since a lot the plugin that support versioning,
-- have outdated releases, which may break your Neovim install.
version = false, -- always use the latest git commit
-- version = "*", -- try installing the latest stable version for plugins that support semver
},
install = { colorscheme = { "tokyonight", "habamax" } },
checker = {
enabled = true, -- check for plugin updates periodically
notify = false, -- notify on update
}, -- automatically check for plugin updates
performance = {
rtp = {
-- disable some rtp plugins
disabled_plugins = {
"gzip",
-- "matchit",
-- "matchparen",
-- "netrwPlugin",
"tarPlugin",
"tohtml",
"tutor",
"zipPlugin",
},
},
},
})
-3
View File
@@ -1,3 +0,0 @@
-- Options are automatically loaded before lazy.nvim startup
-- Default options that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/options.lua
-- Add any additional options here
@@ -1,19 +0,0 @@
return {
{
"neanias/everforest-nvim",
lazy = false,
priority = 1000,
config = function()
require("everforest").setup({
background = "medium",
transparent_background_level = 2,
})
end,
},
{
"LazyVim/LazyVim",
opts = {
colorscheme = "everforest",
},
},
}
-3
View File
@@ -1,3 +0,0 @@
indent_type = "Spaces"
indent_width = 2
column_width = 120
-62
View File
@@ -1,62 +0,0 @@
#!/bin/bash
GITHUB_USERNAME="andwati"
DESTINATION_FOLDER=~/dev
# Define colors
GREEN="\e[32m"
BLUE="\e[34m"
RED="\e[31m"
RESET="\e[0m"
clone_repositories() {
local parent_dir=$1
local repo_name=$2
# Clone the repository using GitHub CLI
echo -e "${BLUE}Cloning repository: $repo_name${RESET}"
if gh repo clone "$GITHUB_USERNAME/$repo_name" "$parent_dir/$repo_name"; then
echo -e "${GREEN}Successfully cloned: $repo_name${RESET}"
else
echo -e "${RED}Failed to clone: $repo_name${RESET}"
return
fi
# Move into the cloned repository folder
cd "$parent_dir/$repo_name" || exit
# If there are submodules, initialize and update them
if [ -f ".gitmodules" ]; then
echo -e "${BLUE}Initializing and updating submodules for: $repo_name${RESET}"
git submodule init
git submodule update
fi
}
main() {
# Ensure the destination folder exists
mkdir -p "$DESTINATION_FOLDER"
# Fetch the list of repositories for the user using GitHub CLI, including private and public repos
echo -e "${BLUE}Fetching repositories for user: $GITHUB_USERNAME${RESET}"
repos=$(gh repo list "$GITHUB_USERNAME" --visibility public --limit 100 --json name -q '.[].name')
private_repos=$(gh repo list "$GITHUB_USERNAME" --visibility private --limit 100 --json name -q '.[].name')
# Combine public and private repositories
repos="$repos $private_repos"
# Initialize progress variables
total_repos=$(echo "$repos" | wc -w)
count=0
# Loop through each repository and clone it
for repo in $repos; do
count=$((count + 1))
echo -e "${BLUE}Progress: [$count/$total_repos]${RESET}"
clone_repositories "$DESTINATION_FOLDER" "$repo"
done
echo -e "${GREEN}Cloning complete! ${RESET}"
}
main
+7
View File
@@ -0,0 +1,7 @@
set -g mouse on
set -g history-limit 10000
set -g base-index 1
setw -g pane-base-index 1
setw -g mode-keys vi
set -g status-style bg=black,fg=white
set -g status-right '%Y-%m-%d %H:%M'
+45 -10
View File
@@ -1,12 +1,4 @@
{ {
"workbench.colorTheme": "Everforest Dark",
"workbench.productIconTheme": "material-product-icons",
"workbench.iconTheme": "material-icon-theme",
"editor.fontFamily": "'JetBrainsMono Nerd Font','Droid Sans Mono', monospace",
"workbench.browser.searchEngine": "google",
"workbench.browser.openLocalhostLinks": false,
"diffEditor.renderSideBySide": true,
"diffEditor.useInlineViewWhenSpaceIsLimited": true,
"json.schemaDownload.trustedDomains": { "json.schemaDownload.trustedDomains": {
"https://biomejs.dev": true, "https://biomejs.dev": true,
"https://developer.microsoft.com/json-schemas/": true, "https://developer.microsoft.com/json-schemas/": true,
@@ -15,9 +7,52 @@
"https://raw.githubusercontent.com/devcontainers/spec/": true, "https://raw.githubusercontent.com/devcontainers/spec/": true,
"https://raw.githubusercontent.com/microsoft/vscode/": true, "https://raw.githubusercontent.com/microsoft/vscode/": true,
"https://schemastore.azurewebsites.net/": true, "https://schemastore.azurewebsites.net/": true,
"https://turbo.build": true,
"https://turborepo.dev": true,
"https://ui.shadcn.com": true,
"https://www.schemastore.org/": true "https://www.schemastore.org/": true
}, },
"python.analysis.typeCheckingMode": "strict", "claudeCode.preferredLocation": "panel",
"git.confirmSync": false,
"diffEditor.renderSideBySide": false,
"git.autofetch": true, "git.autofetch": true,
"git.confirmSync": false "git.enableSmartCommit": true,
"files.autoSave": "onFocusChange",
"editor.formatOnSave": true,
"gitlens.ai.model": "vscode",
"gitlens.ai.vscode.model": "copilot:gpt-4.1",
"security.allowedUNCHosts": [
"wsl.localhost"
],
"diffEditor.ignoreTrimWhitespace": true,
"redhat.telemetry.enabled": false,
"explorer.confirmDelete": false,
"chat.viewSessions.orientation": "stacked",
"snyk.securityAtInception.autoConfigureSnykMcpServer": true,
"snyk.securityAtInception.executionFrequency": "On Code Generation",
"snyk.yesWelcomeNotification": false,
"diffEditor.maxComputationTime": 0,
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"workbench.colorTheme": "GitHub Dark",
"workbench.iconTheme": "material-icon-theme",
"workbench.productIconTheme": "material-product-icons",
"workbench.editor.enablePreview": false,
"cSpell.enabledNotifications": {
"Lines too Long": false,
"Average Word Length too Long": false,
"Maximum Word Length Exceeded": false
},
"editor.fontFamily": "'Monaspace Argon NF','Droid Sans Mono', monospace",
"terminal.integrated.fontFamily": "'Monaspace Argon NF'",
"cSpell.userWords": [
"Mikro"
],
"editor.foldingImportsByDefault": true,
"js/ts.updateImportsOnFileMove.enabled": "always",
"editor.codeActionsOnSave": {
}
} }
+77
View File
@@ -0,0 +1,77 @@
ahmadalli.vscode-nginx-conf
alexcvzz.vscode-sqlite
angular.ng-template
anthropic.claude-code
biomejs.biome
bradlc.vscode-tailwindcss
catppuccin.catppuccin-vsc
charliermarsh.ruff
dart-code.dart-code
dart-code.flutter
davidanson.vscode-markdownlint
dbaeumer.vscode-eslint
donjayamanne.githistory
dsznajder.es7-react-js-snippets
eamodio.gitlens
esbenp.prettier-vscode
expo.vscode-expo-tools
github.github-vscode-theme
github.vscode-github-actions
github.vscode-pull-request-github
golang.go
llvm-vs-code-extensions.lldb-dap
mechatroner.rainbow-csv
mermaidchart.vscode-mermaid-chart
mhutchie.git-graph
ms-azuretools.vscode-containers
ms-azuretools.vscode-docker
ms-kubernetes-tools.vscode-kubernetes-tools
ms-python.black-formatter
ms-python.debugpy
ms-python.python
ms-python.vscode-pylance
ms-python.vscode-python-envs
ms-toolsai.jupyter
ms-toolsai.jupyter-keymap
ms-toolsai.jupyter-renderers
ms-toolsai.vscode-jupyter-cell-tags
ms-toolsai.vscode-jupyter-slideshow
ms-vscode-remote.remote-containers
ms-vscode.cmake-tools
ms-vscode.cpp-devtools
ms-vscode.cpptools
ms-vscode.cpptools-extension-pack
ms-vscode.cpptools-themes
ms-vscode.makefile-tools
ms-vscode.powershell
ms-vscode.vscode-chat-customizations-evaluations
njpwerner.autodocstring
njqdev.vscode-python-typehint
nuxtr.nuxtr-vscode
openai.chatgpt
phplasma.csv-to-table
pkief.material-icon-theme
pkief.material-product-icons
prisma.prisma
redhat.java
redhat.vscode-yaml
redis.redis-for-vscode
ritwickdey.liveserver
rust-lang.rust-analyzer
samuelcolvin.jinjahtml
shakram02.bash-beautify
snyk-security.snyk-vulnerability-scanner
streetsidesoftware.code-spell-checker
tamasfe.even-better-toml
tomoki1207.pdf
twxs.cmake
usernamehw.errorlens
vadimcn.vscode-lldb
vscjava.vscode-gradle
vscjava.vscode-java-debug
vscjava.vscode-java-dependency
vscjava.vscode-java-pack
vscjava.vscode-java-test
vscjava.vscode-maven
vue.volar
wakatime.vscode-wakatime
-1841
View File
File diff suppressed because it is too large Load Diff
-10
View File
@@ -1,10 +0,0 @@
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
# Created by `pipx` on 2026-07-26 14:32:10
export PATH="$PATH:/home/ian/.local/bin"
# Added by Antigravity CLI installer
export PATH="/home/ian/.local/bin:$PATH"
+9
View File
@@ -0,0 +1,9 @@
alias ls='eza'
alias ll='eza -lah'
alias la='eza -a'
alias lt='eza --tree'
alias grep='rg'
alias gs='git status'
alias gd='git diff'
alias gco='git checkout'
alias dc='docker compose'
-1
View File
@@ -1 +0,0 @@
. "$HOME/.cargo/env"
+10 -195
View File
@@ -1,200 +1,15 @@
# Add deno completions to search path HISTFILE="$HOME/.zsh_history"
if [[ ":$FPATH:" != *":/home/ian/.zsh/completions:"* ]]; then export FPATH="/home/ian/.zsh/completions:$FPATH"; fi HISTSIZE=50000
# Enable Powerlevel10k instant prompt. Must stay close to the top of ~/.zshrc. SAVEHIST=50000
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then setopt HIST_IGNORE_DUPS SHARE_HISTORY EXTENDED_HISTORY INC_APPEND_HISTORY
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
# Add deno completions to search path autoload -Uz compinit && compinit
if [[ ":$FPATH:" != *":$HOME/.zsh/completions:"* ]]; then export FPATH="$HOME/.zsh/completions:$FPATH"; fi
# If you come from bash you might have to change your $PATH.
# export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH
# Path to your Oh My Zsh installation. export EDITOR="vim"
export ZSH="$HOME/.oh-my-zsh" export PATH="$HOME/.local/bin:$PATH"
# Set name of the theme to load --- if set to "random", it will [[ -f "$HOME/.zsh_aliases" ]] && source "$HOME/.zsh_aliases"
# load a random theme each time Oh My Zsh is loaded, in which case,
# to know which specific one was loaded, run: echo $RANDOM_THEME
# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
ZSH_THEME="powerlevel10k/powerlevel10k"
# Set list of themes to pick from when loading at random PROMPT='%F{blue}%~%f %(?.%F{green}.%F{red})❯%f '
# Setting this variable when ZSH_THEME=random will cause zsh to load
# a theme from this variable instead of looking in $ZSH/themes/
# If set to an empty array, this variable will have no effect.
# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" )
# Uncomment the following line to use case-sensitive completion. # --- devenv installers (managed by install/lib.sh profile_add, appended below) ---
# CASE_SENSITIVE="true"
# Uncomment the following line to use hyphen-insensitive completion.
# Case-sensitive completion must be off. _ and - will be interchangeable.
# HYPHEN_INSENSITIVE="true"
# Uncomment one of the following lines to change the auto-update behavior
# zstyle ':omz:update' mode disabled # disable automatic updates
# zstyle ':omz:update' mode auto # update automatically without asking
# zstyle ':omz:update' mode reminder # just remind me to update when it's time
# Uncomment the following line to change how often to auto-update (in days).
# zstyle ':omz:update' frequency 13
# Uncomment the following line if pasting URLs and other text is messed up.
# DISABLE_MAGIC_FUNCTIONS="true"
# Uncomment the following line to disable colors in ls.
# DISABLE_LS_COLORS="true"
# Uncomment the following line to disable auto-setting terminal title.
# DISABLE_AUTO_TITLE="true"
# Uncomment the following line to enable command auto-correction.
# ENABLE_CORRECTION="true"
# Uncomment the following line to display red dots whilst waiting for completion.
# You can also set it to another string to have that shown instead of the default red dots.
# e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f"
# Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765)
# COMPLETION_WAITING_DOTS="true"
# Uncomment the following line if you want to disable marking untracked files
# under VCS as dirty. This makes repository status check for large repositories
# much, much faster.
# DISABLE_UNTRACKED_FILES_DIRTY="true"
# Uncomment the following line if you want to change the command execution time
# stamp shown in the history command output.
# You can set one of the optional three formats:
# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
# or set a custom format using the strftime function format specifications,
# see 'man strftime' for details.
# HIST_STAMPS="mm/dd/yyyy"
# Would you like to use another custom folder than $ZSH/custom?
# ZSH_CUSTOM=/path/to/new-custom-folder
# Which plugins would you like to load?
# Standard plugins can be found in $ZSH/plugins/
# Custom plugins may be added to $ZSH_CUSTOM/plugins/
# Example format: plugins=(rails git textmate ruby lighthouse)
# Add wisely, as too many plugins slow down shell startup.
plugins=(
git sudo extract archlinux docker docker-compose fzf
zsh-completions you-should-use
zsh-autosuggestions zsh-syntax-highlighting zsh-history-substring-search
)
source $ZSH/oh-my-zsh.sh
# To customize the prompt, run `p10k configure` or edit ~/.p10k.zsh.
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
# Up/Down arrows filter history by what's already typed, not just cycle it.
bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down
# zoxide: smarter `cd` that learns frequently/recently used directories.
command -v zoxide >/dev/null && eval "$(zoxide init zsh)"
# User configuration
# Needed for gpg-agent to prompt on the right terminal when pinentry falls
# back to a tty (e.g. pinentry-curses); harmless no-op with pinentry-qt.
export GPG_TTY=$(tty)
# export MANPATH="/usr/local/man:$MANPATH"
# You may need to manually set your language environment
# export LANG=en_US.UTF-8
# Preferred editor for local and remote sessions
# if [[ -n $SSH_CONNECTION ]]; then
# export EDITOR='vim'
# else
# export EDITOR='nvim'
# fi
# Compilation flags
# export ARCHFLAGS="-arch $(uname -m)"
# Set personal aliases, overriding those provided by Oh My Zsh libs,
# plugins, and themes. Aliases can be placed here, though Oh My Zsh
# users are encouraged to define aliases within a top-level file in
# the $ZSH_CUSTOM folder, with .zsh extension. Examples:
# - $ZSH_CUSTOM/aliases.zsh
# - $ZSH_CUSTOM/macos.zsh
# For a full list of active aliases, run `alias`.
#
# Example aliases
# alias zshconfig="mate ~/.zshrc"
# alias ohmyzsh="mate ~/.oh-my-zsh"
# eza: colourful, icon-rich listings that stay clean when piped.
alias ls='eza --color=auto --icons=auto --hyperlink=auto --group-directories-first'
alias l='eza -lah --git --color-scale=all --color=auto --icons=auto --hyperlink=auto --group-directories-first'
alias ll='eza -lh --git --color-scale=all --color=auto --icons=auto --hyperlink=auto --group-directories-first'
alias la='eza -lAh --git --color-scale=all --color=auto --icons=auto --hyperlink=auto --group-directories-first'
alias lt='eza --tree --level=2 --color=auto --icons=auto --hyperlink=auto --group-directories-first'
# nvm: the lazy-load stub (wrapper functions deferring to a helper that
# sourced nvm.sh on first real use) was unreliable in some non-interactive
# shell contexts, where the wrapper could run without the helper function
# being defined yet. Sourcing eagerly costs 300-700ms on shell startup but
# always works.
export NVM_DIR="$HOME/.nvm"
() {
# oh-my-zsh sets extendedglob, which breaks nvm's `lts/*`-style alias
# globs and silently kills auto-activation of the default version.
# local_options scopes this to the anonymous function, restoring the
# prior setting once nvm.sh is sourced.
setopt local_options no_extended_glob
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
}
# Lazy-load pyenv's shell integration (~130-150ms here). Shims are
# already on PATH via .zprofile, so `python`/`pip` work immediately
# without this; it's only needed for `pyenv shell/activate/...` and
# completions, which this loads for real on first actual `pyenv` call.
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
pyenv() {
unfunction pyenv
eval "$(command pyenv init - zsh)"
pyenv "$@"
}
# bun completions
[ -s "$HOME/.bun/_bun" ] && source "$HOME/.bun/_bun"
# bun
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
# pnpm
export PNPM_HOME="/home/ian/.local/share/pnpm"
case ":$PATH:" in
*":$PNPM_HOME/bin:"*) ;;
*) export PATH="$PNPM_HOME/bin:$PATH" ;;
esac
# pnpm end
[ -s "$HOME/.deno/env" ] && . "$HOME/.deno/env"
# Created by `pipx` on 2026-07-26 14:32:10
export PATH="$PATH:/home/ian/.local/bin"
. "/home/ian/.deno/env"
# Java (Amazon Corretto 21) and Android SDK, for Android Studio / Gradle builds.
export JAVA_HOME="/usr/lib/jvm/java-21-amazon-corretto"
export ANDROID_HOME="$HOME/Android/Sdk"
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"
# Added by Antigravity CLI installer
export PATH="/home/ian/.local/bin:$PATH"
# Resend CLI
export PATH="$HOME/.resend/bin:$PATH"
export PATH="/home/ian/depot_tools:$PATH"