Dotfiles/setup/modules/shell-setup.sh

273 lines
15 KiB
Bash
Executable File

#!/bin/bash
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ setup/modules/shell-setup.sh — Shell environment deployment ║
# ║ ║
# ║ PURPOSE: ║
# ║ Installs and configures the full shell environment: zsh, oh-my-zsh, ║
# ║ plugins, neovim with lazy.nvim plugins, yazi, starship prompt, and ║
# ║ deploys all dotfile symlinks to their correct locations. ║
# ║ ║
# ║ WHEN TO RUN: ║
# ║ After core-packages.sh (needs base packages). Runs as the logged-in ║
# ║ user — must NOT run as root. ║
# ║ ║
# ║ WHAT GETS DEPLOYED: ║
# ║ ~/.bashrc, ~/.zshrc — symlinks to Dotfiles repo ║
# ║ ~/.config/starship.toml — symlink to repo ║
# ║ ~/.config/nvim/ — symlink to repo/nvim/ ║
# ║ ~/.config/micro/ — copy from repo/micro/ (plugin state needs cp)║
# ║ ~/.config/alot/ — symlink for email client config ║
# ║ ~/.config/yazi/ — symlink for file manager config ║
# ║ ~/Pictures/fflogo.svg — logo asset ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
set -euo pipefail
# -e: any failed package install or symlink operation aborts the module
# -u: unset variable references are errors
# -o pipefail: catch pipe failures
# Load shared logging helpers (log, skip, warn, err functions)
source "$(dirname "${BASH_SOURCE[0]}")/lib/logging.sh"
# ── System update ──────────────────────────────────────────────────────────────
# WHY: Ensure the package database is current before installing to avoid
# dependency conflicts on a rolling-release distro like Arch.
log "Updating system..."
sudo pacman -Syu --noconfirm
# ── Base shell packages ────────────────────────────────────────────────────────
# WHY: Install package-by-package with idempotency checks rather than one big
# pacman call. This lets us skip already-installed packages individually
# and produce useful "already installed" log messages.
# Each package in PACKAGES is checked with `pacman -Qi` (query installed).
#
# Package notes:
# zsh — primary shell (replaces bash as default)
# neovim — primary editor (configured via dotfiles/nvim/)
# pyright — Python LSP server for neovim (coc/LSP plugin)
# bash-language-server — bash LSP for neovim
# btop — interactive resource monitor
# clang — C/C++ toolchain; needed by treesitter + some nvim plugins
# fastfetch — system info display on terminal open
# fzf — fuzzy finder; used by shell history, yazi, neovim
# hyfetch — pride-themed neofetch variant
# lua-language-server — Lua LSP for Hyprland config editing in neovim
# micro — beginner-friendly terminal text editor (alternative to nano)
# pulsemixer — TUI PulseAudio/PipeWire mixer
# yazi — terminal file manager with image preview
# z — smart directory jumper (learns frequent paths)
# qrencode — QR code generator for terminal
# distrobox — run other Linux distros in containers
# dysk — disk usage summary (prettier df alternative)
# glow — render markdown in the terminal
# notmuch — fast email indexer (used by alot mail client)
# alot — TUI email client built on notmuch
log "Installing base shell packages..."
PACKAGES=(zsh neovim curl pyright bash atftp bash-language-server btop clang fastfetch fzf hyfetch lua-language-server micro nano pulsemixer yazi z qrencode distrobox dysk python python-pip glow notmuch alot)
for pkg in "${PACKAGES[@]}"; do
# -Qi queries the local package database; if it returns non-zero, pkg is not installed
if ! pacman -Qi "$pkg" &>/dev/null; then
log "Installing $pkg..."
# --needed: skip if already at latest version (double-safety with -Qi check)
sudo pacman -S --noconfirm --needed "$pkg"
else
skip "$pkg already installed."
fi
done
# ── abook (AUR) ───────────────────────────────────────────────────────────────
# WHY: abook is an address book app that integrates with mail clients like alot.
# It is only available in the AUR (not in official repos), so we use yay.
# IDEMPOTENCY: Check if `abook` binary exists before installing.
if ! command -v abook &>/dev/null; then
log "Installing abook (AUR)..."
yay -S --noconfirm --needed abook
else
skip "abook already installed."
fi
# ── yay fallback ──────────────────────────────────────────────────────────────
# WHY: shell-setup.sh can run standalone (e.g. on an existing system that didn't
# go through package-managers.sh first). This guards against yay being absent.
# HOW: Build yay from the AUR using its PKGBUILD. /tmp/yay is ephemeral but fine
# for a build that completes synchronously.
if ! command -v yay &>/dev/null; then
log "Installing yay..."
sudo pacman -S --noconfirm --needed git base-devel
git clone https://aur.archlinux.org/yay.git /tmp/yay
cd /tmp/yay && makepkg -si --noconfirm
cd ~
else
skip "yay already installed."
fi
# ── Rust / Cargo ──────────────────────────────────────────────────────────────
# WHY: Some shell tools are installed via `cargo install`. This provides the
# Rust toolchain in the user's home directory (~/.cargo/) separate from
# any system-wide Rust installation.
# NOTE: Uses the official rustup installer script rather than the pacman package
# because it installs to ~/.cargo which is user-owned (no sudo needed for
# cargo install later). The -y flag disables interactive prompts.
if ! command -v cargo &>/dev/null; then
log "Installing Rust & Cargo..."
# --proto: restrict to HTTPS only for security
# --tlsv1.2: require TLS 1.2 minimum
# -s -- -y: pass -y to the installer script (non-interactive)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# Source cargo env so `cargo` is available in this shell session immediately
. "$HOME/.cargo/env"
else
skip "Rust & Cargo already installed."
fi
# ── nvm + Node.js ─────────────────────────────────────────────────────────────
# WHY: Same rationale as in package-managers.sh. shell-setup.sh can run
# independently so nvm is bootstrapped here as a fallback.
if ! command -v node &>/dev/null; then
log "Installing nvm and Node.js..."
if [ ! -d "$HOME/.nvm" ]; then
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash
fi
# Source nvm into current shell to make the `nvm` command available immediately
. "$HOME/.nvm/nvm.sh"
nvm install 22
else
skip "Node.js already installed."
fi
# ── Git global configuration ───────────────────────────────────────────────────
# WHY: Set neovim as the commit message editor so that `git commit` without -m
# opens neovim instead of vi/nano. This is user-global config (not per-repo).
log "Configuring git..."
git config --global core.editor nvim
# ── Dotfile deployment ────────────────────────────────────────────────────────
# WHY: We use symlinks (ln -sf) rather than copies wherever possible so that
# edits to files in ~/Dotfiles/ are immediately reflected in the live config
# without needing to re-run this script.
# EXCEPTION: micro/ is copied because micro stores plugin state files in its
# config directory — symlinks would write plugin data back to the repo.
log "Deploying dotfiles..."
mkdir -p ~/.config ~/Pictures
# Defensive: an earlier root/sudo step (e.g. the installer creating
# ~/.config/Yubico for FIDO before the user-level config exists) can leave
# ~/.config owned by root. `mkdir -p` above won't fix that, and every symlink
# below would then fail with "Permission denied". If ~/.config isn't writable by
# us, reclaim the whole tree. Non-fatal so it never blocks the rest of setup.
if [[ -e "$HOME/.config" && ! -w "$HOME/.config" ]]; then
warn "~/.config is not writable — reclaiming ownership for $(id -un)..."
sudo chown -R "$(id -un):$(id -gn)" "$HOME/.config" 2>/dev/null || true
fi
# Shell init files — symlink so edits in the repo apply immediately
ln -sf ~/Dotfiles/.bashrc ~/.bashrc
ln -sf ~/Dotfiles/.zshrc ~/.zshrc
# Starship prompt config — symlink into ~/.config/
ln -sf ~/Dotfiles/starship.toml ~/.config/starship.toml
# Micro editor — copy (not symlink) because micro writes plugin/state data into
# its config directory and we don't want those writes going into the git repo
rm -rf ~/.config/micro
cp -r ~/Dotfiles/micro ~/.config/
# Neovim — symlink the entire config directory to the repo
rm -rf ~/.config/nvim
ln -sf ~/Dotfiles/nvim ~/.config/nvim
# ── Neovim plugin sync ────────────────────────────────────────────────────────
# WHY: lazy.nvim (the Neovim plugin manager) needs to download and compile plugins
# on first launch. We run it headlessly here so the first interactive launch
# is fast and plugins are ready.
# HOW: --headless runs neovim without a UI; +Lazy! sync triggers lazy.nvim's
# sync command; +qa quits after sync completes.
# || true: a failed sync is non-fatal — user can run :Lazy sync manually later.
log "Syncing neovim plugins (lazy.nvim)..."
nvim --headless "+Lazy! sync" +qa 2>/dev/null || true
# alot email client config — symlink
rm -rf ~/.config/alot
ln -sf ~/Dotfiles/alot ~/.config/alot
# yazi file manager config — symlink
rm -rf ~/.config/yazi
ln -sf ~/Dotfiles/yazi ~/.config/yazi
# spotify-tui config — symlink (for the TUI Spotify client)
rm -rf ~/.config/spotify-tui
ln -sf ~/Dotfiles/spotify-tui ~/.config/spotify-tui
# Copy FF (Fastfetch) logo SVG to ~/Pictures for the fastfetch config to reference
cp -f ~/Dotfiles/resources/fflogo.svg ~/Pictures/fflogo.svg
# ── Starship prompt ────────────────────────────────────────────────────────────
# WHY: Starship is a fast, customizable cross-shell prompt. The config is in
# dotfiles/starship.toml (symlinked above). The binary itself must be
# installed separately — the official install script handles arch/platform
# detection automatically.
# IDEMPOTENCY: Check for the `starship` binary before running the installer.
if ! command -v starship &>/dev/null; then
log "Installing Starship..."
# -sS: silent but show errors; --yes: non-interactive
curl -sS https://starship.rs/install.sh | sh -s -- --yes
else
skip "Starship already installed."
fi
# ── oh-my-zsh ─────────────────────────────────────────────────────────────────
# WHY: oh-my-zsh provides the plugin framework, completion system, and themes
# that our .zshrc configuration depends on. It installs to ~/.oh-my-zsh/.
# HOW: Download and run the official install script.
# --unattended: skip ALL interactive prompts — crucially the "overwrite .zshrc?"
# question, which otherwise hangs an automated install. It also
# implies RUNZSH=no and CHSH=no.
# KEEP_ZSHRC=yes: do NOT replace ~/.zshrc with the oh-my-zsh template. We already
# symlinked ~/.zshrc to the dotfiles copy above; without this the
# installer would clobber that symlink with its own template.
# RUNZSH=no / CHSH=no kept explicit for clarity (we change the shell ourselves below).
if [ ! -d "$HOME/.oh-my-zsh" ]; then
log "Installing oh-my-zsh..."
KEEP_ZSHRC=yes RUNZSH=no CHSH=no \
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
else
skip "oh-my-zsh already installed."
fi
# ── oh-my-zsh plugins ─────────────────────────────────────────────────────────
# WHY: These two plugins are referenced in .zshrc and must exist in ZSH_CUSTOM.
# They are not bundled with oh-my-zsh and must be cloned separately.
# ZSH_CUSTOM defaults to ~/.oh-my-zsh/custom/ — the standard plugin install location.
ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
# zsh-syntax-highlighting: colors commands as you type (green=valid, red=invalid)
if [ ! -d "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" ]; then
log "Installing zsh-syntax-highlighting..."
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git \
"$ZSH_CUSTOM/plugins/zsh-syntax-highlighting"
else
skip "zsh-syntax-highlighting already installed."
fi
# zsh-autosuggestions: suggests previously-typed commands in gray as you type
if [ ! -d "$ZSH_CUSTOM/plugins/zsh-autosuggestions" ]; then
log "Installing zsh-autosuggestions..."
git clone https://github.com/zsh-users/zsh-autosuggestions \
"$ZSH_CUSTOM/plugins/zsh-autosuggestions"
else
skip "zsh-autosuggestions already installed."
fi
# ── Default shell change ───────────────────────────────────────────────────────
# WHY: New login shells still default to bash unless explicitly changed.
# `chsh` writes the new shell to /etc/passwd for this user.
# HOW: Compare current $SHELL to /usr/bin/zsh and change if different.
if [ "$SHELL" != "/usr/bin/zsh" ]; then
log "Setting zsh as default shell..."
chsh -s /usr/bin/zsh
else
skip "zsh is already the default shell."
fi
log "Shell setup complete."