diff --git a/README.md b/README.md new file mode 100644 index 0000000..598072c --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# dotfiles + +David's personal dotfiles. Clean, minimal, actually deployable. + +## What's in here + +| File | Description | +|------|-------------| +| `zshrc` | Zsh config — prompt, history, aliases, tool integrations | +| `gitconfig` | Git aliases, modern defaults, color config | +| `gitignore` | Global git ignores | +| `tmux.conf` | Tmux config — Tokyo Night theme, sane bindings | +| `tmux.mac.conf` | macOS-specific tmux tweaks (pbcopy) | +| `ghostty.config` | Ghostty terminal config — Tokyo Night, Monaspace font | +| `nvim/` | Neovim config (LazyVim-based, Ruby + Copilot extras) | + +## Approach + +No dotfiles framework. Just a git repo + symlinks. + +**Why?** +- v1–v3 used increasingly complex module systems. Too much ceremony. +- v4 proved that a flat file of configs works fine. +- v5 adds neovim and a simple install script — nothing more. + +## Install + +```bash +git clone https://github.com/davidstosik/dotfiles ~/.dotfiles +cd ~/.dotfiles +./install.sh +``` + +The install script: +1. Symlinks config files into `$HOME` and `$HOME/.config/` +2. Installs TPM (tmux plugin manager) if missing +3. Prints next steps for Neovim and tools + +## Prerequisites + +- macOS with [Homebrew](https://brew.sh) +- Install tools you actually use: + +```bash +brew install git tmux neovim mise gh the_silver_searcher fzf +brew install --cask ghostty +``` + +For 1Password CLI (used for shell completion in zshrc): +```bash +brew install 1password-cli +``` + +For NVM (Node version manager, used in zshrc): +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +``` + +## Machine-local overrides + +For machine-specific settings, create files that won't be committed: + +- `~/.gitconfig.local` — included by gitconfig (name, email, work stuff) +- `~/.zshrc.local` — sourced at end of zshrc if it exists + +Example `~/.gitconfig.local`: +```ini +[user] + name = David Stosik + email = work@example.com +``` + +## VPN fix + +The `vpn-fix` function in zshrc fixes routing when there's a subnet conflict +between your home LAN (192.168.1.0/24) and a WireGuard VPN. Run it after +connecting to VPN if home devices become unreachable. + +## Updating + +```bash +cd ~/.dotfiles +git pull +./install.sh # re-runs symlinks, safe to run multiple times +``` diff --git a/gitconfig b/gitconfig index 44ba892..e2fd408 100644 --- a/gitconfig +++ b/gitconfig @@ -1,3 +1,11 @@ +# gitconfig — David Stosik's Git config +# Machine-specific settings (name/email/work stuff) go in ~/.gitconfig.local + +[include] + # Per-machine overrides: name, email, work remotes, etc. + # Create this file — it won't be committed to dotfiles. + path = ~/.gitconfig.local + [user] name = David Stosik email = 816901+davidstosik@users.noreply.github.com @@ -13,71 +21,84 @@ [init] defaultBranch = main +# ── Aliases ─────────────────────────────────────────────────────────────────── [alias] - addp = add --patch - ci = commit --verbose - cia = commit --amend --no-edit - ciam = commit --amend - cim = commit --verbose --message - co = checkout - cob = checkout -b - diffc = diff --cached + addp = add --patch + ci = commit --verbose + cia = commit --amend --no-edit + ciam = commit --amend + cim = commit --verbose --message + co = checkout + cob = checkout -b + diffc = diff --cached + lol = log --decorate --pretty=format:'%C(yellow)%h%Creset -%C(auto)%d%Creset %s %Cgreen(%cd) %C(bold blue)<%an>%Creset' --abbrev-commit --date=auto:human + pushf = push --force-with-lease + recent = branch --sort=-committerdate --format="%(committerdate:relative)%09%(refname:short)" + st = status + unstage = reset HEAD find-merge = "!sh -c 'commit=$0 && branch=${1:-HEAD} && (git rev-list $commit..$branch --ancestry-path | cat -n; git rev-list $commit..$branch --first-parent | cat -n) | sort -k2 -s | uniq -f1 -d | sort -n | tail -1 | cut -f2'" - lol = log --decorate --pretty=format:'%C(yellow)%h%Creset -%C(auto)%d%Creset %s %Cgreen(%cd) %C(bold blue)<%an>%Creset' --abbrev-commit --date=auto:human - pushf = push --force-with-lease - recent = branch --sort=-committerdate --format=\"%(committerdate:relative)%09%(refname:short)\" - st = status - unstage = reset HEAD +# ── Colors ──────────────────────────────────────────────────────────────────── [column] ui = auto [color "status"] - header = white dim - added = green bold # or updated - changed = yellow bold - untracked = red bold dim - branch = magenta bold - nobranch = red bold - localBranch = magenta bold - remoteBranch = magenta bold - unmerged = red bold ul + header = white dim + added = green bold + changed = yellow bold + untracked = red bold dim + branch = magenta bold + nobranch = red bold + localBranch = magenta bold + remoteBranch = magenta bold + unmerged = red bold ul [color "diff"] - meta = yellow - context = white dim - whitespace = red reverse - frag = magenta bold - old = red - new = green + meta = yellow + context = white dim whitespace = red reverse + frag = magenta bold + old = red + new = green +# ── Branch & tag sorting ────────────────────────────────────────────────────── [tag] sort = version:refname [branch] sort = -committerdate +# ── Rebase ──────────────────────────────────────────────────────────────────── [rebase] autoSquash = true - autoStash = true + autoStash = true updateRefs = true +# ── Diff ────────────────────────────────────────────────────────────────────── [diff] - algorithm = histogram - colorMoved = plain + algorithm = histogram + colorMoved = plain mnemonicPrefix = true - renames = true + renames = true +# ── Push / Fetch ───────────────────────────────────────────────────────────── [push] autoSetupRemote = true - followTags = true + followTags = true [fetch] - prune = true + prune = true pruneTags = true - all = true + all = true + +# ── Merge ───────────────────────────────────────────────────────────────────── +[merge] + conflictstyle = zdiff3 +[pull] + rebase = true + +# ── Misc ────────────────────────────────────────────────────────────────────── [help] autocorrect = prompt @@ -85,14 +106,8 @@ verbose = true [rerere] - enabled = true + enabled = true autoupdate = true -[merge] - conflictstyle = zdiff3 - -[pull] - rebase = true - [feature] experimental = true diff --git a/gitignore b/gitignore index 597f4f6..6f8d578 100644 --- a/gitignore +++ b/gitignore @@ -1,11 +1,16 @@ -*.DS_Store +# macOS +.DS_Store -# Vim +# Vim swap files *.swp *.swo - *~ +# Log files *.log -.claude \ No newline at end of file +# Editor/tool state +.claude + +# Machine-local overrides (not committed — put them in ~/.zshrc.local / ~/.gitconfig.local) +*.local diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..1a6c84c --- /dev/null +++ b/install.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# install.sh — set up dotfiles via symlinks +# Safe to run multiple times. Won't overwrite files that aren't symlinks. + +set -euo pipefail + +DOTFILES="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BACKUP_DIR="$HOME/.dotfiles-backup/$(date +%Y%m%d-%H%M%S)" + +info() { echo " [·] $*"; } +success() { echo " [✓] $*"; } +warn() { echo " [!] $*"; } +error() { echo " [✗] $*" >&2; } + +# Symlink $1 (source in dotfiles) to $2 (target in $HOME) +# Backs up existing non-symlink files before replacing. +link_file() { + local src="$1" + local dst="$2" + local dst_dir + dst_dir="$(dirname "$dst")" + + mkdir -p "$dst_dir" + + if [ -L "$dst" ]; then + # Already a symlink — update it + ln -sf "$src" "$dst" + success "Updated symlink: $dst → $src" + elif [ -e "$dst" ]; then + # Existing file, back it up + mkdir -p "$BACKUP_DIR" + mv "$dst" "$BACKUP_DIR/$(basename "$dst")" + warn "Backed up existing file: $dst → $BACKUP_DIR/" + ln -s "$src" "$dst" + success "Linked: $dst → $src" + else + ln -s "$src" "$dst" + success "Linked: $dst → $src" + fi +} + +echo "" +echo "=== Dotfiles install ===" +echo "" + +# ── Home directory configs ────────────────────────────────────────────────── +info "Linking home directory configs..." +link_file "$DOTFILES/zshrc" "$HOME/.zshrc" +link_file "$DOTFILES/gitconfig" "$HOME/.gitconfig" +link_file "$DOTFILES/gitignore" "$HOME/.gitignore" +link_file "$DOTFILES/tmux.conf" "$HOME/.tmux.conf" +link_file "$DOTFILES/tmux.mac.conf" "$HOME/.tmux.mac.conf" + +# ── Ghostty ───────────────────────────────────────────────────────────────── +info "Linking Ghostty config..." +link_file "$DOTFILES/ghostty.config" "$HOME/.config/ghostty/config" + +# ── Neovim ────────────────────────────────────────────────────────────────── +if [ -d "$DOTFILES/nvim" ]; then + info "Linking Neovim config..." + link_file "$DOTFILES/nvim" "$HOME/.config/nvim" +fi + +# ── TPM (Tmux Plugin Manager) ──────────────────────────────────────────────── +TPM_DIR="$HOME/.tmux/plugins/tpm" +if [ ! -d "$TPM_DIR" ]; then + info "Installing TPM (Tmux Plugin Manager)..." + git clone --depth=1 https://github.com/tmux-plugins/tpm "$TPM_DIR" + success "TPM installed. Start tmux and press prefix + I to install plugins." +else + success "TPM already installed." +fi + +echo "" +echo "=== Done! ===" +echo "" +echo "Next steps:" +echo " 1. Restart your shell or run: source ~/.zshrc" +echo " 2. Start tmux, press C-a + I to install tmux plugins" +echo " 3. Run nvim — LazyVim will bootstrap on first launch" +echo " 4. Create ~/.gitconfig.local for machine-specific git settings" +echo " 5. Create ~/.zshrc.local for machine-specific shell settings" +echo "" diff --git a/nvim/init.lua b/nvim/init.lua new file mode 100644 index 0000000..5a6bb98 --- /dev/null +++ b/nvim/init.lua @@ -0,0 +1,5 @@ +-- nvim/init.lua +-- LazyVim bootstrap — plugins and config live in lua/ +-- See: https://www.lazyvim.org/ + +require("config.lazy") diff --git a/nvim/lua/config/autocmds.lua b/nvim/lua/config/autocmds.lua new file mode 100644 index 0000000..8a81128 --- /dev/null +++ b/nvim/lua/config/autocmds.lua @@ -0,0 +1,12 @@ +-- Autocmds are loaded on the VeryLazy event +-- LazyVim defaults: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/autocmds.lua + +-- Add your custom autocmds here +-- Example: +-- vim.api.nvim_create_autocmd("FileType", { +-- pattern = "ruby", +-- callback = function() +-- vim.opt_local.tabstop = 2 +-- vim.opt_local.shiftwidth = 2 +-- end, +-- }) diff --git a/nvim/lua/config/keymaps.lua b/nvim/lua/config/keymaps.lua new file mode 100644 index 0000000..9202bd8 --- /dev/null +++ b/nvim/lua/config/keymaps.lua @@ -0,0 +1,5 @@ +-- Keymaps are loaded on the VeryLazy event +-- LazyVim defaults: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/keymaps.lua + +-- Add your custom keymaps here +-- Example: vim.keymap.set("n", "X", ":SomeCommand", { desc = "Do something" }) diff --git a/nvim/lua/config/lazy.lua b/nvim/lua/config/lazy.lua new file mode 100644 index 0000000..103bfa5 --- /dev/null +++ b/nvim/lua/config/lazy.lua @@ -0,0 +1,64 @@ +-- Bootstrap lazy.nvim plugin manager +-- Clones lazy.nvim on first launch if not present + +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 = { + -- LazyVim base — opinionated defaults, great for daily use + { "LazyVim/LazyVim", import = "lazyvim.plugins" }, + + -- Extras — opt into what you actually use + { import = "lazyvim.plugins.extras.coding.mini-surround" }, + { import = "lazyvim.plugins.extras.coding.yanky" }, + { import = "lazyvim.plugins.extras.lang.json" }, + { import = "lazyvim.plugins.extras.lang.markdown" }, + { import = "lazyvim.plugins.extras.lang.ruby" }, + { import = "lazyvim.plugins.extras.ui.treesitter-context" }, + + -- Your custom plugins (see lua/plugins/) + { import = "plugins" }, + }, + defaults = { + -- Don't lazy-load by default for snappy startups + lazy = false, + -- Use latest stable versions + version = false, + }, + install = { + colorscheme = { "tokyonight", "habamax" }, + }, + checker = { + -- Automatically check for plugin updates (notification only) + enabled = true, + notify = false, + }, + performance = { + rtp = { + -- Disable unused built-in plugins + disabled_plugins = { + "gzip", + "tarPlugin", + "tohtml", + "tutor", + "zipPlugin", + }, + }, + }, +}) diff --git a/nvim/lua/config/options.lua b/nvim/lua/config/options.lua new file mode 100644 index 0000000..76cd97f --- /dev/null +++ b/nvim/lua/config/options.lua @@ -0,0 +1,12 @@ +-- Options are loaded before lazy.nvim startup +-- LazyVim defaults: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/options.lua + +-- Sync clipboard with system (works with macOS pbcopy/pbpaste) +vim.opt.clipboard = "unnamedplus" + +-- Disable autoformat by default — run manually with cf +vim.g.autoformat = false + +-- Use the system Ruby's neovim gem if available +-- Adjust path for rbenv or mise if needed +-- vim.g.ruby_host_prog = vim.fn.expand("~/.rbenv/shims/neovim-ruby-host") diff --git a/nvim/lua/plugins/colorscheme.lua b/nvim/lua/plugins/colorscheme.lua new file mode 100644 index 0000000..d5753a7 --- /dev/null +++ b/nvim/lua/plugins/colorscheme.lua @@ -0,0 +1,22 @@ +-- Colorscheme: Tokyo Night +-- Matches the terminal (Ghostty) and tmux themes for a unified look. +-- https://github.com/folke/tokyonight.nvim + +return { + { + "folke/tokyonight.nvim", + lazy = false, + priority = 1000, + opts = { + style = "night", -- "night" | "storm" | "day" | "moon" + transparent = true, -- matches terminal background opacity + terminal_colors = true, + }, + }, + { + "LazyVim/LazyVim", + opts = { + colorscheme = "tokyonight", + }, + }, +} diff --git a/nvim/lua/plugins/ruby.lua b/nvim/lua/plugins/ruby.lua new file mode 100644 index 0000000..c134889 --- /dev/null +++ b/nvim/lua/plugins/ruby.lua @@ -0,0 +1,17 @@ +-- Ruby-specific plugins and settings +-- LazyVim already includes ruby extras via lazyvim.plugins.extras.lang.ruby +-- (solargraph/rubocop LSP, treesitter, etc.) — add overrides here. + +return { + -- vim-rails: Rails-aware navigation, :Emodel, :Econtroller, etc. + { + "tpope/vim-rails", + ft = { "ruby", "eruby" }, + }, + + -- vim-bundler: aware of Gemfile, adds :Bundle command + { + "tpope/vim-bundler", + ft = "ruby", + }, +} diff --git a/nvim/lua/plugins/ui.lua b/nvim/lua/plugins/ui.lua new file mode 100644 index 0000000..8a468b5 --- /dev/null +++ b/nvim/lua/plugins/ui.lua @@ -0,0 +1,11 @@ +-- UI tweaks + +return { + -- Markdown headlines (bold, colored headers in Markdown files) + { + "lukas-reineke/headlines.nvim", + dependencies = "nvim-treesitter/nvim-treesitter", + ft = { "markdown", "norg", "rmd", "org" }, + opts = {}, + }, +} diff --git a/nvim/stylua.toml b/nvim/stylua.toml new file mode 100644 index 0000000..0f90030 --- /dev/null +++ b/nvim/stylua.toml @@ -0,0 +1,3 @@ +indent_type = "Spaces" +indent_width = 2 +column_width = 120 diff --git a/zshrc b/zshrc index f569fd2..8ee7d04 100644 --- a/zshrc +++ b/zshrc @@ -1,65 +1,102 @@ -# Sync command history between panes -setopt SHARE_HISTORY -setopt INC_APPEND_HISTORY -setopt HIST_IGNORE_DUPS -setopt HIST_IGNORE_ALL_DUPS +# ───────────────────────────────────────────────────────────────────────────── +# zshrc — David Stosik's Zsh config +# ───────────────────────────────────────────────────────────────────────────── + +# ── History ────────────────────────────────────────────────────────────────── HISTSIZE=10000 SAVEHIST=10000 +setopt SHARE_HISTORY # sync history across sessions +setopt INC_APPEND_HISTORY # write to history immediately +setopt HIST_IGNORE_DUPS # don't store duplicate consecutive entries +setopt HIST_IGNORE_ALL_DUPS # remove older duplicate entries +# ── Environment ────────────────────────────────────────────────────────────── export EDITOR=vim -bindkey -e # for emacs keys in shell despite EDITOR -bindkey "^[[3~" delete-char # restore Delete key in tmux +export PATH="$HOME/.local/bin:$HOME/bin:$PATH" -alias g=git -alias hist='fc -lt "%F %T"' +# ── Key bindings ───────────────────────────────────────────────────────────── +bindkey -e # emacs key bindings (even though EDITOR=vim) +bindkey "^[[3~" delete-char # restore Delete key in tmux +# ── Completion ─────────────────────────────────────────────────────────────── autoload -U colors && colors autoload -Uz vcs_info autoload -Uz compinit && compinit -if command -v op &> /dev/null; then - eval "$(op completion zsh)" ; compdef _op op -fi - -# FIXME: should probably be in .zprofile on Mac -if command -v gt &> /dev/null; then - eval "$(gt completion)" -fi +# ── Prompt ─────────────────────────────────────────────────────────────────── +# Two-line prompt: path + git branch on line 1, $ on line 2 +# Colors: green path, blue git info, yellow unstaged (*), green staged (!) -if command -v mise &> /dev/null; then - eval "$(mise activate zsh)" -fi - -precmd_update_vcs_info() { - vcs_info -} +precmd_update_vcs_info() { vcs_info } precmd_functions+=(precmd_update_vcs_info) setopt prompt_subst default_vcs_info_color=$fg[blue] zstyle ':vcs_info:*' enable git -zstyle ':vcs_info:git:*' formats "%{$default_vcs_info_color%}(%b%u%c)%{$reset_color%}" -zstyle ':vcs_info:git:*' actionformats "%{$default_vcs_info_color%}(%b|%{$fg_bold[magenta]%}%a%{$default_vcs_info_color%}%u%c)%{$reset_color%}" +zstyle ':vcs_info:git:*' formats "%{$default_vcs_info_color%}(%b%u%c)%{$reset_color%}" +zstyle ':vcs_info:git:*' actionformats "%{$default_vcs_info_color%}(%b|%{$fg_bold[magenta]%}%a%{$default_vcs_info_color%}%u%c)%{$reset_color%}" zstyle ':vcs_info:git:*' check-for-changes true -zstyle ':vcs_info:git:*' unstagedstr "%{$fg_bold[yellow]%}*%{$default_vcs_info_color%}" -zstyle ':vcs_info:git:*' stagedstr "%{$fg_bold[green]%}!%{$default_vcs_info_color%}" +zstyle ':vcs_info:git:*' unstagedstr "%{$fg_bold[yellow]%}*%{$default_vcs_info_color%}" +zstyle ':vcs_info:git:*' stagedstr "%{$fg_bold[green]%}!%{$default_vcs_info_color%}" PROMPT=$'\n''%{$fg[green]%}%~%{$reset_color%} ${vcs_info_msg_0_}'$'\n''%{$fg[white]%}%{$dim%}%(!.#.$)%{$reset_color%} ' -RPROMPT='' # '%{$fg[cyan]%}%T%{$reset_color%}' +RPROMPT='' # Uncomment for right-side clock: '%{$fg[cyan]%}%T%{$reset_color%}' + +# ── Aliases ─────────────────────────────────────────────────────────────────── +alias g=git +alias hist='fc -lt "%F %T"' + +# ── Homebrew (macOS) ───────────────────────────────────────────────────────── +[[ -x /opt/homebrew/bin/brew ]] && eval "$(/opt/homebrew/bin/brew shellenv)" # Apple Silicon +[[ -x /usr/local/bin/brew ]] && eval "$(/usr/local/bin/brew shellenv)" # Intel -if command -v tmux &> /dev/null && [[ -z "$TMUX" ]]; then +# ── Tmux — auto-attach on login ─────────────────────────────────────────────── +# Opens (or re-attaches to) a tmux session named after your username. +if command -v tmux &>/dev/null && [[ -z "$TMUX" ]]; then tmux new-session -A -s "$USER" fi +# ── mise (runtime version manager — replaces rbenv/nvm/pyenv) ──────────────── +if command -v mise &>/dev/null; then + eval "$(mise activate zsh)" +fi + +# ── NVM (Node Version Manager) ─────────────────────────────────────────────── +# Keep if you prefer nvm over mise for Node. Remove if using mise. export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm -[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion -export PATH="$HOME/.local/bin:$PATH" +[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" +[ -s "$NVM_DIR/bash_completion" ] && source "$NVM_DIR/bash_completion" +# ── 1Password CLI completions ───────────────────────────────────────────────── +if command -v op &>/dev/null; then + eval "$(op completion zsh)" + compdef _op op +fi +# ── Graphite (gt) completions ───────────────────────────────────────────────── +if command -v gt &>/dev/null; then + eval "$(gt completion)" +fi + +# ── FZF ────────────────────────────────────────────────────────────────────── +# Fuzzy finder — install with: brew install fzf +if command -v fzf &>/dev/null; then + # Use ag for fzf file search if available + if command -v ag &>/dev/null; then + export FZF_DEFAULT_COMMAND='ag --hidden --ignore .git -g ""' + export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" + fi + # Load fzf shell integration (keybindings + completion) + source <(fzf --zsh) 2>/dev/null || true +fi + +# ── VPN fix ─────────────────────────────────────────────────────────────────── +# Fixes routing when home LAN (192.168.1.0/24) conflicts with WireGuard VPN. +# Run after connecting to VPN if home devices become unreachable. vpn-fix() { - local iface=$(ifconfig | awk '/^utun/{iface=$1} /10\.10\.10\./{gsub(/:$/,"",iface); print iface}') + local iface + iface=$(ifconfig | awk '/^utun/{iface=$1} /10\.10\.10\./{gsub(/:$/,"",iface); print iface}') if [[ -z "$iface" ]]; then echo "WireGuard interface not found — is VPN connected?" return 1 @@ -69,6 +106,11 @@ vpn-fix() { return 0 fi sudo -v -p "sudo password required to add route: " && \ - sudo route add 192.168.1.0/24 -interface "$iface" &>/dev/null && \ - echo "✓ Route added: 192.168.1.0/24 → $iface" + sudo route add 192.168.1.0/24 -interface "$iface" &>/dev/null && \ + echo "✓ Route added: 192.168.1.0/24 → $iface" } + +# ── Local overrides ─────────────────────────────────────────────────────────── +# Machine-specific settings that aren't committed to git. +# Create ~/.zshrc.local for per-machine config (work email, custom paths, etc.) +[[ -f "$HOME/.zshrc.local" ]] && source "$HOME/.zshrc.local"