#!/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. # RUNZSH=no: don't switch to zsh and start a new shell during install # CHSH=no: don't automatically change the default shell (we do that explicitly below) if [ ! -d "$HOME/.oh-my-zsh" ]; then log "Installing oh-my-zsh..." RUNZSH=no CHSH=no sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" 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."